diff options
Diffstat (limited to 'src/plugins')
634 files changed, 38262 insertions, 20357 deletions
diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 4c3b3a39b4..d26d2aae44 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from plugins.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(TARGET Qt::Sql) add_subdirectory(sqldrivers) @@ -24,3 +25,6 @@ if (TARGET Qt::Network) add_subdirectory(networkinformation) add_subdirectory(tls) endif() +if (QT_FEATURE_ctf AND TARGET Qt::Network) + add_subdirectory(tracing) +endif() diff --git a/src/plugins/doc/snippets/code/src_plugins_platforms_qnx_qqnxwindow.cpp b/src/plugins/doc/snippets/code/src_plugins_platforms_qnx_qqnxwindow.cpp index f0071aeafe..6b6e2966b2 100644 --- a/src/plugins/doc/snippets/code/src_plugins_platforms_qnx_qqnxwindow.cpp +++ b/src/plugins/doc/snippets/code/src_plugins_platforms_qnx_qqnxwindow.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause //! [0] QQuickView *view = new QQuickView(parent); diff --git a/src/plugins/generic/CMakeLists.txt b/src/plugins/generic/CMakeLists.txt index f5890d0961..6d3cf2a925 100644 --- a/src/plugins/generic/CMakeLists.txt +++ b/src/plugins/generic/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from generic.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_evdev) add_subdirectory(evdevmouse) @@ -18,6 +19,6 @@ if(QT_FEATURE_libinput) add_subdirectory(libinput) endif() if(FREEBSD) - # add_subdirectory(bsdkeyboard) # special case TODO - # add_subdirectory(bsdmouse) # special case TODO + # add_subdirectory(bsdkeyboard) # TODO: QTBUG-112770 + # add_subdirectory(bsdmouse) # TODO: QTBUG-112770 endif() diff --git a/src/plugins/generic/evdevkeyboard/CMakeLists.txt b/src/plugins/generic/evdevkeyboard/CMakeLists.txt index 1568ac82cb..53be12face 100644 --- a/src/plugins/generic/evdevkeyboard/CMakeLists.txt +++ b/src/plugins/generic/evdevkeyboard/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from evdevkeyboard.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEvdevKeyboardPlugin Plugin: @@ -17,7 +18,3 @@ qt_internal_add_plugin(QEvdevKeyboardPlugin Qt::GuiPrivate Qt::InputSupportPrivate ) - -#### Keys ignored in scope 1:.:.:evdevkeyboard.pro:<TRUE>: -# OTHER_FILES = "evdevkeyboard.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/evdevkeyboard/main.cpp b/src/plugins/generic/evdevkeyboard/main.cpp index 00d4c216d8..2931fea907 100644 --- a/src/plugins/generic/evdevkeyboard/main.cpp +++ b/src/plugins/generic/evdevkeyboard/main.cpp @@ -27,7 +27,8 @@ QObject* QEvdevKeyboardPlugin::create(const QString &key, { if (!key.compare(QLatin1String("EvdevKeyboard"), Qt::CaseInsensitive)) return new QEvdevKeyboardManager(key, specification); - return 0; + + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/evdevmouse/CMakeLists.txt b/src/plugins/generic/evdevmouse/CMakeLists.txt index f467f631f5..e0836ecccc 100644 --- a/src/plugins/generic/evdevmouse/CMakeLists.txt +++ b/src/plugins/generic/evdevmouse/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from evdevmouse.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEvdevMousePlugin Plugin: @@ -17,7 +18,3 @@ qt_internal_add_plugin(QEvdevMousePlugin Qt::GuiPrivate Qt::InputSupportPrivate ) - -#### Keys ignored in scope 1:.:.:evdevmouse.pro:<TRUE>: -# OTHER_FILES = "evdevmouse.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/evdevmouse/main.cpp b/src/plugins/generic/evdevmouse/main.cpp index dd24c85d81..9629d2c927 100644 --- a/src/plugins/generic/evdevmouse/main.cpp +++ b/src/plugins/generic/evdevmouse/main.cpp @@ -27,7 +27,8 @@ QObject* QEvdevMousePlugin::create(const QString &key, { if (!key.compare(QLatin1String("EvdevMouse"), Qt::CaseInsensitive)) return new QEvdevMouseManager(key, specification); - return 0; + + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/evdevtablet/CMakeLists.txt b/src/plugins/generic/evdevtablet/CMakeLists.txt index 4f39c1be87..ef21469f70 100644 --- a/src/plugins/generic/evdevtablet/CMakeLists.txt +++ b/src/plugins/generic/evdevtablet/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from evdevtablet.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEvdevTabletPlugin Plugin: @@ -17,7 +18,3 @@ qt_internal_add_plugin(QEvdevTabletPlugin Qt::GuiPrivate Qt::InputSupportPrivate ) - -#### Keys ignored in scope 1:.:.:evdevtablet.pro:<TRUE>: -# OTHER_FILES = "evdevtablet.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/evdevtablet/main.cpp b/src/plugins/generic/evdevtablet/main.cpp index 4ac58333e9..6313c09921 100644 --- a/src/plugins/generic/evdevtablet/main.cpp +++ b/src/plugins/generic/evdevtablet/main.cpp @@ -27,7 +27,7 @@ QObject* QEvdevTabletPlugin::create(const QString &key, if (!key.compare(QLatin1String("EvdevTablet"), Qt::CaseInsensitive)) return new QEvdevTabletManager(key, spec); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/evdevtouch/CMakeLists.txt b/src/plugins/generic/evdevtouch/CMakeLists.txt index 4b90efbd69..f3f489586a 100644 --- a/src/plugins/generic/evdevtouch/CMakeLists.txt +++ b/src/plugins/generic/evdevtouch/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from evdevtouch.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEvdevTouchScreenPlugin Plugin: @@ -17,7 +18,3 @@ qt_internal_add_plugin(QEvdevTouchScreenPlugin Qt::GuiPrivate Qt::InputSupportPrivate ) - -#### Keys ignored in scope 1:.:.:evdevtouch.pro:<TRUE>: -# OTHER_FILES = "evdevtouch.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/evdevtouch/main.cpp b/src/plugins/generic/evdevtouch/main.cpp index 958a69f032..f39f9645bc 100644 --- a/src/plugins/generic/evdevtouch/main.cpp +++ b/src/plugins/generic/evdevtouch/main.cpp @@ -27,7 +27,7 @@ QObject* QEvdevTouchScreenPlugin::create(const QString &key, if (!key.compare(QLatin1String("EvdevTouch"), Qt::CaseInsensitive)) return new QEvdevTouchManager(key, spec); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/libinput/CMakeLists.txt b/src/plugins/generic/libinput/CMakeLists.txt index bf423c601d..f92b3d5353 100644 --- a/src/plugins/generic/libinput/CMakeLists.txt +++ b/src/plugins/generic/libinput/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from libinput.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QLibInputPlugin Plugin: @@ -17,7 +18,3 @@ qt_internal_add_plugin(QLibInputPlugin Qt::GuiPrivate Qt::InputSupportPrivate ) - -#### Keys ignored in scope 1:.:.:libinput.pro:<TRUE>: -# OTHER_FILES = "libinput.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/libinput/main.cpp b/src/plugins/generic/libinput/main.cpp index 3005868b57..a191fd3d9b 100644 --- a/src/plugins/generic/libinput/main.cpp +++ b/src/plugins/generic/libinput/main.cpp @@ -20,7 +20,7 @@ QObject *QLibInputPlugin::create(const QString &key, const QString &specificatio if (!key.compare(QLatin1String("libinput"), Qt::CaseInsensitive)) return new QLibInputHandler(key, specification); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/tslib/CMakeLists.txt b/src/plugins/generic/tslib/CMakeLists.txt index 42d6f59b13..8512133799 100644 --- a/src/plugins/generic/tslib/CMakeLists.txt +++ b/src/plugins/generic/tslib/CMakeLists.txt @@ -1,6 +1,7 @@ -# Generated from tslib.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -qt_find_package(Tslib) # special case +qt_find_package(Tslib) ##################################################################### ## QTsLibPlugin Plugin: @@ -20,7 +21,3 @@ qt_internal_add_plugin(QTsLibPlugin Qt::GuiPrivate Qt::InputSupportPrivate ) - -#### Keys ignored in scope 1:.:.:tslib.pro:<TRUE>: -# OTHER_FILES = "tslib.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/tslib/main.cpp b/src/plugins/generic/tslib/main.cpp index bb3041eb56..c2f4ccb105 100644 --- a/src/plugins/generic/tslib/main.cpp +++ b/src/plugins/generic/tslib/main.cpp @@ -22,7 +22,7 @@ QObject* QTsLibPlugin::create(const QString &key, || !key.compare(QLatin1String("TslibRaw"), Qt::CaseInsensitive)) return new QTsLibMouseHandler(key, specification); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/tuiotouch/CMakeLists.txt b/src/plugins/generic/tuiotouch/CMakeLists.txt index 8271216182..b03ed9e360 100644 --- a/src/plugins/generic/tuiotouch/CMakeLists.txt +++ b/src/plugins/generic/tuiotouch/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from tuiotouch.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QTuioTouchPlugin Plugin: @@ -24,7 +25,3 @@ qt_internal_add_plugin(QTuioTouchPlugin Qt::GuiPrivate Qt::Network ) - -#### Keys ignored in scope 1:.:.:tuiotouch.pro:<TRUE>: -# OTHER_FILES = "tuiotouch.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/generic/tuiotouch/main.cpp b/src/plugins/generic/tuiotouch/main.cpp index 30f3af3597..037a5d64a0 100644 --- a/src/plugins/generic/tuiotouch/main.cpp +++ b/src/plugins/generic/tuiotouch/main.cpp @@ -30,7 +30,7 @@ QObject* QTuioTouchPlugin::create(const QString &key, if (!key.compare(QLatin1String("TuioTouch"), Qt::CaseInsensitive)) return new QTuioHandler(spec); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/generic/tuiotouch/qtuiohandler.cpp b/src/plugins/generic/tuiotouch/qtuiohandler.cpp index 3e67050589..2815368e84 100644 --- a/src/plugins/generic/tuiotouch/qtuiohandler.cpp +++ b/src/plugins/generic/tuiotouch/qtuiohandler.cpp @@ -39,7 +39,7 @@ QTuioHandler::QTuioHandler(const QString &specification) bool invertx = false; bool inverty = false; - for (int i = 0; i < args.count(); ++i) { + for (int i = 0; i < args.size(); ++i) { if (args.at(i).startsWith("udp=")) { QString portString = args.at(i).section('=', 1, 1); portNumber = portString.toInt(); @@ -59,6 +59,7 @@ QTuioHandler::QTuioHandler(const QString &specification) case 180: case 270: rotationAngle = argValue; + break; default: break; } @@ -136,10 +137,10 @@ void QTuioHandler::processPackets() messages.push_back(msg); } - for (const QOscMessage &message : qAsConst(messages)) { + for (const QOscMessage &message : std::as_const(messages)) { if (message.addressPattern() == "/tuio/2Dcur") { QList<QVariant> arguments = message.arguments(); - if (arguments.count() == 0) { + if (arguments.size() == 0) { qCWarning(lcTuioHandler, "Ignoring TUIO message with no arguments"); continue; } @@ -159,7 +160,7 @@ void QTuioHandler::processPackets() } } else if (message.addressPattern() == "/tuio/2Dobj") { QList<QVariant> arguments = message.arguments(); - if (arguments.count() == 0) { + if (arguments.size() == 0) { qCWarning(lcTuioHandler, "Ignoring TUIO message with no arguments"); continue; } @@ -188,8 +189,8 @@ void QTuioHandler::processPackets() void QTuioHandler::process2DCurSource(const QOscMessage &message) { QList<QVariant> arguments = message.arguments(); - if (arguments.count() != 2) { - qCWarning(lcTuioSource) << "Ignoring malformed TUIO source message: " << arguments.count(); + if (arguments.size() != 2) { + qCWarning(lcTuioSource) << "Ignoring malformed TUIO source message: " << arguments.size(); return; } @@ -214,7 +215,7 @@ void QTuioHandler::process2DCurAlive(const QOscMessage &message) QMap<int, QTuioCursor> oldActiveCursors = m_activeCursors; QMap<int, QTuioCursor> newActiveCursors; - for (int i = 1; i < arguments.count(); ++i) { + for (int i = 1; i < arguments.size(); ++i) { if (QMetaType::Type(arguments.at(i).userType()) != QMetaType::Int) { qCWarning(lcTuioHandler) << "Ignoring malformed TUIO alive message (bad argument on position" << i << arguments << ')'; return; @@ -255,8 +256,8 @@ void QTuioHandler::process2DCurAlive(const QOscMessage &message) void QTuioHandler::process2DCurSet(const QOscMessage &message) { QList<QVariant> arguments = message.arguments(); - if (arguments.count() < 7) { - qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with too few arguments: " << arguments.count(); + if (arguments.size() < 7) { + qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with too few arguments: " << arguments.size(); return; } @@ -326,7 +327,7 @@ void QTuioHandler::process2DCurFseq(const QOscMessage &message) Q_UNUSED(message); // TODO: do we need to do anything with the frame id? QWindow *win = QGuiApplication::focusWindow(); - if (!win && QGuiApplication::topLevelWindows().length() > 0 && forceDelivery) + if (!win && QGuiApplication::topLevelWindows().size() > 0 && forceDelivery) win = QGuiApplication::topLevelWindows().at(0); if (!win) @@ -335,12 +336,12 @@ void QTuioHandler::process2DCurFseq(const QOscMessage &message) QList<QWindowSystemInterface::TouchPoint> tpl; tpl.reserve(m_activeCursors.size() + m_deadCursors.size()); - for (const QTuioCursor &tc : qAsConst(m_activeCursors)) { + for (const QTuioCursor &tc : std::as_const(m_activeCursors)) { QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win); tpl.append(tp); } - for (const QTuioCursor &tc : qAsConst(m_deadCursors)) { + for (const QTuioCursor &tc : std::as_const(m_deadCursors)) { QWindowSystemInterface::TouchPoint tp = cursorToTouchPoint(tc, win); tp.state = QEventPoint::State::Released; tpl.append(tp); @@ -353,8 +354,8 @@ void QTuioHandler::process2DCurFseq(const QOscMessage &message) void QTuioHandler::process2DObjSource(const QOscMessage &message) { QList<QVariant> arguments = message.arguments(); - if (arguments.count() != 2) { - qCWarning(lcTuioSource, ) << "Ignoring malformed TUIO source message: " << arguments.count(); + if (arguments.size() != 2) { + qCWarning(lcTuioSource ) << "Ignoring malformed TUIO source message: " << arguments.size(); return; } @@ -379,7 +380,7 @@ void QTuioHandler::process2DObjAlive(const QOscMessage &message) QMap<int, QTuioToken> oldActiveTokens = m_activeTokens; QMap<int, QTuioToken> newActiveTokens; - for (int i = 1; i < arguments.count(); ++i) { + for (int i = 1; i < arguments.size(); ++i) { if (QMetaType::Type(arguments.at(i).userType()) != QMetaType::Int) { qCWarning(lcTuioHandler) << "Ignoring malformed TUIO alive message (bad argument on position" << i << arguments << ')'; return; @@ -420,8 +421,8 @@ void QTuioHandler::process2DObjAlive(const QOscMessage &message) void QTuioHandler::process2DObjSet(const QOscMessage &message) { QList<QVariant> arguments = message.arguments(); - if (arguments.count() < 7) { - qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with too few arguments: " << arguments.count(); + if (arguments.size() < 7) { + qCWarning(lcTuioSet) << "Ignoring malformed TUIO set message with too few arguments: " << arguments.size(); return; } @@ -499,7 +500,7 @@ void QTuioHandler::process2DObjFseq(const QOscMessage &message) Q_UNUSED(message); // TODO: do we need to do anything with the frame id? QWindow *win = QGuiApplication::focusWindow(); - if (!win && QGuiApplication::topLevelWindows().length() > 0 && forceDelivery) + if (!win && QGuiApplication::topLevelWindows().size() > 0 && forceDelivery) win = QGuiApplication::topLevelWindows().at(0); if (!win) @@ -508,12 +509,12 @@ void QTuioHandler::process2DObjFseq(const QOscMessage &message) QList<QWindowSystemInterface::TouchPoint> tpl; tpl.reserve(m_activeTokens.size() + m_deadTokens.size()); - for (const QTuioToken & t : qAsConst(m_activeTokens)) { + for (const QTuioToken & t : std::as_const(m_activeTokens)) { QWindowSystemInterface::TouchPoint tp = tokenToTouchPoint(t, win); tpl.append(tp); } - for (const QTuioToken & t : qAsConst(m_deadTokens)) { + for (const QTuioToken & t : std::as_const(m_deadTokens)) { QWindowSystemInterface::TouchPoint tp = tokenToTouchPoint(t, win); tp.state = QEventPoint::State::Released; tp.velocity = QVector2D(); diff --git a/src/plugins/imageformats/CMakeLists.txt b/src/plugins/imageformats/CMakeLists.txt index 00fefbdc0d..c5ab716d00 100644 --- a/src/plugins/imageformats/CMakeLists.txt +++ b/src/plugins/imageformats/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from imageformats.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_ico) add_subdirectory(ico) diff --git a/src/plugins/imageformats/gif/CMakeLists.txt b/src/plugins/imageformats/gif/CMakeLists.txt index 1241fa2f0f..b56859a264 100644 --- a/src/plugins/imageformats/gif/CMakeLists.txt +++ b/src/plugins/imageformats/gif/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from gif.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QGifPlugin Plugin: @@ -10,10 +11,7 @@ qt_internal_add_plugin(QGifPlugin SOURCES main.cpp qgifhandler.cpp qgifhandler_p.h - LIBRARIES # special case - Qt::Gui # special case - Qt::GuiPrivate # special case + LIBRARIES + Qt::Gui + Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:gif.pro:<TRUE>: -# OTHER_FILES = "gif.json" diff --git a/src/plugins/imageformats/gif/qgifhandler.cpp b/src/plugins/imageformats/gif/qgifhandler.cpp index add6b64855..8ad4ff7510 100644 --- a/src/plugins/imageformats/gif/qgifhandler.cpp +++ b/src/plugins/imageformats/gif/qgifhandler.cpp @@ -1146,9 +1146,9 @@ QVariant QGifHandler::option(ImageOption option) const } // before the first frame is read, or we have an empty data stream if (frameNumber == -1) - return (imageSizes.count() > 0) ? QVariant(imageSizes.at(0)) : QVariant(); + return (imageSizes.size() > 0) ? QVariant(imageSizes.at(0)) : QVariant(); // after the last frame has been read, the next size is undefined - if (frameNumber >= imageSizes.count() - 1) + if (frameNumber >= imageSizes.size() - 1) return QVariant(); // and the last case: the size of the next frame return imageSizes.at(frameNumber + 1); @@ -1175,7 +1175,7 @@ int QGifHandler::imageCount() const QGIFFormat::scan(device(), &imageSizes, &loopCnt); scanIsCached = true; } - return imageSizes.count(); + return imageSizes.size(); } int QGifHandler::loopCount() const diff --git a/src/plugins/imageformats/ico/CMakeLists.txt b/src/plugins/imageformats/ico/CMakeLists.txt index 8cdedb2cfe..c9cd0f0d40 100644 --- a/src/plugins/imageformats/ico/CMakeLists.txt +++ b/src/plugins/imageformats/ico/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from ico.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QICOPlugin Plugin: @@ -14,7 +15,5 @@ qt_internal_add_plugin(QICOPlugin Qt::Core Qt::CorePrivate Qt::Gui + NO_UNITY_BUILD ) - -#### Keys ignored in scope 1:.:.:ico.pro:<TRUE>: -# OTHER_FILES = "ico.json" diff --git a/src/plugins/imageformats/ico/qicohandler.cpp b/src/plugins/imageformats/ico/qicohandler.cpp index 496e927919..18b39766f5 100644 --- a/src/plugins/imageformats/ico/qicohandler.cpp +++ b/src/plugins/imageformats/ico/qicohandler.cpp @@ -499,7 +499,7 @@ QImage ICOReader::iconAt(int index) if (!image.isNull()) { readBMP(image); if (!image.isNull()) { - if (icoAttrib.depth == 32) { + if (icoAttrib.nbits == 32) { img = std::move(image).convertToFormat(QImage::Format_ARGB32_Premultiplied); } else { QImage mask(image.width(), image.height(), QImage::Format_Mono); @@ -565,14 +565,14 @@ bool ICOReader::write(QIODevice *device, const QList<QImage> &images) { bool retValue = false; - if (images.count()) { + if (images.size()) { qint64 origOffset = device->pos(); ICONDIR id; id.idReserved = 0; id.idType = 1; - id.idCount = images.count(); + id.idCount = images.size(); ICONDIRENTRY * entries = new ICONDIRENTRY[id.idCount]; BMP_INFOHDR * bmpHeaders = new BMP_INFOHDR[id.idCount]; @@ -735,6 +735,8 @@ bool QtIcoHandler::supportsOption(ImageOption option) const */ bool QtIcoHandler::canRead() const { + if (knownCanRead) + return true; bool bCanRead = false; QIODevice *device = QImageIOHandler::device(); if (device) { @@ -744,6 +746,7 @@ bool QtIcoHandler::canRead() const } else { qCWarning(lcIco, "QtIcoHandler::canRead() called with no device"); } + knownCanRead = bCanRead; return bCanRead; } diff --git a/src/plugins/imageformats/ico/qicohandler.h b/src/plugins/imageformats/ico/qicohandler.h index 0fe251ab60..61c3eea465 100644 --- a/src/plugins/imageformats/ico/qicohandler.h +++ b/src/plugins/imageformats/ico/qicohandler.h @@ -30,7 +30,7 @@ public: private: int m_currentIconIndex; ICOReader *m_pICOReader; - + mutable bool knownCanRead = false; }; QT_END_NAMESPACE diff --git a/src/plugins/imageformats/jpeg/CMakeLists.txt b/src/plugins/imageformats/jpeg/CMakeLists.txt index bb5a78fa40..6b077a7647 100644 --- a/src/plugins/imageformats/jpeg/CMakeLists.txt +++ b/src/plugins/imageformats/jpeg/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from jpeg.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause qt_find_package(WrapJpeg PROVIDED_TARGETS WrapJpeg::WrapJpeg) diff --git a/src/plugins/imageformats/jpeg/qjpeghandler.cpp b/src/plugins/imageformats/jpeg/qjpeghandler.cpp index eebc5940ce..59b73587c5 100644 --- a/src/plugins/imageformats/jpeg/qjpeghandler.cpp +++ b/src/plugins/imageformats/jpeg/qjpeghandler.cpp @@ -49,6 +49,7 @@ extern "C" { static void my_error_exit (j_common_ptr cinfo) { + (*cinfo->err->output_message)(cinfo); my_error_mgr* myerr = (my_error_mgr*) cinfo->err; longjmp(myerr->setjmp_buffer, 1); } @@ -171,9 +172,14 @@ inline static bool read_jpeg_format(QImage::Format &format, j_decompress_ptr cin format = QImage::Format_Grayscale8; break; case 3: - case 4: format = QImage::Format_RGB32; break; + case 4: + if (cinfo->out_color_space == JCS_CMYK) + format = QImage::Format_CMYK8888; + else + format = QImage::Format_RGB32; + break; default: result = false; break; @@ -191,9 +197,14 @@ static bool ensureValidImage(QImage *dest, struct jpeg_decompress_struct *info, format = QImage::Format_Grayscale8; break; case 3: - case 4: format = QImage::Format_RGB32; break; + case 4: + if (info->out_color_space == JCS_CMYK) + format = QImage::Format_CMYK8888; + else + format = QImage::Format_RGB32; + break; default: return false; // unsupported format } @@ -205,7 +216,7 @@ static bool read_jpeg_image(QImage *outImage, QSize scaledSize, QRect scaledClipRect, QRect clipRect, int quality, Rgb888ToRgb32Converter converter, - j_decompress_ptr info, struct my_error_mgr* err ) + j_decompress_ptr info, struct my_error_mgr* err, bool invertCMYK) { if (!setjmp(err->setjmp_buffer)) { // -1 means default quality. @@ -347,14 +358,15 @@ static bool read_jpeg_image(QImage *outImage, QRgb *out = (QRgb*)outImage->scanLine(y); converter(out, in, clip.width()); } else if (info->out_color_space == JCS_CMYK) { - // Convert CMYK->RGB. uchar *in = rows[0] + clip.x() * 4; - QRgb *out = (QRgb*)outImage->scanLine(y); - for (int i = 0; i < clip.width(); ++i) { - int k = in[3]; - *out++ = qRgb(k * in[0] / 255, k * in[1] / 255, - k * in[2] / 255); - in += 4; + quint32 *out = (quint32*)outImage->scanLine(y); + if (invertCMYK) { + for (int i = 0; i < clip.width(); ++i) { + *out++ = 0xffffffffu - (in[0] | in[1] << 8 | in[2] << 16 | in[3] << 24); + in += 4; + } + } else { + memcpy(out, in, clip.width() * 4); } } else if (info->output_components == 1) { // Grayscale. @@ -458,7 +470,7 @@ static inline void set_text(const QImage &image, j_compress_ptr cinfo, const QSt if (!comment.isEmpty()) comment += ": "; comment += it.value().toUtf8(); - if (comment.length() > maxMarkerSize) + if (comment.size() > maxMarkerSize) comment.truncate(maxMarkerSize); jpeg_write_marker(cinfo, JPEG_COM, (const JOCTET *)comment.constData(), comment.size()); } @@ -492,7 +504,8 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, int sourceQuality, const QString &description, bool optimize, - bool progressive) + bool progressive, + bool invertCMYK) { bool success = false; const QList<QRgb> cmap = image.colorTable(); @@ -532,10 +545,15 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, cinfo.in_color_space = gray ? JCS_GRAYSCALE : JCS_RGB; break; case QImage::Format_Grayscale8: + case QImage::Format_Grayscale16: gray = true; cinfo.input_components = 1; cinfo.in_color_space = JCS_GRAYSCALE; break; + case QImage::Format_CMYK8888: + cinfo.input_components = 4; + cinfo.in_color_space = JCS_CMYK; + break; default: cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; @@ -568,7 +586,7 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, jpeg_start_compress(&cinfo, TRUE); set_text(image, &cinfo, description); - if (cinfo.in_color_space == JCS_RGB) + if (cinfo.in_color_space == JCS_RGB || cinfo.in_color_space == JCS_CMYK) write_icc_profile(image, &cinfo); row_pointer[0] = new uchar[cinfo.image_width*cinfo.input_components]; @@ -630,6 +648,12 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, case QImage::Format_Grayscale8: memcpy(row, image.constScanLine(cinfo.next_scanline), w); break; + case QImage::Format_Grayscale16: + { + QImage rowImg = image.copy(0, cinfo.next_scanline, w, 1).convertToFormat(QImage::Format_Grayscale8); + memcpy(row, rowImg.constScanLine(0), w); + } + break; case QImage::Format_RGB888: memcpy(row, image.constScanLine(cinfo.next_scanline), w * 3); break; @@ -646,6 +670,17 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, } } break; + case QImage::Format_CMYK8888: { + auto *cmykIn = reinterpret_cast<const quint32 *>(image.constScanLine(cinfo.next_scanline)); + auto *cmykOut = reinterpret_cast<quint32 *>(row); + if (invertCMYK) { + for (int i = 0; i < w; ++i) + cmykOut[i] = 0xffffffffu - cmykIn[i]; + } else { + memcpy(cmykOut, cmykIn, w * 4); + } + break; + } default: { // (Testing shows that this way is actually faster than converting to RGB888 + memcpy) @@ -681,7 +716,8 @@ static bool write_jpeg_image(const QImage &image, int sourceQuality, const QString &description, bool optimize, - bool progressive) + bool progressive, + bool invertCMYK) { // protect these objects from the setjmp/longjmp pair inside // do_write_jpeg_image (by making them non-local). @@ -692,7 +728,7 @@ static bool write_jpeg_image(const QImage &image, const bool success = do_write_jpeg_image(cinfo, row_pointer, image, device, sourceQuality, description, - optimize, progressive); + optimize, progressive, invertCMYK); delete [] row_pointer[0]; return success; @@ -737,6 +773,21 @@ public: QStringList readTexts; QByteArray iccProfile; + // Photoshop historically invertes the quantities in CMYK JPEG files: + // 0 means 100% ink, 255 means no ink. Every reader does the same, + // for compatibility reasons. + // Use such an interpretation by default, but also offer the alternative + // of not inverting the channels. + // This is just a "fancy" API; it could be reduced to a boolean setting + // for CMYK files. + enum class SubType { + Automatic, + Inverted_CMYK, + CMYK, + NSubTypes + }; + SubType subType = SubType::Automatic; + struct jpeg_decompress_struct info; struct my_jpeg_source_mgr * iod_src; struct my_error_mgr err; @@ -751,6 +802,14 @@ public: QJpegHandler *q; }; +static const char SupportedJPEGSubtypes[][14] = { + "Automatic", + "Inverted_CMYK", + "CMYK" +}; + +static_assert(std::size(SupportedJPEGSubtypes) == size_t(QJpegHandlerPrivate::SubType::NSubTypes)); + static bool readExifHeader(QDataStream &stream) { char prefix[6]; @@ -967,7 +1026,8 @@ bool QJpegHandlerPrivate::read(QImage *image) if (state == ReadHeader) { - bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, rgb888ToRgb32ConverterPtr, &info, &err); + const bool invertCMYK = subType != QJpegHandlerPrivate::SubType::CMYK; + bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, rgb888ToRgb32ConverterPtr, &info, &err, invertCMYK); if (success) { for (int i = 0; i < readTexts.size()-1; i+=2) image->setText(readTexts.at(i), readTexts.at(i+1)); @@ -1053,13 +1113,14 @@ extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orie bool QJpegHandler::write(const QImage &image) { + const bool invertCMYK = d->subType != QJpegHandlerPrivate::SubType::CMYK; if (d->transformation != QImageIOHandler::TransformationNone) { // We don't support writing EXIF headers so apply the transform to the data. QImage img = image; qt_imageTransform(img, d->transformation); - return write_jpeg_image(img, device(), d->quality, d->description, d->optimize, d->progressive); + return write_jpeg_image(img, device(), d->quality, d->description, d->optimize, d->progressive, invertCMYK); } - return write_jpeg_image(image, device(), d->quality, d->description, d->optimize, d->progressive); + return write_jpeg_image(image, device(), d->quality, d->description, d->optimize, d->progressive, invertCMYK); } bool QJpegHandler::supportsOption(ImageOption option) const @@ -1070,6 +1131,8 @@ bool QJpegHandler::supportsOption(ImageOption option) const || option == ClipRect || option == Description || option == Size + || option == SubType + || option == SupportedSubTypes || option == ImageFormat || option == OptimizedWrite || option == ProgressiveScanWrite @@ -1093,6 +1156,13 @@ QVariant QJpegHandler::option(ImageOption option) const case Size: d->readJpegHeader(device()); return d->size; + case SubType: + return QByteArray(SupportedJPEGSubtypes[int(d->subType)]); + case SupportedSubTypes: { + QByteArrayList list(std::begin(SupportedJPEGSubtypes), + std::end(SupportedJPEGSubtypes)); + return QVariant::fromValue(list); + } case ImageFormat: d->readJpegHeader(device()); return d->format; @@ -1128,6 +1198,16 @@ void QJpegHandler::setOption(ImageOption option, const QVariant &value) case Description: d->description = value.toString(); break; + case SubType: { + const QByteArray subType = value.toByteArray(); + for (size_t i = 0; i < std::size(SupportedJPEGSubtypes); ++i) { + if (subType == SupportedJPEGSubtypes[i]) { + d->subType = QJpegHandlerPrivate::SubType(i); + break; + } + } + break; + } case OptimizedWrite: d->optimize = value.toBool(); break; @@ -1138,6 +1218,7 @@ void QJpegHandler::setOption(ImageOption option, const QVariant &value) int transformation = value.toInt(); if (transformation > 0 && transformation < 8) d->transformation = QImageIOHandler::Transformations(transformation); + break; } default: break; diff --git a/src/plugins/networkinformation/CMakeLists.txt b/src/plugins/networkinformation/CMakeLists.txt index 06bbe89121..04da816264 100644 --- a/src/plugins/networkinformation/CMakeLists.txt +++ b/src/plugins/networkinformation/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + if(WIN32 AND QT_FEATURE_networklistmanager) add_subdirectory(networklistmanager) endif() diff --git a/src/plugins/networkinformation/android/CMakeLists.txt b/src/plugins/networkinformation/android/CMakeLists.txt index 0883ec74e2..07d9201bbb 100644 --- a/src/plugins/networkinformation/android/CMakeLists.txt +++ b/src/plugins/networkinformation/android/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + set(java_sources jar/src/org/qtproject/qt/android/networkinformation/QtAndroidNetworkInformation.java @@ -9,8 +12,10 @@ qt_internal_add_jar(Qt${QtBase_VERSION_MAJOR}AndroidNetworkInformationBackend OUTPUT_DIR "${QT_BUILD_DIR}/jar" ) +qt_path_join(destination ${INSTALL_DATADIR} "jar") + install_jar(Qt${QtBase_VERSION_MAJOR}AndroidNetworkInformationBackend - DESTINATION jar + DESTINATION ${destination} COMPONENT Devel ) @@ -24,8 +29,6 @@ qt_internal_add_plugin(QAndroidNetworkInformationPlugin wrapper/androidconnectivitymanager.cpp wrapper/androidconnectivitymanager.h LIBRARIES Qt::NetworkPrivate - DEFINES - QT_USE_QSTRINGBUILDER ) set_property( diff --git a/src/plugins/networkinformation/android/jar/build.gradle b/src/plugins/networkinformation/android/jar/build.gradle index c947852f79..86a42bd9c1 100644 --- a/src/plugins/networkinformation/android/jar/build.gradle +++ b/src/plugins/networkinformation/android/jar/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:8.0.2' } } @@ -23,12 +23,10 @@ repositories { } android { - compileSdkVersion 31 - buildToolsVersion "31.0.3" + compileSdk 34 defaultConfig { - minSdkVersion 23 - targetSdkVersion 31 + minSdkVersion 28 } sourceSets { diff --git a/src/plugins/networkinformation/android/jar/src/org/qtproject/qt/android/networkinformation/QtAndroidNetworkInformation.java b/src/plugins/networkinformation/android/jar/src/org/qtproject/qt/android/networkinformation/QtAndroidNetworkInformation.java index 6a259e5ea6..6a56c506b0 100644 --- a/src/plugins/networkinformation/android/jar/src/org/qtproject/qt/android/networkinformation/QtAndroidNetworkInformation.java +++ b/src/plugins/networkinformation/android/jar/src/org/qtproject/qt/android/networkinformation/QtAndroidNetworkInformation.java @@ -15,9 +15,9 @@ import android.os.Build; public class QtAndroidNetworkInformation { private static final String LOG_TAG = "QtAndroidNetworkInformation"; - private static native void connectivityChanged(AndroidConnectivity connectivity); + private static native void networkConnectivityChanged(int connectivity); private static native void genericInfoChanged(boolean captivePortal, boolean metered); - private static native void transportMediumChanged(Transport transportMedium); + private static native void transportMediumChanged(int transportMedium); private static QtNetworkInformationCallback m_callback = null; private static final Object m_lock = new Object(); @@ -96,14 +96,14 @@ public class QtAndroidNetworkInformation { private void setState(AndroidConnectivity s) { if (previousState != s) { previousState = s; - connectivityChanged(s); + networkConnectivityChanged(s.ordinal()); } } private void setTransportMedium(Transport t) { if (previousTransport != t) { previousTransport = t; - transportMediumChanged(t); + transportMediumChanged(t.ordinal()); } } diff --git a/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp b/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp index b3035db2fa..3c9f952968 100644 --- a/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp +++ b/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp @@ -20,14 +20,15 @@ Q_GLOBAL_STATIC(AndroidConnectivityManagerInstance, androidConnManagerInstance) static const char networkInformationClass[] = "org/qtproject/qt/android/networkinformation/QtAndroidNetworkInformation"; -static void networkConnectivityChanged(JNIEnv *env, jobject obj, jobject enumValue) +static void networkConnectivityChanged(JNIEnv *env, jobject obj, jint enumValue) { Q_UNUSED(env); Q_UNUSED(obj); - const jint value = QJniObject(enumValue).callMethod<jint>("ordinal"); - const auto connectivity = static_cast<AndroidConnectivityManager::AndroidConnectivity>(value); + const auto connectivity = + static_cast<AndroidConnectivityManager::AndroidConnectivity>(enumValue); Q_EMIT androidConnManagerInstance->connManager->connectivityChanged(connectivity); } +Q_DECLARE_JNI_NATIVE_METHOD(networkConnectivityChanged) static void genericInfoChanged(JNIEnv *env, jobject obj, jboolean captivePortal, jboolean metered) { @@ -36,30 +37,26 @@ static void genericInfoChanged(JNIEnv *env, jobject obj, jboolean captivePortal, Q_EMIT androidConnManagerInstance->connManager->captivePortalChanged(captivePortal); Q_EMIT androidConnManagerInstance->connManager->meteredChanged(metered); } +Q_DECLARE_JNI_NATIVE_METHOD(genericInfoChanged) -static void transportMediumChangedCallback(JNIEnv *env, jobject obj, jobject enumValue) +static void transportMediumChanged(JNIEnv *env, jobject obj, jint enumValue) { Q_UNUSED(env); Q_UNUSED(obj); - const jint value = QJniObject(enumValue).callMethod<jint>("ordinal"); - const auto transport = static_cast<AndroidConnectivityManager::AndroidTransport>(value); + const auto transport = static_cast<AndroidConnectivityManager::AndroidTransport>(enumValue); emit androidConnManagerInstance->connManager->transportMediumChanged(transport); } +Q_DECLARE_JNI_NATIVE_METHOD(transportMediumChanged) + +Q_DECLARE_JNI_CLASS(ConnectivityManager, "android/net/ConnectivityManager") AndroidConnectivityManager::AndroidConnectivityManager() { if (!registerNatives()) return; - m_connectivityManager = QJniObject::callStaticObjectMethod( - networkInformationClass, "getConnectivityManager", - "(Landroid/content/Context;)Landroid/net/ConnectivityManager;", - QAndroidApplication::context()); - if (!m_connectivityManager.isValid()) - return; - QJniObject::callStaticMethod<void>(networkInformationClass, "registerReceiver", - "(Landroid/content/Context;)V", QAndroidApplication::context()); + QAndroidApplication::context()); } AndroidConnectivityManager *AndroidConnectivityManager::getInstance() @@ -71,36 +68,30 @@ AndroidConnectivityManager *AndroidConnectivityManager::getInstance() : nullptr; } +bool AndroidConnectivityManager::isValid() const +{ + return registerNatives(); +} + AndroidConnectivityManager::~AndroidConnectivityManager() { QJniObject::callStaticMethod<void>(networkInformationClass, "unregisterReceiver", - "(Landroid/content/Context;)V", QAndroidApplication::context()); + QAndroidApplication::context()); } -bool AndroidConnectivityManager::registerNatives() +bool AndroidConnectivityManager::registerNatives() const { - QJniEnvironment env; - QJniObject networkReceiver(networkInformationClass); - if (!networkReceiver.isValid()) - return false; - - const QByteArray connectivityEnumSig = - QByteArray("(L") + networkInformationClass + "$AndroidConnectivity;)V"; - const QByteArray transportEnumSig = - QByteArray("(L") + networkInformationClass + "$Transport;)V"; - - jclass clazz = env->GetObjectClass(networkReceiver.object()); - static JNINativeMethod methods[] = { - { "connectivityChanged", connectivityEnumSig.data(), - reinterpret_cast<void *>(networkConnectivityChanged) }, - { "genericInfoChanged", "(ZZ)V", - reinterpret_cast<void *>(genericInfoChanged) }, - { "transportMediumChanged", transportEnumSig.data(), - reinterpret_cast<void *>(transportMediumChangedCallback) }, - }; - const bool ret = (env->RegisterNatives(clazz, methods, std::size(methods)) == JNI_OK); - env->DeleteLocalRef(clazz); - return ret; + static const bool registered = []() { + QJniEnvironment env; + return env.registerNativeMethods(networkInformationClass, { + Q_JNI_NATIVE_METHOD(networkConnectivityChanged), + Q_JNI_NATIVE_METHOD(genericInfoChanged), + Q_JNI_NATIVE_METHOD(transportMediumChanged), + }); + }(); + return registered; } QT_END_NAMESPACE + +#include "moc_androidconnectivitymanager.cpp" diff --git a/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.h b/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.h index 9b51bbfc19..d15faf0e8e 100644 --- a/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.h +++ b/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.h @@ -33,7 +33,7 @@ public: static AndroidConnectivityManager *getInstance(); ~AndroidConnectivityManager(); - inline bool isValid() const { return m_connectivityManager.isValid(); } + inline bool isValid() const; Q_SIGNALS: void connectivityChanged(AndroidConnectivity connectivity); @@ -44,8 +44,7 @@ Q_SIGNALS: private: friend struct AndroidConnectivityManagerInstance; AndroidConnectivityManager(); - bool registerNatives(); - QJniObject m_connectivityManager; + bool registerNatives() const; Q_DISABLE_COPY_MOVE(AndroidConnectivityManager); }; diff --git a/src/plugins/networkinformation/glib/CMakeLists.txt b/src/plugins/networkinformation/glib/CMakeLists.txt index 17a16a15c0..019f4f1358 100644 --- a/src/plugins/networkinformation/glib/CMakeLists.txt +++ b/src/plugins/networkinformation/glib/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QGlibNetworkInformationPlugin OUTPUT_NAME qglib CLASS_NAME QGlibNetworkInformationBackendFactory diff --git a/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp b/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp index 10ce5dbc60..0b45eb9ce3 100644 --- a/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp +++ b/src/plugins/networkinformation/glib/qglibnetworkinformationbackend.cpp @@ -51,7 +51,8 @@ public: static QNetworkInformation::Features featuresSupportedStatic() { using Feature = QNetworkInformation::Feature; - return QNetworkInformation::Features(Feature::Reachability | Feature::CaptivePortal); + return QNetworkInformation::Features(Feature::Reachability | Feature::CaptivePortal + | Feature::Metered); } bool isValid() const; @@ -59,10 +60,12 @@ public: private: Q_DISABLE_COPY_MOVE(QGlibNetworkInformationBackend) - static void updateInformation(QGlibNetworkInformationBackend *backend); + static void updateConnectivity(QGlibNetworkInformationBackend *backend); + static void updateMetered(QGlibNetworkInformationBackend *backend); GNetworkMonitor *networkMonitor = nullptr; - gulong handlerId = 0; + gulong connectivityHandlerId = 0; + gulong meteredHandlerId = 0; }; class QGlibNetworkInformationBackendFactory : public QNetworkInformationBackendFactory @@ -95,23 +98,28 @@ private: QGlibNetworkInformationBackend::QGlibNetworkInformationBackend() : networkMonitor(g_network_monitor_get_default()) { - updateInformation(this); + updateConnectivity(this); + updateMetered(this); - handlerId = g_signal_connect_swapped(networkMonitor, "notify::connectivity", - G_CALLBACK(updateInformation), this); + connectivityHandlerId = g_signal_connect_swapped(networkMonitor, "notify::connectivity", + G_CALLBACK(updateConnectivity), this); + + meteredHandlerId = g_signal_connect_swapped(networkMonitor, "notify::network-metered", + G_CALLBACK(updateMetered), this); } QGlibNetworkInformationBackend::~QGlibNetworkInformationBackend() { - g_signal_handler_disconnect(networkMonitor, handlerId); + g_signal_handler_disconnect(networkMonitor, meteredHandlerId); + g_signal_handler_disconnect(networkMonitor, connectivityHandlerId); } bool QGlibNetworkInformationBackend::isValid() const { - return G_OBJECT_TYPE_NAME(networkMonitor) != "GNetworkMonitorBase"_L1; + return QLatin1StringView(G_OBJECT_TYPE_NAME(networkMonitor)) != "GNetworkMonitorBase"_L1; } -void QGlibNetworkInformationBackend::updateInformation(QGlibNetworkInformationBackend *backend) +void QGlibNetworkInformationBackend::updateConnectivity(QGlibNetworkInformationBackend *backend) { const auto connectivityState = g_network_monitor_get_connectivity(backend->networkMonitor); const bool behindPortal = (connectivityState == G_NETWORK_CONNECTIVITY_PORTAL); @@ -119,6 +127,11 @@ void QGlibNetworkInformationBackend::updateInformation(QGlibNetworkInformationBa backend->setBehindCaptivePortal(behindPortal); } +void QGlibNetworkInformationBackend::updateMetered(QGlibNetworkInformationBackend *backend) +{ + backend->setMetered(g_network_monitor_get_network_metered(backend->networkMonitor)); +} + QT_END_NAMESPACE #include "qglibnetworkinformationbackend.moc" diff --git a/src/plugins/networkinformation/networklistmanager/CMakeLists.txt b/src/plugins/networkinformation/networklistmanager/CMakeLists.txt index d927d4af60..acd3754f4e 100644 --- a/src/plugins/networkinformation/networklistmanager/CMakeLists.txt +++ b/src/plugins/networkinformation/networklistmanager/CMakeLists.txt @@ -1,8 +1,12 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QNLMNIPlugin OUTPUT_NAME qnetworklistmanager CLASS_NAME QNetworkListManagerNetworkInformationBackendFactory PLUGIN_TYPE networkinformation DEFAULT_IF WIN32 AND QT_FEATURE_networklistmanager + EXCEPTIONS SOURCES qnetworklistmanagernetworkinformationbackend.cpp qnetworklistmanagerevents.h qnetworklistmanagerevents.cpp @@ -10,9 +14,10 @@ qt_internal_add_plugin(QNLMNIPlugin Qt::NetworkPrivate ) -qt_internal_extend_target(QNLMNIPlugin CONDITION WIN32 AND MSVC AND NOT CLANG +qt_internal_extend_target(QNLMNIPlugin CONDITION WIN32 LIBRARIES runtimeobject + oleaut32 ) # Don't repeat the target name in AUTOGEN_BUILD_DIR to work around issues with overlong paths. diff --git a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.cpp b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.cpp index 72023c6628..caa5046751 100644 --- a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.cpp +++ b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.cpp @@ -2,19 +2,17 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qnetworklistmanagerevents.h" +#include <QtCore/private/qsystemerror_p.h> -#ifdef SUPPORTS_WINRT -#include <winrt/base.h> -// Workaround for Windows SDK bug. -// See https://github.com/microsoft/Windows.UI.Composition-Win32-Samples/issues/47 -namespace winrt::impl -{ - template <typename Async> - auto wait_for(Async const& async, Windows::Foundation::TimeSpan const& timeout); -} +#include <QtCore/qpointer.h> + +#include <mutex> + +#if QT_CONFIG(cpp_winrt) +#include <QtCore/private/qt_winrtbase_p.h> #include <winrt/Windows.Networking.Connectivity.h> -#endif +#endif // QT_CONFIG(cpp_winrt) QT_BEGIN_NAMESPACE @@ -37,7 +35,7 @@ QNetworkListManagerEvents::QNetworkListManagerEvents() : QObject(nullptr) IID_INetworkListManager, &networkListManager); if (FAILED(hr)) { qCWarning(lcNetInfoNLM) << "Could not get a NetworkListManager instance:" - << errorStringFromHResult(hr); + << QSystemError::windowsComString(hr); return; } @@ -49,7 +47,7 @@ QNetworkListManagerEvents::QNetworkListManagerEvents() : QObject(nullptr) } if (FAILED(hr)) { qCWarning(lcNetInfoNLM) << "Failed to get connection point for network list manager events:" - << errorStringFromHResult(hr); + << QSystemError::windowsComString(hr); } } @@ -87,26 +85,39 @@ bool QNetworkListManagerEvents::start() auto hr = connectionPoint->Advise(this, &cookie); if (FAILED(hr)) { qCWarning(lcNetInfoNLM) << "Failed to subscribe to network connectivity events:" - << errorStringFromHResult(hr); + << QSystemError::windowsComString(hr); return false; } // Update connectivity since it might have changed since this class was constructed NLM_CONNECTIVITY connectivity; hr = networkListManager->GetConnectivity(&connectivity); - if (FAILED(hr)) - qCWarning(lcNetInfoNLM) << "Could not get connectivity:" << errorStringFromHResult(hr); - else + if (FAILED(hr)) { + qCWarning(lcNetInfoNLM) << "Could not get connectivity:" + << QSystemError::windowsComString(hr); + } else { emit connectivityChanged(connectivity); + } -#ifdef SUPPORTS_WINRT +#if QT_CONFIG(cpp_winrt) using namespace winrt::Windows::Networking::Connectivity; - // Register for changes in the network and store a token to unregister later: - token = NetworkInformation::NetworkStatusChanged( - [this](const winrt::Windows::Foundation::IInspectable sender) { - Q_UNUSED(sender); - emitWinRTUpdates(); - }); + using winrt::Windows::Foundation::IInspectable; + try { + // Register for changes in the network and store a token to unregister later: + token = NetworkInformation::NetworkStatusChanged( + [owner = QPointer(this)](const IInspectable sender) { + Q_UNUSED(sender); + if (owner) { + std::scoped_lock locker(owner->winrtLock); + if (owner->token) + owner->emitWinRTUpdates(); + } + }); + } catch (const winrt::hresult_error &ex) { + qCWarning(lcNetInfoNLM) << "Failed to register network status changed callback:" + << QSystemError::windowsComString(ex.code()); + } + // Emit initial state emitWinRTUpdates(); #endif @@ -114,24 +125,28 @@ bool QNetworkListManagerEvents::start() return true; } -bool QNetworkListManagerEvents::stop() +void QNetworkListManagerEvents::stop() { Q_ASSERT(connectionPoint); auto hr = connectionPoint->Unadvise(cookie); if (FAILED(hr)) { qCWarning(lcNetInfoNLM) << "Failed to unsubscribe from network connectivity events:" - << errorStringFromHResult(hr); - return false; + << QSystemError::windowsComString(hr); + } else { + cookie = 0; } - cookie = 0; + // Even if we fail we should still try to unregister from winrt events: -#ifdef SUPPORTS_WINRT - using namespace winrt::Windows::Networking::Connectivity; - // Pass the token we stored earlier to unregister: - NetworkInformation::NetworkStatusChanged(token); - token = {}; +#if QT_CONFIG(cpp_winrt) + // Try to synchronize unregistering with potentially in-progress callbacks + std::scoped_lock locker(winrtLock); + if (token) { + using namespace winrt::Windows::Networking::Connectivity; + // Pass the token we stored earlier to unregister: + NetworkInformation::NetworkStatusChanged(token); + token = {}; + } #endif - return true; } bool QNetworkListManagerEvents::checkBehindCaptivePortal() @@ -155,7 +170,7 @@ bool QNetworkListManagerEvents::checkBehindCaptivePortal() VariantInit(&variant); const auto scopedVariantClear = qScopeGuard([&variant]() { VariantClear(&variant); }); - const wchar_t *versions[] = { NA_InternetConnectivityV6, NA_InternetConnectivityV4 }; + const wchar_t *versions[] = { L"NA_InternetConnectivityV6", L"NA_InternetConnectivityV4" }; for (const auto version : versions) { hr = propertyBag->Read(version, &variant, nullptr); if (SUCCEEDED(hr) @@ -172,7 +187,7 @@ bool QNetworkListManagerEvents::checkBehindCaptivePortal() return false; } -#ifdef SUPPORTS_WINRT +#if QT_CONFIG(cpp_winrt) namespace { using namespace winrt::Windows::Networking::Connectivity; // NB: this isn't part of "network list manager", but sadly NLM doesn't have an @@ -185,7 +200,14 @@ QNetworkInformation::TransportMedium getTransportMedium(const ConnectionProfile if (profile.IsWlanConnectionProfile()) return QNetworkInformation::TransportMedium::WiFi; - NetworkAdapter adapter = profile.NetworkAdapter(); + NetworkAdapter adapter(nullptr); + try { + adapter = profile.NetworkAdapter(); + } catch (const winrt::hresult_error &ex) { + qCWarning(lcNetInfoNLM) << "Failed to obtain network adapter:" + << QSystemError::windowsComString(ex.code()); + // pass, we will return Unknown anyway + } if (adapter == nullptr) return QNetworkInformation::TransportMedium::Unknown; @@ -208,7 +230,16 @@ QNetworkInformation::TransportMedium getTransportMedium(const ConnectionProfile [[nodiscard]] bool getMetered(const ConnectionProfile &profile) { - ConnectionCost cost = profile.GetConnectionCost(); + ConnectionCost cost(nullptr); + try { + cost = profile.GetConnectionCost(); + } catch (const winrt::hresult_error &ex) { + qCWarning(lcNetInfoNLM) << "Failed to obtain connection cost:" + << QSystemError::windowsComString(ex.code()); + // pass, we return false if we get an empty object back anyway + } + if (cost == nullptr) + return false; NetworkCostType type = cost.NetworkCostType(); return type == NetworkCostType::Fixed || type == NetworkCostType::Variable; } @@ -217,12 +248,21 @@ QNetworkInformation::TransportMedium getTransportMedium(const ConnectionProfile void QNetworkListManagerEvents::emitWinRTUpdates() { using namespace winrt::Windows::Networking::Connectivity; - ConnectionProfile profile = NetworkInformation::GetInternetConnectionProfile(); + ConnectionProfile profile = nullptr; + try { + profile = NetworkInformation::GetInternetConnectionProfile(); + } catch (const winrt::hresult_error &ex) { + qCWarning(lcNetInfoNLM) << "Failed to obtain connection profile:" + << QSystemError::windowsComString(ex.code()); + // pass, we would just return early if we get an empty object back anyway + } if (profile == nullptr) return; emit transportMediumChanged(getTransportMedium(profile)); emit isMeteredChanged(getMetered(profile)); } -#endif +#endif // QT_CONFIG(cpp_winrt) QT_END_NAMESPACE + +#include "moc_qnetworklistmanagerevents.cpp" diff --git a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.h b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.h index cf9a08ca84..d91cd8a4cc 100644 --- a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.h +++ b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.h @@ -1,6 +1,9 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QNETWORKLISTMANAGEREVENTS_H +#define QNETWORKLISTMANAGEREVENTS_H + #include <QtNetwork/private/qtnetworkglobal_p.h> #include <QtNetwork/qnetworkinformation.h> @@ -8,19 +11,16 @@ #include <QtCore/qstring.h> #include <QtCore/qobject.h> #include <QtCore/qloggingcategory.h> +#include <QtCore/qmutex.h> #include <objbase.h> +#include <ocidl.h> #include <netlistmgr.h> #include <wrl/client.h> #include <wrl/wrappers/corewrappers.h> -#include <comdef.h> - -#if QT_CONFIG(cpp_winrt) && !defined(Q_CC_CLANG) -#define SUPPORTS_WINRT 1 -#endif -#ifdef SUPPORTS_WINRT -#include <winrt/base.h> +#if QT_CONFIG(cpp_winrt) +#include <QtCore/private/qt_winrtbase_p.h> #endif using namespace Microsoft::WRL; @@ -28,12 +28,6 @@ using namespace Microsoft::WRL; QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(lcNetInfoNLM) -inline QString errorStringFromHResult(HRESULT hr) -{ - _com_error error(hr); - return QString::fromWCharArray(error.ErrorMessage()); -} - class QNetworkListManagerEvents : public QObject, public INetworkListManagerEvents { Q_OBJECT @@ -56,7 +50,7 @@ public: HRESULT STDMETHODCALLTYPE ConnectivityChanged(NLM_CONNECTIVITY newConnectivity) override; [[nodiscard]] bool start(); - bool stop(); + void stop(); [[nodiscard]] bool checkBehindCaptivePortal(); @@ -69,10 +63,11 @@ private: ComPtr<INetworkListManager> networkListManager = nullptr; ComPtr<IConnectionPoint> connectionPoint = nullptr; -#ifdef SUPPORTS_WINRT +#if QT_CONFIG(cpp_winrt) void emitWinRTUpdates(); winrt::event_token token; + QMutex winrtLock; #endif QAtomicInteger<ULONG> ref = 0; @@ -80,3 +75,5 @@ private: }; QT_END_NAMESPACE + +#endif // QNETWORKLISTMANAGEREVENTS_H diff --git a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp index eb42abc48e..766648486e 100644 --- a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp +++ b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp @@ -9,6 +9,8 @@ #include <QtCore/private/qobject_p.h> #include <QtCore/qscopeguard.h> +#include <QtCore/private/qfunctions_win_p.h> + QT_BEGIN_NAMESPACE // Declared in qnetworklistmanagerevents.h @@ -68,8 +70,9 @@ public: { return QNetworkInformation::Features(QNetworkInformation::Feature::Reachability | QNetworkInformation::Feature::CaptivePortal -#ifdef SUPPORTS_WINRT +#if QT_CONFIG(cpp_winrt) | QNetworkInformation::Feature::TransportMedium + | QNetworkInformation::Feature::Metered #endif ); } @@ -82,12 +85,13 @@ private: void setConnectivity(NLM_CONNECTIVITY newConnectivity); void checkCaptivePortal(); + QComHelper comHelper; + ComPtr<QNetworkListManagerEvents> managerEvents; NLM_CONNECTIVITY connectivity = NLM_CONNECTIVITY_DISCONNECTED; bool monitoring = false; - bool comInitFailed = false; }; class QNetworkListManagerNetworkInformationBackendFactory : public QNetworkInformationBackendFactory @@ -121,12 +125,9 @@ public: QNetworkListManagerNetworkInformationBackend::QNetworkListManagerNetworkInformationBackend() { - auto hr = CoInitialize(nullptr); - if (FAILED(hr)) { - qCWarning(lcNetInfoNLM) << "Failed to initialize COM:" << errorStringFromHResult(hr); - comInitFailed = true; + if (!comHelper.isValid()) return; - } + managerEvents = new QNetworkListManagerEvents(); connect(managerEvents.Get(), &QNetworkListManagerEvents::connectivityChanged, this, &QNetworkListManagerNetworkInformationBackend::setConnectivity); @@ -140,8 +141,6 @@ QNetworkListManagerNetworkInformationBackend::QNetworkListManagerNetworkInformat QNetworkListManagerNetworkInformationBackend::~QNetworkListManagerNetworkInformationBackend() { - if (comInitFailed) - return; stop(); } @@ -164,11 +163,8 @@ void QNetworkListManagerNetworkInformationBackend::checkCaptivePortal() bool QNetworkListManagerNetworkInformationBackend::event(QEvent *event) { - if (event->type() == QEvent::ThreadChange && monitoring) { - stop(); - QMetaObject::invokeMethod(this, &QNetworkListManagerNetworkInformationBackend::start, - Qt::QueuedConnection); - } + if (event->type() == QEvent::ThreadChange) + qFatal("Moving QNetworkListManagerNetworkInformationBackend to different thread is not supported"); return QObject::event(event); } @@ -177,15 +173,9 @@ bool QNetworkListManagerNetworkInformationBackend::start() { Q_ASSERT(!monitoring); - if (comInitFailed) { - auto hr = CoInitialize(nullptr); - if (FAILED(hr)) { - qCWarning(lcNetInfoNLM) << "Failed to initialize COM:" << errorStringFromHResult(hr); - comInitFailed = true; - return false; - } - comInitFailed = false; - } + if (!comHelper.isValid()) + return false; + if (!managerEvents) managerEvents = new QNetworkListManagerEvents(); @@ -198,14 +188,10 @@ void QNetworkListManagerNetworkInformationBackend::stop() { if (monitoring) { Q_ASSERT(managerEvents); - // Can return false but realistically shouldn't since that would break everything: managerEvents->stop(); monitoring = false; managerEvents.Reset(); } - - CoUninitialize(); - comInitFailed = true; // we check this value in start() to see if we need to re-initialize } QT_END_NAMESPACE diff --git a/src/plugins/networkinformation/networkmanager/CMakeLists.txt b/src/plugins/networkinformation/networkmanager/CMakeLists.txt index 5fc69f2d55..9d76dbe7b4 100644 --- a/src/plugins/networkinformation/networkmanager/CMakeLists.txt +++ b/src/plugins/networkinformation/networkmanager/CMakeLists.txt @@ -1,9 +1,13 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QNetworkManagerNetworkInformationPlugin OUTPUT_NAME qnetworkmanager CLASS_NAME QNetworkManagerNetworkInformationBackendFactory PLUGIN_TYPE networkinformation DEFAULT_IF LINUX SOURCES + qnetworkmanagernetworkinformationbackend.h qnetworkmanagernetworkinformationbackend.cpp qnetworkmanagerservice.h qnetworkmanagerservice.cpp diff --git a/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.cpp b/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.cpp index 67129af2de..f583d1dcf6 100644 --- a/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.cpp +++ b/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.cpp @@ -1,9 +1,7 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#include <QtNetwork/private/qnetworkinformation_p.h> - -#include "qnetworkmanagerservice.h" +#include "qnetworkmanagernetworkinformationbackend.h" #include <QtCore/qglobal.h> #include <QtCore/private/qobject_p.h> @@ -94,46 +92,20 @@ bool isMeteredFromNMMetered(QNetworkManagerInterface::NMMetered metered) case QNetworkManagerInterface::NM_METERED_UNKNOWN: return false; } - Q_UNREACHABLE(); - return false; + Q_UNREACHABLE_RETURN(false); } } // unnamed namespace static QString backendName() { - return QString::fromUtf16(QNetworkInformationBackend::PluginNames - [QNetworkInformationBackend::PluginNamesLinuxIndex]); + return QStringView(QNetworkInformationBackend::PluginNames + [QNetworkInformationBackend::PluginNamesLinuxIndex]).toString(); } -class QNetworkManagerNetworkInformationBackend : public QNetworkInformationBackend +QString QNetworkManagerNetworkInformationBackend::name() const { - Q_OBJECT -public: - QNetworkManagerNetworkInformationBackend(); - ~QNetworkManagerNetworkInformationBackend() = default; - - QString name() const override { return backendName(); } - QNetworkInformation::Features featuresSupported() const override - { - if (!isValid()) - return {}; - return featuresSupportedStatic(); - } - - static QNetworkInformation::Features featuresSupportedStatic() - { - using Feature = QNetworkInformation::Feature; - return QNetworkInformation::Features(Feature::Reachability | Feature::CaptivePortal - | Feature::TransportMedium | Feature::Metered); - } - - bool isValid() const { return iface.isValid(); } - -private: - Q_DISABLE_COPY_MOVE(QNetworkManagerNetworkInformationBackend) - - QNetworkManagerInterface iface; -}; + return backendName(); +} class QNetworkManagerNetworkInformationBackendFactory : public QNetworkInformationBackendFactory { @@ -168,34 +140,42 @@ private: QNetworkManagerNetworkInformationBackend::QNetworkManagerNetworkInformationBackend() { - auto updateReachability = [this](QNetworkManagerInterface::NMState newState) { - setReachability(reachabilityFromNMState(newState)); - }; - updateReachability(iface.state()); - connect(&iface, &QNetworkManagerInterface::stateChanged, this, std::move(updateReachability)); - - auto updateBehindCaptivePortal = [this](QNetworkManagerInterface::NMConnectivityState state) { - const bool behindPortal = (state == QNetworkManagerInterface::NM_CONNECTIVITY_PORTAL); - setBehindCaptivePortal(behindPortal); - }; - updateBehindCaptivePortal(iface.connectivityState()); - connect(&iface, &QNetworkManagerInterface::connectivityChanged, this, - std::move(updateBehindCaptivePortal)); - - auto updateTransportMedium = [this](QNetworkManagerInterface::NMDeviceType newDevice) { - setTransportMedium(transportMediumFromDeviceType(newDevice)); - }; - updateTransportMedium(iface.deviceType()); - connect(&iface, &QNetworkManagerInterface::deviceTypeChanged, this, - std::move(updateTransportMedium)); - - auto updateMetered = [this](QNetworkManagerInterface::NMMetered metered) { - setMetered(isMeteredFromNMMetered(metered)); - }; - updateMetered(iface.meteredState()); - connect(&iface, &QNetworkManagerInterface::meteredChanged, this, std::move(updateMetered)); + if (!iface.isValid()) + return; + iface.setBackend(this); + onStateChanged(iface.state()); + onConnectivityChanged(iface.connectivityState()); + onDeviceTypeChanged(iface.deviceType()); + onMeteredChanged(iface.meteredState()); +} + +void QNetworkManagerNetworkInformationBackend::onStateChanged( + QNetworkManagerInterface::NMState newState) +{ + setReachability(reachabilityFromNMState(newState)); } +void QNetworkManagerNetworkInformationBackend::onConnectivityChanged( + QNetworkManagerInterface::NMConnectivityState connectivityState) +{ + const bool behindPortal = + (connectivityState == QNetworkManagerInterface::NM_CONNECTIVITY_PORTAL); + setBehindCaptivePortal(behindPortal); +} + +void QNetworkManagerNetworkInformationBackend::onDeviceTypeChanged( + QNetworkManagerInterface::NMDeviceType newDevice) +{ + setTransportMedium(transportMediumFromDeviceType(newDevice)); +} + +void QNetworkManagerNetworkInformationBackend::onMeteredChanged( + QNetworkManagerInterface::NMMetered metered) +{ + setMetered(isMeteredFromNMMetered(metered)); +}; + QT_END_NAMESPACE #include "qnetworkmanagernetworkinformationbackend.moc" +#include "moc_qnetworkmanagernetworkinformationbackend.cpp" diff --git a/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.h b/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.h new file mode 100644 index 0000000000..3b60f0949c --- /dev/null +++ b/src/plugins/networkinformation/networkmanager/qnetworkmanagernetworkinformationbackend.h @@ -0,0 +1,60 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QNETWORKMANAGERINFORMATIONBACKEND_H +#define QNETWORKMANAGERINFORMATIONBACKEND_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtNetwork/private/qnetworkinformation_p.h> +#include "qnetworkmanagerservice.h" + +QT_BEGIN_NAMESPACE + +class QNetworkManagerNetworkInformationBackend : public QNetworkInformationBackend +{ + Q_OBJECT +public: + QNetworkManagerNetworkInformationBackend(); + ~QNetworkManagerNetworkInformationBackend() = default; + + QString name() const override; + QNetworkInformation::Features featuresSupported() const override + { + if (!isValid()) + return {}; + return featuresSupportedStatic(); + } + + static QNetworkInformation::Features featuresSupportedStatic() + { + using Feature = QNetworkInformation::Feature; + return QNetworkInformation::Features(Feature::Reachability | Feature::CaptivePortal + | Feature::TransportMedium | Feature::Metered); + } + + bool isValid() const { return iface.isValid(); } + + void onStateChanged(QNetworkManagerInterface::NMState state); + void onConnectivityChanged(QNetworkManagerInterface::NMConnectivityState connectivityState); + void onDeviceTypeChanged(QNetworkManagerInterface::NMDeviceType deviceType); + void onMeteredChanged(QNetworkManagerInterface::NMMetered metered); + +private: + Q_DISABLE_COPY_MOVE(QNetworkManagerNetworkInformationBackend) + + QNetworkManagerInterface iface; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.cpp b/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.cpp index 3a64c1892c..c055555cac 100644 --- a/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.cpp +++ b/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qnetworkmanagerservice.h" +#include "qnetworkmanagernetworkinformationbackend.h" #include <QObject> #include <QList> @@ -14,21 +15,40 @@ #include <QtDBus/QDBusObjectPath> #include <QtDBus/QDBusPendingCall> -#define DBUS_PROPERTIES_INTERFACE "org.freedesktop.DBus.Properties" +#define DBUS_PROPERTIES_INTERFACE "org.freedesktop.DBus.Properties"_L1 -#define NM_DBUS_SERVICE "org.freedesktop.NetworkManager" +#define NM_DBUS_INTERFACE "org.freedesktop.NetworkManager" +#define NM_DBUS_SERVICE NM_DBUS_INTERFACE ""_L1 -#define NM_DBUS_PATH "/org/freedesktop/NetworkManager" -#define NM_DBUS_INTERFACE NM_DBUS_SERVICE -#define NM_CONNECTION_DBUS_INTERFACE NM_DBUS_SERVICE ".Connection.Active" -#define NM_DEVICE_DBUS_INTERFACE NM_DBUS_SERVICE ".Device" +#define NM_DBUS_PATH "/org/freedesktop/NetworkManager"_L1 +#define NM_CONNECTION_DBUS_INTERFACE NM_DBUS_SERVICE ".Connection.Active"_L1 +#define NM_DEVICE_DBUS_INTERFACE NM_DBUS_SERVICE ".Device"_L1 QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; +namespace { +constexpr QLatin1StringView propertiesChangedKey = "PropertiesChanged"_L1; +const QString &stateKey() +{ + static auto key = u"State"_s; + return key; +} +const QString &connectivityKey() +{ + static auto key = u"Connectivity"_s; + return key; +} +const QString &primaryConnectionKey() +{ + static auto key = u"PrimaryConnection"_s; + return key; +} +} + QNetworkManagerInterfaceBase::QNetworkManagerInterfaceBase(QObject *parent) - : QDBusAbstractInterface(NM_DBUS_SERVICE ""_L1, NM_DBUS_PATH ""_L1, + : QDBusAbstractInterface(NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, QDBusConnection::systemBus(), parent) { } @@ -41,45 +61,49 @@ bool QNetworkManagerInterfaceBase::networkManagerAvailable() QNetworkManagerInterface::QNetworkManagerInterface(QObject *parent) : QNetworkManagerInterfaceBase(parent) { - if (!isValid()) + if (!QDBusAbstractInterface::isValid()) return; PropertiesDBusInterface managerPropertiesInterface( - NM_DBUS_SERVICE ""_L1, NM_DBUS_PATH ""_L1, DBUS_PROPERTIES_INTERFACE, + NM_DBUS_SERVICE, NM_DBUS_PATH, DBUS_PROPERTIES_INTERFACE, QDBusConnection::systemBus()); QList<QVariant> argumentList; - argumentList << NM_DBUS_INTERFACE ""_L1; + argumentList << NM_DBUS_SERVICE; QDBusPendingReply<QVariantMap> propsReply = managerPropertiesInterface.callWithArgumentList( QDBus::Block, "GetAll"_L1, argumentList); - if (!propsReply.isError()) { - propertyMap = propsReply.value(); - } else { - qWarning() << "propsReply" << propsReply.error().message(); + if (propsReply.isError()) { + validDBusConnection = false; + if (auto error = propsReply.error(); error.type() != QDBusError::AccessDenied) + qWarning() << "Failed to query NetworkManager properties:" << error.message(); + return; } + propertyMap = propsReply.value(); - QDBusConnection::systemBus().connect(NM_DBUS_SERVICE ""_L1, NM_DBUS_PATH ""_L1, - DBUS_PROPERTIES_INTERFACE""_L1, "PropertiesChanged"_L1, this, - SLOT(setProperties(QString, QMap<QString, QVariant>, QList<QString>))); + validDBusConnection = QDBusConnection::systemBus().connect(NM_DBUS_SERVICE, NM_DBUS_PATH, + DBUS_PROPERTIES_INTERFACE, propertiesChangedKey, this, + SLOT(setProperties(QString,QMap<QString,QVariant>,QList<QString>))); } QNetworkManagerInterface::~QNetworkManagerInterface() { - QDBusConnection::systemBus().disconnect(NM_DBUS_SERVICE ""_L1, NM_DBUS_PATH ""_L1, - DBUS_PROPERTIES_INTERFACE ""_L1, "PropertiesChanged"_L1, this, - SLOT(setProperties(QString, QMap<QString, QVariant>, QList<QString>))); + QDBusConnection::systemBus().disconnect(NM_DBUS_SERVICE, NM_DBUS_PATH, + DBUS_PROPERTIES_INTERFACE, propertiesChangedKey, this, + SLOT(setProperties(QString,QMap<QString,QVariant>,QList<QString>))); } QNetworkManagerInterface::NMState QNetworkManagerInterface::state() const { - if (propertyMap.contains("State")) - return static_cast<QNetworkManagerInterface::NMState>(propertyMap.value("State").toUInt()); + auto it = propertyMap.constFind(stateKey()); + if (it != propertyMap.cend()) + return static_cast<QNetworkManagerInterface::NMState>(it->toUInt()); return QNetworkManagerInterface::NM_STATE_UNKNOWN; } QNetworkManagerInterface::NMConnectivityState QNetworkManagerInterface::connectivityState() const { - if (propertyMap.contains("Connectivity")) - return static_cast<NMConnectivityState>(propertyMap.value("Connectivity").toUInt()); + auto it = propertyMap.constFind(connectivityKey()); + if (it != propertyMap.cend()) + return static_cast<NMConnectivityState>(it->toUInt()); return QNetworkManagerInterface::NM_CONNECTIVITY_UNKNOWN; } @@ -102,7 +126,7 @@ static std::optional<QDBusInterface> getPrimaryDevice(const QDBusObjectPath &dev std::optional<QDBusObjectPath> QNetworkManagerInterface::primaryConnectionDevicePath() const { - auto it = propertyMap.constFind(u"PrimaryConnection"_s); + auto it = propertyMap.constFind(primaryConnectionKey()); if (it != propertyMap.cend()) return it->value<QDBusObjectPath>(); return std::nullopt; @@ -150,6 +174,11 @@ auto QNetworkManagerInterface::extractDeviceMetered(const QDBusObjectPath &devic return static_cast<NMMetered>(metered.toUInt()); } +void QNetworkManagerInterface::setBackend(QNetworkManagerNetworkInformationBackend *ourBackend) +{ + backend = ourBackend; +} + void QNetworkManagerInterface::setProperties(const QString &interfaceName, const QMap<QString, QVariant> &map, const QStringList &invalidatedProperties) @@ -169,18 +198,18 @@ void QNetworkManagerInterface::setProperties(const QString &interfaceName, } if (valueChanged) { - if (i.key() == "State"_L1) { + if (i.key() == stateKey()) { quint32 state = i.value().toUInt(); - Q_EMIT stateChanged(static_cast<NMState>(state)); - } else if (i.key() == "Connectivity"_L1) { + backend->onStateChanged(static_cast<NMState>(state)); + } else if (i.key() == connectivityKey()) { quint32 state = i.value().toUInt(); - Q_EMIT connectivityChanged(static_cast<NMConnectivityState>(state)); - } else if (i.key() == "PrimaryConnection"_L1) { + backend->onConnectivityChanged(static_cast<NMConnectivityState>(state)); + } else if (i.key() == primaryConnectionKey()) { const QDBusObjectPath devicePath = i->value<QDBusObjectPath>(); - Q_EMIT deviceTypeChanged(extractDeviceType(devicePath)); - Q_EMIT meteredChanged(extractDeviceMetered(devicePath)); + backend->onDeviceTypeChanged(extractDeviceType(devicePath)); + backend->onMeteredChanged(extractDeviceMetered(devicePath)); } else if (i.key() == "Metered"_L1) { - Q_EMIT meteredChanged(static_cast<NMMetered>(i->toUInt())); + backend->onMeteredChanged(static_cast<NMMetered>(i->toUInt())); } } } diff --git a/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.h b/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.h index 30e7945d28..5201e8485b 100644 --- a/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.h +++ b/src/plugins/networkinformation/networkmanager/qnetworkmanagerservice.h @@ -39,6 +39,7 @@ enum NMDeviceState { QT_BEGIN_NAMESPACE class QDBusObjectPath; +class QNetworkManagerNetworkInformationBackend; // This tiny class exists for the purpose of seeing if NetworkManager is available without // initializing everything the derived/full class needs. @@ -46,7 +47,7 @@ class QNetworkManagerInterfaceBase : public QDBusAbstractInterface { Q_OBJECT public: - QNetworkManagerInterfaceBase(QObject *parent = nullptr); + explicit QNetworkManagerInterfaceBase(QObject *parent = nullptr); ~QNetworkManagerInterfaceBase() = default; static bool networkManagerAvailable(); @@ -128,19 +129,17 @@ public: NM_METERED_GUESS_NO, }; - QNetworkManagerInterface(QObject *parent = nullptr); + explicit QNetworkManagerInterface(QObject *parent = nullptr); ~QNetworkManagerInterface(); + void setBackend(QNetworkManagerNetworkInformationBackend *ourBackend); + NMState state() const; NMConnectivityState connectivityState() const; NMDeviceType deviceType() const; NMMetered meteredState() const; -Q_SIGNALS: - void stateChanged(NMState); - void connectivityChanged(NMConnectivityState); - void deviceTypeChanged(NMDeviceType); - void meteredChanged(NMMetered); + bool isValid() const { return QDBusAbstractInterface::isValid() && validDBusConnection; } private Q_SLOTS: void setProperties(const QString &interfaceName, const QMap<QString, QVariant> &map, @@ -155,13 +154,15 @@ private: std::optional<QDBusObjectPath> primaryConnectionDevicePath() const; QVariantMap propertyMap; + QNetworkManagerNetworkInformationBackend *backend = nullptr; + bool validDBusConnection = true; }; class PropertiesDBusInterface : public QDBusAbstractInterface { public: PropertiesDBusInterface(const QString &service, const QString &path, const QString &interface, - const QDBusConnection &connection, QObject *parent = 0) + const QDBusConnection &connection, QObject *parent = nullptr) : QDBusAbstractInterface(service, path, interface.toLatin1().data(), connection, parent) { } diff --git a/src/plugins/networkinformation/scnetworkreachability/CMakeLists.txt b/src/plugins/networkinformation/scnetworkreachability/CMakeLists.txt index c9ee7d9a42..a939ab4405 100644 --- a/src/plugins/networkinformation/scnetworkreachability/CMakeLists.txt +++ b/src/plugins/networkinformation/scnetworkreachability/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QSCNetworkReachabilityNetworkInformationPlugin OUTPUT_NAME qscnetworkreachability CLASS_NAME QSCNetworkReachabilityNetworkInformationBackendFactory diff --git a/src/plugins/platforminputcontexts/CMakeLists.txt b/src/plugins/platforminputcontexts/CMakeLists.txt index b5150df4f3..78b3ec99d9 100644 --- a/src/plugins/platforminputcontexts/CMakeLists.txt +++ b/src/plugins/platforminputcontexts/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from platforminputcontexts.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_xkbcommon) add_subdirectory(compose) diff --git a/src/plugins/platforminputcontexts/compose/CMakeLists.txt b/src/plugins/platforminputcontexts/compose/CMakeLists.txt index e3fbf913b9..9e71a7a07c 100644 --- a/src/plugins/platforminputcontexts/compose/CMakeLists.txt +++ b/src/plugins/platforminputcontexts/compose/CMakeLists.txt @@ -1,11 +1,12 @@ -# Generated from compose.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QComposePlatformInputContextPlugin Plugin: ##################################################################### -qt_find_package(XKB) # special case -pkg_get_variable(PKG_X11_PREFIX x11 prefix) # special case +qt_find_package(XKB) +pkg_get_variable(PKG_X11_PREFIX x11 prefix) qt_internal_add_plugin(QComposePlatformInputContextPlugin OUTPUT_NAME composeplatforminputcontextplugin @@ -21,7 +22,3 @@ qt_internal_add_plugin(QComposePlatformInputContextPlugin Qt::GuiPrivate XKB::XKB ) - -#### Keys ignored in scope 1:.:.:compose.pro:<TRUE>: -# OTHER_FILES = "$$PWD/compose.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforminputcontexts/compose/qcomposeplatforminputcontext.cpp b/src/plugins/platforminputcontexts/compose/qcomposeplatforminputcontext.cpp index 7e66e5fae8..3e74189076 100644 --- a/src/plugins/platforminputcontexts/compose/qcomposeplatforminputcontext.cpp +++ b/src/plugins/platforminputcontexts/compose/qcomposeplatforminputcontext.cpp @@ -108,8 +108,7 @@ bool QComposeInputContext::filterEvent(const QEvent *event) case XKB_COMPOSE_NOTHING: return false; default: - Q_UNREACHABLE(); - return false; + Q_UNREACHABLE_RETURN(false); } } diff --git a/src/plugins/platforminputcontexts/ibus/CMakeLists.txt b/src/plugins/platforminputcontexts/ibus/CMakeLists.txt index 7ccc627eb1..54847e86fd 100644 --- a/src/plugins/platforminputcontexts/ibus/CMakeLists.txt +++ b/src/plugins/platforminputcontexts/ibus/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from ibus.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QIbusPlatformInputContextPlugin Plugin: @@ -22,7 +23,3 @@ qt_internal_add_plugin(QIbusPlatformInputContextPlugin Qt::GuiPrivate XKB::XKB ) - -#### Keys ignored in scope 1:.:.:ibus.pro:<TRUE>: -# OTHER_FILES = "$$PWD/ibus.json" -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml b/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml index 9c67a38c57..30fa7431c3 100644 --- a/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml +++ b/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml @@ -2,6 +2,12 @@ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> <node> <interface name="org.freedesktop.IBus.InputContext"> + <property name="ClientCommitPreedit" type="(b)" access="readwrite"> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="QIBusPropTypeClientCommitPreedit"/> + </property> + <property name='ContentType' type='(uu)' access='readwrite'> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="QIBusPropTypeContentType"/> + </property> <method name="ProcessKeyEvent"> <arg name="keyval" direction="in" type="u"/> <arg name="keycode" direction="in" type="u"/> @@ -14,6 +20,12 @@ <arg name="w" direction="in" type="i"/> <arg name="h" direction="in" type="i"/> </method> + <method name='SetCursorLocationRelative'> + <arg name="x" direction="in" type="i"/> + <arg name="y" direction="in" type="i"/> + <arg name="w" direction="in" type="i"/> + <arg name="h" direction="in" type="i"/> + </method> <method name="FocusIn"/> <method name="FocusOut"/> <method name="Reset"/> @@ -56,6 +68,12 @@ <arg name="cursor_pos" type="u"/> <arg name="visible" type="b"/> </signal> + <signal name="UpdatePreeditTextWithMode"> + <arg name="text" type="v"/> + <arg name="cursor_pos" type="u"/> + <arg name="visible" type="b"/> + <arg name="mode" type="u"/> + </signal> <signal name="ShowPreeditText"/> <signal name="HidePreeditText"/> <signal name="UpdateAuxiliaryText"> diff --git a/src/plugins/platforminputcontexts/ibus/main.cpp b/src/plugins/platforminputcontexts/ibus/main.cpp index 52d2cd0084..d74be4bedf 100644 --- a/src/plugins/platforminputcontexts/ibus/main.cpp +++ b/src/plugins/platforminputcontexts/ibus/main.cpp @@ -28,9 +28,12 @@ QIBusPlatformInputContext *QIbusPlatformInputContextPlugin::create(const QString qDBusRegisterMetaType<QIBusAttribute>(); qDBusRegisterMetaType<QIBusAttributeList>(); qDBusRegisterMetaType<QIBusText>(); + qDBusRegisterMetaType<QIBusPropTypeClientCommitPreedit>(); + qDBusRegisterMetaType<QIBusPropTypeContentType>(); return new QIBusPlatformInputContext; } - return 0; + + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.cpp b/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.cpp index 8e2027272a..248abbc32b 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.cpp +++ b/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.cpp @@ -2,7 +2,7 @@ * This file was generated by qdbusxml2cpp version 0.8 * Command line was: qdbusxml2cpp -N -p qibusinputcontextproxy -c QIBusInputContextProxy interfaces/org.freedesktop.IBus.InputContext.xml * - * qdbusxml2cpp is Copyright (C) 2015 The Qt Company Ltd. + * qdbusxml2cpp is Copyright (C) 2023 The Qt Company Ltd and other contributors. * * This is an auto-generated file. * This file may have been hand-edited. Look for HAND-EDIT comments @@ -24,4 +24,3 @@ QIBusInputContextProxy::~QIBusInputContextProxy() { } -#include "moc_qibusinputcontextproxy.cpp" diff --git a/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h b/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h index 3d11706c06..82e78aa35b 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h +++ b/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h @@ -2,25 +2,26 @@ * This file was generated by qdbusxml2cpp version 0.8 * Command line was: qdbusxml2cpp -N -p qibusinputcontextproxy -c QIBusInputContextProxy interfaces/org.freedesktop.IBus.InputContext.xml * - * qdbusxml2cpp is Copyright (C) 2015 The Qt Company Ltd. + * qdbusxml2cpp is Copyright (C) 2023 The Qt Company Ltd and other contributors. * * This is an auto-generated file. * Do not edit! All changes made to it will be lost. */ -#ifndef QIBUSINPUTCONTEXTPROXY_H_1394889529 -#define QIBUSINPUTCONTEXTPROXY_H_1394889529 +#ifndef QIBUSINPUTCONTEXTPROXY_H +#define QIBUSINPUTCONTEXTPROXY_H -#include <QObject> -#include <QByteArray> -#include <QList> -#include <QMap> -#include <QString> -#include <QStringList> -#include <QVariant> -#include <QDBusAbstractInterface> -#include <QDBusPendingReply> +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> +// Added for QIBusPropTypeClientCommitPreedit and QIBusPropTypeContentType +#include "qibustypes.h" /* * Proxy class for interface org.freedesktop.IBus.InputContext @@ -37,95 +38,114 @@ public: ~QIBusInputContextProxy(); + Q_PROPERTY(QIBusPropTypeClientCommitPreedit ClientCommitPreedit READ clientCommitPreedit WRITE setClientCommitPreedit) + inline QIBusPropTypeClientCommitPreedit clientCommitPreedit() const + { return qvariant_cast< QIBusPropTypeClientCommitPreedit >(property("ClientCommitPreedit")); } + inline void setClientCommitPreedit(const QIBusPropTypeClientCommitPreedit &value) + { setProperty("ClientCommitPreedit", QVariant::fromValue(value)); } + + Q_PROPERTY(QIBusPropTypeContentType ContentType READ contentType WRITE setContentType) + inline QIBusPropTypeContentType contentType() const + { return qvariant_cast< QIBusPropTypeContentType >(property("ContentType")); } + inline void setContentType(const QIBusPropTypeContentType &value) + { setProperty("ContentType", QVariant::fromValue(value)); } + public Q_SLOTS: // METHODS inline QDBusPendingReply<> Destroy() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("Destroy"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("Destroy"), argumentList); } inline QDBusPendingReply<> Disable() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("Disable"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("Disable"), argumentList); } inline QDBusPendingReply<> Enable() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("Enable"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("Enable"), argumentList); } inline QDBusPendingReply<> FocusIn() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("FocusIn"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("FocusIn"), argumentList); } inline QDBusPendingReply<> FocusOut() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("FocusOut"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("FocusOut"), argumentList); } inline QDBusPendingReply<QDBusVariant> GetEngine() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("GetEngine"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("GetEngine"), argumentList); } inline QDBusPendingReply<bool> IsEnabled() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("IsEnabled"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("IsEnabled"), argumentList); } inline QDBusPendingReply<bool> ProcessKeyEvent(uint keyval, uint keycode, uint state) { QList<QVariant> argumentList; argumentList << QVariant::fromValue(keyval) << QVariant::fromValue(keycode) << QVariant::fromValue(state); - return asyncCallWithArgumentList(QLatin1String("ProcessKeyEvent"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("ProcessKeyEvent"), argumentList); } inline QDBusPendingReply<> PropertyActivate(const QString &name, int state) { QList<QVariant> argumentList; argumentList << QVariant::fromValue(name) << QVariant::fromValue(state); - return asyncCallWithArgumentList(QLatin1String("PropertyActivate"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("PropertyActivate"), argumentList); } inline QDBusPendingReply<> Reset() { QList<QVariant> argumentList; - return asyncCallWithArgumentList(QLatin1String("Reset"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("Reset"), argumentList); } inline QDBusPendingReply<> SetCapabilities(uint caps) { QList<QVariant> argumentList; argumentList << QVariant::fromValue(caps); - return asyncCallWithArgumentList(QLatin1String("SetCapabilities"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("SetCapabilities"), argumentList); } inline QDBusPendingReply<> SetCursorLocation(int x, int y, int w, int h) { QList<QVariant> argumentList; argumentList << QVariant::fromValue(x) << QVariant::fromValue(y) << QVariant::fromValue(w) << QVariant::fromValue(h); - return asyncCallWithArgumentList(QLatin1String("SetCursorLocation"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("SetCursorLocation"), argumentList); + } + + inline QDBusPendingReply<> SetCursorLocationRelative(int x, int y, int w, int h) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(x) << QVariant::fromValue(y) << QVariant::fromValue(w) << QVariant::fromValue(h); + return asyncCallWithArgumentList(QStringLiteral("SetCursorLocationRelative"), argumentList); } inline QDBusPendingReply<> SetEngine(const QString &name) { QList<QVariant> argumentList; argumentList << QVariant::fromValue(name); - return asyncCallWithArgumentList(QLatin1String("SetEngine"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("SetEngine"), argumentList); } inline QDBusPendingReply<> SetSurroundingText(const QDBusVariant &text, uint cursor_pos, uint anchor_pos) { QList<QVariant> argumentList; argumentList << QVariant::fromValue(text) << QVariant::fromValue(cursor_pos) << QVariant::fromValue(anchor_pos); - return asyncCallWithArgumentList(QLatin1String("SetSurroundingText"), argumentList); + return asyncCallWithArgumentList(QStringLiteral("SetSurroundingText"), argumentList); } Q_SIGNALS: // SIGNALS @@ -149,6 +169,7 @@ Q_SIGNALS: // SIGNALS void UpdateAuxiliaryText(const QDBusVariant &text, bool visible); void UpdateLookupTable(const QDBusVariant &table, bool visible); void UpdatePreeditText(const QDBusVariant &text, uint cursor_pos, bool visible); + void UpdatePreeditTextWithMode(const QDBusVariant &text, uint cursor_pos, bool visible, uint mode); void UpdateProperty(const QDBusVariant &prop); }; diff --git a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp index ca468f5ada..00c7884cda 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp +++ b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp @@ -8,6 +8,7 @@ #include <QWindow> #include <QEvent> #include <QFile> +#include <QFileInfo> #include <QStandardPaths> #include <QDBusVariant> #include <QDBusPendingReply> @@ -26,6 +27,8 @@ #include <private/qguiapplication_p.h> #include <private/qxkbcommon_p.h> +#include <memory> + #include <sys/types.h> #include <signal.h> @@ -46,26 +49,35 @@ enum { debug = 0 }; class QIBusPlatformInputContextPrivate { + Q_DISABLE_COPY_MOVE(QIBusPlatformInputContextPrivate) public: + // This enum might be synced with IBusPreeditFocusMode + // in ibustypes.h of IBUS project + enum PreeditFocusMode { + PREEDIT_CLEAR = 0, + PREEDIT_COMMIT = 1, + }; + QIBusPlatformInputContextPrivate(); ~QIBusPlatformInputContextPrivate() { - delete context; - delete bus; - delete portalBus; - delete connection; + // dereference QDBusConnection to actually disconnect + serviceWatcher.setConnection(QDBusConnection(QString())); + context = nullptr; + portalBus = nullptr; + bus = nullptr; + QDBusConnection::disconnectFromBus("QIBusProxy"_L1); } static QString getSocketPath(); - QDBusConnection *createConnection(); + void createConnection(); void initBus(); void createBusProxy(); - QDBusConnection *connection; - QIBusProxy *bus; - QIBusProxyPortal *portalBus; // bus and portalBus are alternative. - QIBusInputContextProxy *context; + std::unique_ptr<QIBusProxy> bus; + std::unique_ptr<QIBusProxyPortal> portalBus; // bus and portalBus are alternative. + std::unique_ptr<QIBusInputContextProxy> context; QDBusServiceWatcher serviceWatcher; bool usePortal; // return value of shouldConnectIbusPortal @@ -75,6 +87,7 @@ public: QList<QInputMethodEvent::Attribute> attributes; bool needsSurroundingText; QLocale locale; + PreeditFocusMode preeditFocusMode = PREEDIT_COMMIT; // for backward compatibility }; @@ -166,10 +179,18 @@ void QIBusPlatformInputContext::commit() return; } - if (!d->predit.isEmpty()) { - QInputMethodEvent event; - event.setCommitString(d->predit); - QCoreApplication::sendEvent(input, &event); + if (d->preeditFocusMode == QIBusPlatformInputContextPrivate::PREEDIT_COMMIT) { + if (!d->predit.isEmpty()) { + QInputMethodEvent event; + event.setCommitString(d->predit); + QCoreApplication::sendEvent(input, &event); + } + } else { + if (!d->predit.isEmpty()) { + // Clear the existing preedit + QInputMethodEvent event; + QCoreApplication::sendEvent(input, &event); + } } d->context->Reset(); @@ -218,10 +239,31 @@ void QIBusPlatformInputContext::cursorRectChanged() QWindow *inputWindow = qApp->focusWindow(); if (!inputWindow) return; - r.moveTopLeft(inputWindow->mapToGlobal(r.topLeft())); + if (!inputWindow->screen()) + return; + + if (QGuiApplication::platformName().startsWith("wayland"_L1)) { + auto margins = inputWindow->frameMargins(); + r.translate(margins.left(), margins.top()); + qreal scale = inputWindow->devicePixelRatio(); + QRect newRect = QRect(r.x() * scale, r.y() * scale, r.width() * scale, r.height() * scale); + if (debug) + qDebug() << "microFocus" << newRect; + d->context->SetCursorLocationRelative(newRect.x(), newRect.y(), + newRect.width(), newRect.height()); + return; + } + + // x11/xcb + auto screenGeometry = inputWindow->screen()->geometry(); + auto point = inputWindow->mapToGlobal(r.topLeft()); + qreal scale = inputWindow->devicePixelRatio(); + auto native = (point - screenGeometry.topLeft()) * scale + screenGeometry.topLeft(); + QRect newRect(native, r.size() * scale); if (debug) - qDebug() << "microFocus" << r; - d->context->SetCursorLocation(r.x(), r.y(), r.width(), r.height()); + qDebug() << "microFocus" << newRect; + d->context->SetCursorLocation(newRect.x(), newRect.y(), + newRect.width(), newRect.height()); } void QIBusPlatformInputContext::setFocusObject(QObject *object) @@ -232,7 +274,7 @@ void QIBusPlatformInputContext::setFocusObject(QObject *object) // It would seem natural here to call FocusOut() on the input method if we // transition from an IME accepted focus object to one that does not accept it. // Mysteriously however that is not sufficient to fix bug QTBUG-63066. - if (!inputMethodAccepted()) + if (object && !inputMethodAccepted()) return; if (debug) @@ -292,6 +334,15 @@ void QIBusPlatformInputContext::updatePreeditText(const QDBusVariant &text, uint d->predit = t.text; } +void QIBusPlatformInputContext::updatePreeditTextWithMode(const QDBusVariant &text, uint cursorPos, bool visible, uint mode) +{ + updatePreeditText(text, cursorPos, visible); + if (mode > 0) + d->preeditFocusMode = QIBusPlatformInputContextPrivate::PreeditFocusMode::PREEDIT_COMMIT; + else + d->preeditFocusMode = QIBusPlatformInputContextPrivate::PreeditFocusMode::PREEDIT_CLEAR; +} + void QIBusPlatformInputContext::forwardKeyEvent(uint keyval, uint keycode, uint state) { if (!qApp) @@ -468,7 +519,7 @@ void QIBusPlatformInputContext::filterEventFinished(QDBusPendingCallWatcher *cal if (!filtered) { #ifndef QT_NO_CONTEXTMENU if (type == QEvent::KeyPress && qtcode == Qt::Key_Menu - && window != NULL) { + && window != nullptr) { const QPoint globalPos = window->screen()->handle()->cursor()->pos(); const QPoint pos = window->mapFromGlobal(globalPos); QWindowSystemInterfacePrivate::ContextMenuEvent contextMenuEvent(window, false, pos, @@ -498,12 +549,12 @@ void QIBusPlatformInputContext::socketChanged(const QString &str) m_timer.stop(); - if (d->context) - disconnect(d->context); - if (d->bus && d->bus->isValid()) - disconnect(d->bus); - if (d->connection) - d->connection->disconnectFromBus("QIBusProxy"_L1); + // dereference QDBusConnection to actually disconnect + d->serviceWatcher.setConnection(QDBusConnection(QString())); + d->context = nullptr; + d->bus = nullptr; + d->busConnected = false; + QDBusConnection::disconnectFromBus("QIBusProxy"_L1); m_timer.start(100); } @@ -555,37 +606,34 @@ void QIBusPlatformInputContext::globalEngineChanged(const QString &engine_name) void QIBusPlatformInputContext::connectToContextSignals() { if (d->bus && d->bus->isValid()) { - connect(d->bus, SIGNAL(GlobalEngineChanged(QString)), this, SLOT(globalEngineChanged(QString))); + connect(d->bus.get(), SIGNAL(GlobalEngineChanged(QString)), this, SLOT(globalEngineChanged(QString))); } if (d->context) { - connect(d->context, SIGNAL(CommitText(QDBusVariant)), SLOT(commitText(QDBusVariant))); - connect(d->context, SIGNAL(UpdatePreeditText(QDBusVariant,uint,bool)), this, SLOT(updatePreeditText(QDBusVariant,uint,bool))); - connect(d->context, SIGNAL(ForwardKeyEvent(uint,uint,uint)), this, SLOT(forwardKeyEvent(uint,uint,uint))); - connect(d->context, SIGNAL(DeleteSurroundingText(int,uint)), this, SLOT(deleteSurroundingText(int,uint))); - connect(d->context, SIGNAL(RequireSurroundingText()), this, SLOT(surroundingTextRequired())); - connect(d->context, SIGNAL(HidePreeditText()), this, SLOT(hidePreeditText())); - connect(d->context, SIGNAL(ShowPreeditText()), this, SLOT(showPreeditText())); + connect(d->context.get(), SIGNAL(CommitText(QDBusVariant)), SLOT(commitText(QDBusVariant))); + connect(d->context.get(), SIGNAL(UpdatePreeditText(QDBusVariant,uint,bool)), this, SLOT(updatePreeditText(QDBusVariant,uint,bool))); + connect(d->context.get(), SIGNAL(UpdatePreeditTextWithMode(QDBusVariant,uint,bool,uint)), this, SLOT(updatePreeditTextWithMode(QDBusVariant,uint,bool,uint))); + connect(d->context.get(), SIGNAL(ForwardKeyEvent(uint,uint,uint)), this, SLOT(forwardKeyEvent(uint,uint,uint))); + connect(d->context.get(), SIGNAL(DeleteSurroundingText(int,uint)), this, SLOT(deleteSurroundingText(int,uint))); + connect(d->context.get(), SIGNAL(RequireSurroundingText()), this, SLOT(surroundingTextRequired())); + connect(d->context.get(), SIGNAL(HidePreeditText()), this, SLOT(hidePreeditText())); + connect(d->context.get(), SIGNAL(ShowPreeditText()), this, SLOT(showPreeditText())); } } -static inline bool checkRunningUnderFlatpak() +static inline bool checkNeedPortalSupport() { - return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, "flatpak-info"_L1).isEmpty(); + return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP"); } static bool shouldConnectIbusPortal() { // honor the same env as ibus-gtk - return (checkRunningUnderFlatpak() || !qgetenv("IBUS_USE_PORTAL").isNull()); + return (checkNeedPortalSupport() || qEnvironmentVariableIsSet("IBUS_USE_PORTAL")); } QIBusPlatformInputContextPrivate::QIBusPlatformInputContextPrivate() - : connection(0), - bus(0), - portalBus(0), - context(0), - usePortal(shouldConnectIbusPortal()), + : usePortal(shouldConnectIbusPortal()), valid(false), busConnected(false), needsSurroundingText(false) @@ -609,22 +657,23 @@ QIBusPlatformInputContextPrivate::QIBusPlatformInputContextPrivate() void QIBusPlatformInputContextPrivate::initBus() { - connection = createConnection(); + createConnection(); busConnected = false; createBusProxy(); } void QIBusPlatformInputContextPrivate::createBusProxy() { - if (!connection || !connection->isConnected()) + QDBusConnection connection("QIBusProxy"_L1); + if (!connection.isConnected()) return; const char* ibusService = usePortal ? "org.freedesktop.portal.IBus" : "org.freedesktop.IBus"; QDBusReply<QDBusObjectPath> ic; if (usePortal) { - portalBus = new QIBusProxyPortal(QLatin1StringView(ibusService), - "/org/freedesktop/IBus"_L1, - *connection); + portalBus = std::make_unique<QIBusProxyPortal>(QLatin1StringView(ibusService), + "/org/freedesktop/IBus"_L1, + connection); if (!portalBus->isValid()) { qWarning("QIBusPlatformInputContext: invalid portal bus."); return; @@ -632,9 +681,9 @@ void QIBusPlatformInputContextPrivate::createBusProxy() ic = portalBus->CreateInputContext("QIBusInputContext"_L1); } else { - bus = new QIBusProxy(QLatin1StringView(ibusService), - "/org/freedesktop/IBus"_L1, - *connection); + bus = std::make_unique<QIBusProxy>(QLatin1StringView(ibusService), + "/org/freedesktop/IBus"_L1, + connection); if (!bus->isValid()) { qWarning("QIBusPlatformInputContext: invalid bus."); return; @@ -644,7 +693,7 @@ void QIBusPlatformInputContextPrivate::createBusProxy() } serviceWatcher.removeWatchedService(ibusService); - serviceWatcher.setConnection(*connection); + serviceWatcher.setConnection(connection); serviceWatcher.addWatchedService(ibusService); if (!ic.isValid()) { @@ -652,7 +701,7 @@ void QIBusPlatformInputContextPrivate::createBusProxy() return; } - context = new QIBusInputContextProxy(QLatin1StringView(ibusService), ic.value().path(), *connection); + context = std::make_unique<QIBusInputContextProxy>(QLatin1StringView(ibusService), ic.value().path(), connection); if (!context->isValid()) { qWarning("QIBusPlatformInputContext: invalid input context."); @@ -669,6 +718,8 @@ void QIBusPlatformInputContextPrivate::createBusProxy() }; context->SetCapabilities(IBUS_CAP_PREEDIT_TEXT|IBUS_CAP_FOCUS|IBUS_CAP_SURROUNDING_TEXT); + context->setClientCommitPreedit(QIBusPropTypeClientCommitPreedit(true)); + if (debug) qDebug(">>>> bus connected!"); busConnected = true; @@ -714,14 +765,16 @@ QString QIBusPlatformInputContextPrivate::getSocketPath() u'-' + QString::fromLocal8Bit(host) + u'-' + QString::fromLocal8Bit(displayNumber); } -QDBusConnection *QIBusPlatformInputContextPrivate::createConnection() +void QIBusPlatformInputContextPrivate::createConnection() { - if (usePortal) - return new QDBusConnection(QDBusConnection::connectToBus(QDBusConnection::SessionBus, "QIBusProxy"_L1)); - QFile file(getSocketPath()); + if (usePortal) { + QDBusConnection::connectToBus(QDBusConnection::SessionBus, "QIBusProxy"_L1); + return; + } + QFile file(getSocketPath()); if (!file.open(QFile::ReadOnly)) - return 0; + return; QByteArray address; int pid = -1; @@ -740,9 +793,9 @@ QDBusConnection *QIBusPlatformInputContextPrivate::createConnection() if (debug) qDebug() << "IBUS_ADDRESS=" << address << "PID=" << pid; if (address.isEmpty() || pid < 0 || kill(pid, 0) != 0) - return 0; + return; - return new QDBusConnection(QDBusConnection::connectToBus(QString::fromLatin1(address), "QIBusProxy"_L1)); + QDBusConnection::connectToBus(QString::fromLatin1(address), "QIBusProxy"_L1); } QT_END_NAMESPACE diff --git a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.h b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.h index 33ddb7af5d..ef8c0b7c8f 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.h +++ b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.h @@ -14,6 +14,8 @@ #include <QTimer> #include <QWindow> +#include "qibustypes.h" + QT_BEGIN_NAMESPACE class QIBusPlatformInputContextPrivate; @@ -66,6 +68,7 @@ public: public Q_SLOTS: void commitText(const QDBusVariant &text); void updatePreeditText(const QDBusVariant &text, uint cursor_pos, bool visible); + void updatePreeditTextWithMode(const QDBusVariant &text, uint cursor_pos, bool visible, uint mode); void forwardKeyEvent(uint keyval, uint keycode, uint state); void cursorRectChanged(); void deleteSurroundingText(int offset, uint n_chars); diff --git a/src/plugins/platforminputcontexts/ibus/qibusproxy.h b/src/plugins/platforminputcontexts/ibus/qibusproxy.h index c66e900664..73aff1a3d9 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusproxy.h +++ b/src/plugins/platforminputcontexts/ibus/qibusproxy.h @@ -110,7 +110,7 @@ public Q_SLOTS: // METHODS #endif QIBusEngineDesc getGlobalEngine(); -private: +private Q_SLOTS: void globalEngineChanged(const QString &engine_name); Q_SIGNALS: // SIGNALS diff --git a/src/plugins/platforminputcontexts/ibus/qibusproxyportal.cpp b/src/plugins/platforminputcontexts/ibus/qibusproxyportal.cpp index 54d8f731fb..dc5b37aa6e 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusproxyportal.cpp +++ b/src/plugins/platforminputcontexts/ibus/qibusproxyportal.cpp @@ -2,7 +2,7 @@ * This file was generated by qdbusxml2cpp version 0.8 * Command line was: qdbusxml2cpp -N -p qibusproxyportal -c QIBusProxyPortal interfaces/org.freedesktop.IBus.Portal.xml * - * qdbusxml2cpp is Copyright (C) 2017 The Qt Company Ltd. + * qdbusxml2cpp is Copyright (C) 2023 The Qt Company Ltd and other contributors. * * This is an auto-generated file. * This file may have been hand-edited. Look for HAND-EDIT comments @@ -24,4 +24,3 @@ QIBusProxyPortal::~QIBusProxyPortal() { } -#include "moc_qibusproxyportal.cpp" diff --git a/src/plugins/platforminputcontexts/ibus/qibusproxyportal.h b/src/plugins/platforminputcontexts/ibus/qibusproxyportal.h index 4b921db814..450205f12a 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusproxyportal.h +++ b/src/plugins/platforminputcontexts/ibus/qibusproxyportal.h @@ -2,7 +2,7 @@ * This file was generated by qdbusxml2cpp version 0.8 * Command line was: qdbusxml2cpp -N -p qibusproxyportal -c QIBusProxyPortal interfaces/org.freedesktop.IBus.Portal.xml * - * qdbusxml2cpp is Copyright (C) 2017 The Qt Company Ltd. + * qdbusxml2cpp is Copyright (C) 2023 The Qt Company Ltd and other contributors. * * This is an auto-generated file. * Do not edit! All changes made to it will be lost. @@ -11,15 +11,14 @@ #ifndef QIBUSPROXYPORTAL_H #define QIBUSPROXYPORTAL_H -#include <QObject> -#include <QByteArray> -#include <QList> -#include <QMap> -#include <QString> -#include <QStringList> -#include <QVariant> -#include <QDBusAbstractInterface> -#include <QDBusPendingReply> +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> /* * Proxy class for interface org.freedesktop.IBus.Portal diff --git a/src/plugins/platforminputcontexts/ibus/qibustypes.cpp b/src/plugins/platforminputcontexts/ibus/qibustypes.cpp index 9d61d61eb3..ab1a244b6d 100644 --- a/src/plugins/platforminputcontexts/ibus/qibustypes.cpp +++ b/src/plugins/platforminputcontexts/ibus/qibustypes.cpp @@ -322,4 +322,44 @@ newest: argument.endStructure(); } +QIBusPropTypeClientCommitPreedit::QIBusPropTypeClientCommitPreedit(bool inClientCommitPreedit) + : clientCommitPreedit(inClientCommitPreedit) +{ +} + +void QIBusPropTypeClientCommitPreedit::serializeTo(QDBusArgument &argument) const +{ + argument.beginStructure(); + argument << clientCommitPreedit; + argument.endStructure(); +} + +void QIBusPropTypeClientCommitPreedit::deserializeFrom(const QDBusArgument &argument) +{ + argument.beginStructure(); + argument >> clientCommitPreedit; + argument.endStructure(); +} + +QIBusPropTypeContentType::QIBusPropTypeContentType(unsigned int inPurpose, unsigned int inHints) + : purpose(inPurpose) + , hints(inHints) +{ +} + +void QIBusPropTypeContentType::serializeTo(QDBusArgument &argument) const +{ + argument.beginStructure(); + argument << purpose << hints; + argument.endStructure(); +} + +void QIBusPropTypeContentType::deserializeFrom(const QDBusArgument &argument) +{ + argument.beginStructure(); + argument >> purpose; + argument >> hints; + argument.endStructure(); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforminputcontexts/ibus/qibustypes.h b/src/plugins/platforminputcontexts/ibus/qibustypes.h index 60f24bcf54..b697e432a0 100644 --- a/src/plugins/platforminputcontexts/ibus/qibustypes.h +++ b/src/plugins/platforminputcontexts/ibus/qibustypes.h @@ -133,6 +133,44 @@ inline QDBusArgument &operator<<(QDBusArgument &argument, const QIBusEngineDesc inline const QDBusArgument &operator>>(const QDBusArgument &argument, QIBusEngineDesc &desc) { desc.deserializeFrom(argument); return argument; } +class QIBusPropTypeClientCommitPreedit +{ +public: + QIBusPropTypeClientCommitPreedit() {}; + + QIBusPropTypeClientCommitPreedit(bool inClientCommitPreedit); + + void serializeTo(QDBusArgument &argument) const; + void deserializeFrom(const QDBusArgument &argument); + + bool clientCommitPreedit; +}; +inline QDBusArgument &operator<<(QDBusArgument &argument, const QIBusPropTypeClientCommitPreedit &data) +{ data.serializeTo(argument); return argument; } +inline const QDBusArgument &operator>>(const QDBusArgument &argument, QIBusPropTypeClientCommitPreedit &data) +{ data.deserializeFrom(argument); return argument; } + +class QIBusPropTypeContentType +{ +public: + QIBusPropTypeContentType() {}; + + QIBusPropTypeContentType(unsigned int inPurpose, unsigned int inHint); + + void serializeTo(QDBusArgument &argument) const; + void deserializeFrom(const QDBusArgument &argument); + + unsigned int purpose; + unsigned int hints; +}; +inline QDBusArgument &operator<<(QDBusArgument &argument, const QIBusPropTypeContentType &data) +{ data.serializeTo(argument); return argument; } +inline const QDBusArgument &operator>>(const QDBusArgument &argument, QIBusPropTypeContentType &data) +{ data.deserializeFrom(argument); return argument; } + +Q_DECLARE_TYPEINFO(QIBusPropTypeClientCommitPreedit, Q_RELOCATABLE_TYPE); +Q_DECLARE_TYPEINFO(QIBusPropTypeContentType, Q_RELOCATABLE_TYPE); + QT_END_NAMESPACE Q_DECLARE_METATYPE(QIBusAttribute) @@ -140,4 +178,6 @@ Q_DECLARE_METATYPE(QIBusAttributeList) Q_DECLARE_METATYPE(QIBusText) Q_DECLARE_METATYPE(QIBusEngineDesc) +Q_DECLARE_METATYPE(QIBusPropTypeClientCommitPreedit) +Q_DECLARE_METATYPE(QIBusPropTypeContentType) #endif diff --git a/src/plugins/platforms/CMakeLists.txt b/src/plugins/platforms/CMakeLists.txt index ba64b6b0f5..69071a22c2 100644 --- a/src/plugins/platforms/CMakeLists.txt +++ b/src/plugins/platforms/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from platforms.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(ANDROID) add_subdirectory(android) @@ -41,16 +42,16 @@ if(QT_FEATURE_vnc AND TARGET Qt::Network) add_subdirectory(vnc) endif() if(FREEBSD) - # add_subdirectory(bsdfb) # special case TODO + # add_subdirectory(bsdfb) # TODO: QTBUG-112768 endif() if(HAIKU) - # add_subdirectory(haiku) # special case TODO + # add_subdirectory(haiku) # TODO: QTBUG-112768 endif() if(WASM) add_subdirectory(wasm) endif() if(QT_FEATURE_integrityfb) - # add_subdirectory(integrity) # special case TODO + # add_subdirectory(integrity) # TODO: QTBUG-112768 endif() if(QT_FEATURE_vkkhrdisplay) add_subdirectory(vkkhrdisplay) diff --git a/src/plugins/platforms/android/CMakeLists.txt b/src/plugins/platforms/android/CMakeLists.txt index 7db5556d7e..aaf62dd7e7 100644 --- a/src/plugins/platforms/android/CMakeLists.txt +++ b/src/plugins/platforms/android/CMakeLists.txt @@ -1,34 +1,33 @@ -# Generated from android.pro. +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QAndroidIntegrationPlugin Plugin: ##################################################################### -qt_find_package(EGL) # special case +qt_find_package(EGL) qt_internal_add_plugin(QAndroidIntegrationPlugin OUTPUT_NAME qtforandroid PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES android # special case + DEFAULT_IF "android" IN_LIST QT_QPA_PLATFORMS SOURCES androidcontentfileengine.cpp androidcontentfileengine.h androiddeadlockprotector.h androidjniaccessibility.cpp androidjniaccessibility.h - androidjniclipboard.cpp androidjniclipboard.h androidjniinput.cpp androidjniinput.h androidjnimain.cpp androidjnimain.h androidjnimenu.cpp androidjnimenu.h - androidsurfaceclient.h main.cpp qandroidassetsfileenginehandler.cpp qandroidassetsfileenginehandler.h qandroideventdispatcher.cpp qandroideventdispatcher.h qandroidinputcontext.cpp qandroidinputcontext.h qandroidplatformaccessibility.cpp qandroidplatformaccessibility.h - qandroidplatformbackingstore.cpp qandroidplatformbackingstore.h qandroidplatformclipboard.cpp qandroidplatformclipboard.h qandroidplatformdialoghelpers.cpp qandroidplatformdialoghelpers.h qandroidplatformfiledialoghelper.cpp qandroidplatformfiledialoghelper.h qandroidplatformfontdatabase.cpp qandroidplatformfontdatabase.h qandroidplatformforeignwindow.cpp qandroidplatformforeignwindow.h + qandroidplatformiconengine.cpp qandroidplatformiconengine.h qandroidplatformintegration.cpp qandroidplatformintegration.h qandroidplatformmenu.cpp qandroidplatformmenu.h qandroidplatformmenubar.cpp qandroidplatformmenubar.h @@ -41,8 +40,20 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin qandroidplatformtheme.cpp qandroidplatformtheme.h qandroidplatformwindow.cpp qandroidplatformwindow.h qandroidsystemlocale.cpp qandroidsystemlocale.h - DEFINES - QT_USE_QSTRINGBUILDER + androidwindowembedding.cpp androidwindowembedding.h + androidbackendregister.cpp androidbackendregister.h + NO_UNITY_BUILD_SOURCES + # Conflicting symbols and macros with androidjnimain.cpp + # TODO: Unify the usage of FIND_AND_CHECK_CLASS, and similar + # macros. Q_JNI_FIND_AND_CHECK_CLASS in `qjnihelpers_p.h` + # seems to be doing most of the work already. + androidjnimenu.cpp + qandroidinputcontext.cpp + androidjniaccessibility.cpp + qandroidplatformdialoghelpers.cpp + # Conflicting JNI classes, and types + androidcontentfileengine.cpp + qandroidplatformintegration.cpp INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR} ${QtBase_SOURCE_DIR}/src/3rdparty/android @@ -54,12 +65,9 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin Qt::GuiPrivate android jnigraphics - EGL::EGL # special case + EGL::EGL ) -#### Keys ignored in scope 1:.:.:android.pro:<TRUE>: -# OTHER_FILES = "$$PWD/android.json" - ## Scopes: ##################################################################### @@ -77,4 +85,7 @@ qt_internal_extend_target(QAndroidIntegrationPlugin CONDITION QT_FEATURE_vulkan SOURCES qandroidplatformvulkaninstance.cpp qandroidplatformvulkaninstance.h qandroidplatformvulkanwindow.cpp qandroidplatformvulkanwindow.h + NO_UNITY_BUILD_SOURCES + # To avoid undefined symbols due to missing VK_USE_PLATFORM_ANDROID_KHR + qandroidplatformvulkaninstance.cpp qandroidplatformvulkanwindow.cpp ) diff --git a/src/plugins/platforms/android/androidbackendregister.cpp b/src/plugins/platforms/android/androidbackendregister.cpp new file mode 100644 index 0000000000..bfd86138aa --- /dev/null +++ b/src/plugins/platforms/android/androidbackendregister.cpp @@ -0,0 +1,52 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "androidbackendregister.h" + +#include "androidjnimain.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcAndroidBackendRegister, "qt.qpa.androidbackendregister") + +Q_DECLARE_JNI_CLASS(BackendRegister, "org/qtproject/qt/android/BackendRegister"); + +bool AndroidBackendRegister::registerNatives() +{ + return QtJniTypes::BackendRegister::registerNativeMethods( + { Q_JNI_NATIVE_SCOPED_METHOD(registerBackend, AndroidBackendRegister), + Q_JNI_NATIVE_SCOPED_METHOD(unregisterBackend, AndroidBackendRegister) }); +} + +void AndroidBackendRegister::registerBackend(JNIEnv *, jclass, jclass interfaceClass, + jobject interface) +{ + if (AndroidBackendRegister *reg = QtAndroid::backendRegister()) { + const QJniObject classObject(static_cast<jobject>(interfaceClass)); + QString name = classObject.callMethod<jstring>("getName").toString(); + name.replace('.', '/'); + + QMutexLocker lock(®->m_registerMutex); + reg->m_register[name] = QJniObject(interface); + } else { + qCWarning(lcAndroidBackendRegister) + << "AndroidBackendRegister pointer is null, cannot register functionality"; + } +} + +void AndroidBackendRegister::unregisterBackend(JNIEnv *, jclass, jclass interfaceClass) +{ + if (AndroidBackendRegister *reg = QtAndroid::backendRegister()) { + const QJniObject classObject(static_cast<jobject>(interfaceClass)); + QString name = classObject.callMethod<jstring>("getName").toString(); + name.replace('.', '/'); + + QMutexLocker lock(®->m_registerMutex); + reg->m_register.remove(name); + } else { + qCWarning(lcAndroidBackendRegister) + << "AndroidBackendRegister pointer is null, cannot unregister functionality"; + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidbackendregister.h b/src/plugins/platforms/android/androidbackendregister.h new file mode 100644 index 0000000000..c186f7e107 --- /dev/null +++ b/src/plugins/platforms/android/androidbackendregister.h @@ -0,0 +1,67 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef ANDROIDBACKENDREGISTER_H +#define ANDROIDBACKENDREGISTER_H + +#include <type_traits> + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> +#include <QtCore/qjniobject.h> + +#include <QtCore/qstring.h> +#include <QtCore/qmap.h> +#include <QtCore/qmutex.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcAndroidBackendRegister) + +template <typename T> +using ValidInterfaceType = std::enable_if_t<std::is_base_of_v<QtJniTypes::JObjectBase, T>, bool>; + +class AndroidBackendRegister +{ +public: + static bool registerNatives(); + + template <typename T, ValidInterfaceType<T> = true> + [[nodiscard]] T getInterface() + { + QMutexLocker lock(&m_registerMutex); + return m_register.value(QString(QtJniTypes::Traits<T>::className().data())); + } + + template <typename Object> + using IsObjectType = + typename std::disjunction<std::is_base_of<QJniObject, Object>, + std::is_base_of<QtJniTypes::JObjectBase, Object>>; + + template <typename Interface, typename Ret, typename... Args, + ValidInterfaceType<Interface> = true> + auto callInterface(const char *func, Args... args) + { + if (const auto obj = getInterface<Interface>(); obj.isValid()) + return obj.template callMethod<Ret, Args...>(func, std::forward<Args>(args)...); + + if constexpr (IsObjectType<Ret>::value) + return Ret(QJniObject()); + if constexpr (!std::is_same_v<Ret, void>) + return Ret{}; + } + +private: + QMutex m_registerMutex; + QMap<QString, QJniObject> m_register; + + static void registerBackend(JNIEnv *, jclass, jclass interfaceClass, jobject interface); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(registerBackend) + static void unregisterBackend(JNIEnv *, jclass, jclass interfaceClass); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(unregisterBackend) +}; + +QT_END_NAMESPACE + +#endif // ANDROIDBACKENDREGISTER_H diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 04862ccba6..db6c601f33 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -1,5 +1,5 @@ -// Copyright (C) 2019 Volker Krause <vkrause@kde.org> -// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2019 Volker Krause <vkrause@kde.org> +// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "androidcontentfileengine.h" @@ -7,16 +7,38 @@ #include <QtCore/qcoreapplication.h> #include <QtCore/qjnienvironment.h> #include <QtCore/qjniobject.h> +#include <QtCore/qurl.h> +#include <QtCore/qdatetime.h> +#include <QtCore/qmimedatabase.h> -#include <QDebug> +QT_BEGIN_NAMESPACE using namespace QNativeInterface; using namespace Qt::StringLiterals; -AndroidContentFileEngine::AndroidContentFileEngine(const QString &f) - : m_file(f) +Q_DECLARE_JNI_CLASS(ContentResolverType, "android/content/ContentResolver"); +Q_DECLARE_JNI_CLASS(UriType, "android/net/Uri"); +Q_DECLARE_JNI_CLASS(Uri, "android/net/Uri"); +Q_DECLARE_JNI_CLASS(ParcelFileDescriptorType, "android/os/ParcelFileDescriptor"); +Q_DECLARE_JNI_CLASS(CursorType, "android/database/Cursor"); +Q_DECLARE_JNI_TYPE(StringArray, "[Ljava/lang/String;"); + +static QJniObject &contentResolverInstance() +{ + static QJniObject contentResolver; + if (!contentResolver.isValid()) { + contentResolver = QJniObject(QNativeInterface::QAndroidApplication::context()) + .callMethod<QtJniTypes::ContentResolverType>("getContentResolver"); + } + + return contentResolver; +} + +AndroidContentFileEngine::AndroidContentFileEngine(const QString &filename) + : m_initialFile(filename), + m_documentFile(DocumentFile::parseFromAnyUri(filename)) { - setFileName(f); + setFileName(filename); } bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, @@ -29,6 +51,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, } if (openMode & QFileDevice::WriteOnly) { openModeStr += u'w'; + if (!m_documentFile->exists()) { + if (QUrl(m_initialFile).path().startsWith("/tree/"_L1)) { + const int lastSeparatorIndex = m_initialFile.lastIndexOf('/'); + const QString fileName = m_initialFile.mid(lastSeparatorIndex + 1); + + QString mimeType; + const auto mimeTypes = QMimeDatabase().mimeTypesForFileName(fileName); + if (!mimeTypes.empty()) + mimeType = mimeTypes.first().name(); + else + mimeType = "application/octet-stream"; + + if (m_documentFile->parent()) { + auto createdFile = m_documentFile->parent()->createFile(mimeType, fileName); + if (createdFile) + m_documentFile = createdFile; + } + } else { + qWarning() << "open(): non-existent content URI with a document type provided"; + } + } } if (openMode & QFileDevice::Truncate) { openModeStr += u't'; @@ -36,21 +79,19 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, openModeStr += u'a'; } - m_pfd = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative", - "openParcelFdForContentUrl", - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", - QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object(), - QJniObject::fromString(openModeStr).object()); + m_pfd = contentResolverInstance().callMethod< + QtJniTypes::ParcelFileDescriptorType, QtJniTypes::UriType, jstring>( + "openFileDescriptor", + m_documentFile->uri().object(), + QJniObject::fromString(openModeStr).object<jstring>()); if (!m_pfd.isValid()) return false; - const auto fd = m_pfd.callMethod<jint>("getFd", "()I"); + const auto fd = m_pfd.callMethod<jint>("getFd"); if (fd < 0) { - m_pfd.callMethod<void>("close", "()V"); - m_pfd = QJniObject(); + closeNativeFileDescriptor(); return false; } @@ -59,47 +100,130 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, bool AndroidContentFileEngine::close() { + closeNativeFileDescriptor(); + return QFSFileEngine::close(); +} + +void AndroidContentFileEngine::closeNativeFileDescriptor() +{ if (m_pfd.isValid()) { - m_pfd.callMethod<void>("close", "()V"); + m_pfd.callMethod<void>("close"); m_pfd = QJniObject(); } - - return QFSFileEngine::close(); } qint64 AndroidContentFileEngine::size() const { - const jlong size = QJniObject::callStaticMethod<jlong>( - "org/qtproject/qt/android/QtNative", "getSize", - "(Landroid/content/Context;Ljava/lang/String;)J", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - return (qint64)size; + return m_documentFile->length(); +} + +bool AndroidContentFileEngine::remove() +{ + return m_documentFile->remove(); +} + +bool AndroidContentFileEngine::rename(const QString &newName) +{ + if (m_documentFile->rename(newName)) { + m_initialFile = m_documentFile->uri().toString(); + return true; + } + return false; +} + +bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories, + std::optional<QFileDevice::Permissions> permissions) const +{ + Q_UNUSED(permissions) + + QString tmp = dirName; + tmp.remove(m_initialFile); + + QStringList dirParts = tmp.split(u'/'); + dirParts.removeAll(""); + + if (dirParts.isEmpty()) + return false; + + auto createdDir = m_documentFile; + bool allDirsCreated = true; + for (const auto &dir : dirParts) { + // Find if the sub-dir already exists and then don't re-create it + bool subDirExists = false; + for (const DocumentFilePtr &subDir : m_documentFile->listFiles()) { + if (dir == subDir->name() && subDir->isDirectory()) { + createdDir = subDir; + subDirExists = true; + } + } + + if (!subDirExists) { + createdDir = createdDir->createDirectory(dir); + if (!createdDir) { + allDirsCreated = false; + break; + } + } + + if (!createParentDirectories) + break; + } + + return allDirsCreated; +} + +bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const +{ + if (recurseParentDirectories) + qWarning() << "rmpath(): Unsupported for Content URIs"; + + const QString dirFileName = QUrl(dirName).fileName(); + bool deleted = false; + for (const DocumentFilePtr &dir : m_documentFile->listFiles()) { + if (dirFileName == dir->name() && dir->isDirectory()) { + deleted = dir->remove(); + break; + } + } + + return deleted; +} + +QByteArray AndroidContentFileEngine::id() const +{ + return m_documentFile->id().toUtf8(); +} + +QDateTime AndroidContentFileEngine::fileTime(QFile::FileTime time) const +{ + switch (time) { + case QFile::FileModificationTime: + return m_documentFile->lastModified(); + break; + default: + break; + } + + return QDateTime(); } AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlags type) const { - FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag); FileFlags flags; - const bool isDir = QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - // If it is a directory then we know it exists so there is no reason to explicitly check - const bool exists = isDir ? true : QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkFileExists", - "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - if (!exists && !isDir) + if (!m_documentFile->exists()) + return flags; + + flags = ExistsFlag; + if (!m_documentFile->canRead()) return flags; - if (isDir) { - flags = DirectoryType | commonFlags; + + flags |= ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm; + + if (m_documentFile->isDirectory()) { + flags |= DirectoryType; } else { - flags = FileType | commonFlags; - const bool writable = QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkIfWritable", - "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - if (writable) + flags |= FileType; + if (m_documentFile->canWrite()) flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm; } return type & flags; @@ -114,41 +238,39 @@ QString AndroidContentFileEngine::fileName(FileName f) const case DefaultName: case AbsoluteName: case CanonicalName: - return m_file; + return m_documentFile->uri().toString(); case BaseName: - { - const qsizetype pos = m_file.lastIndexOf(u'/'); - return m_file.mid(pos); - } + return m_documentFile->name(); default: - return QString(); + break; } -} -QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, const QStringList &filterNames) -{ - return new AndroidContentFileEngineIterator(filters, filterNames); + return QString(); } -QAbstractFileEngine::Iterator *AndroidContentFileEngine::endEntryList() +QAbstractFileEngine::IteratorUniquePtr +AndroidContentFileEngine::beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) { - return nullptr; + return std::make_unique<AndroidContentFileEngineIterator>(path, filters, filterNames); } AndroidContentFileEngineHandler::AndroidContentFileEngineHandler() = default; AndroidContentFileEngineHandler::~AndroidContentFileEngineHandler() = default; -QAbstractFileEngine* AndroidContentFileEngineHandler::create(const QString &fileName) const +std::unique_ptr<QAbstractFileEngine> +AndroidContentFileEngineHandler::create(const QString &fileName) const { - if (!fileName.startsWith("content"_L1)) - return nullptr; + if (fileName.startsWith("content"_L1)) + return std::make_unique<AndroidContentFileEngine>(fileName); + + return {}; - return new AndroidContentFileEngine(fileName); } -AndroidContentFileEngineIterator::AndroidContentFileEngineIterator(QDir::Filters filters, - const QStringList &filterNames) - : QAbstractFileEngineIterator(filters, filterNames) +AndroidContentFileEngineIterator::AndroidContentFileEngineIterator( + const QString &path, QDir::Filters filters, const QStringList &filterNames) + : QAbstractFileEngineIterator(path, filters, filterNames) { } @@ -156,47 +278,557 @@ AndroidContentFileEngineIterator::~AndroidContentFileEngineIterator() { } -QString AndroidContentFileEngineIterator::next() +bool AndroidContentFileEngineIterator::advance() { - if (!hasNext()) - return QString(); - ++m_index; - return currentFilePath(); -} + if (m_index == -1 && m_files.isEmpty()) { + const auto currentPath = path(); + if (currentPath.isEmpty()) + return false; -bool AndroidContentFileEngineIterator::hasNext() const -{ - if (m_index == -1) { - if (path().isEmpty()) + const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath); + if (iterDoc->isDirectory()) + for (const auto &doc : iterDoc->listFiles()) + m_files.append(doc); + if (m_files.isEmpty()) return false; - const bool isDir = QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", - QAndroidApplication::context(), - QJniObject::fromString(path()).object()); - if (isDir) { - QJniObject objArray = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative", - "listContentsFromTreeUri", - "(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;", - QAndroidApplication::context(), - QJniObject::fromString(path()).object()); - if (objArray.isValid()) { - QJniEnvironment env; - const jsize length = env->GetArrayLength(objArray.object<jarray>()); - for (int i = 0; i != length; ++i) { - m_entries << QJniObject(env->GetObjectArrayElement( - objArray.object<jobjectArray>(), i)).toString(); - } - } - } m_index = 0; + return true; + } + + if (m_index < m_files.size() - 1) { + ++m_index; + return true; } - return m_index < m_entries.size(); + + return false; } QString AndroidContentFileEngineIterator::currentFileName() const { - if (m_index <= 0 || m_index > m_entries.size()) + if (m_index < 0 || m_index > m_files.size()) + return QString(); + return m_files.at(m_index)->name(); +} + +QString AndroidContentFileEngineIterator::currentFilePath() const +{ + if (m_index < 0 || m_index > m_files.size()) return QString(); - return m_entries.at(m_index - 1); + return m_files.at(m_index)->uri().toString(); +} + +// Start of Cursor + +class Cursor +{ +public: + explicit Cursor(const QJniObject &object) + : m_object{object} { } + + ~Cursor() + { + if (m_object.isValid()) + m_object.callMethod<void>("close"); + } + + enum Type { + FIELD_TYPE_NULL = 0x00000000, + FIELD_TYPE_INTEGER = 0x00000001, + FIELD_TYPE_FLOAT = 0x00000002, + FIELD_TYPE_STRING = 0x00000003, + FIELD_TYPE_BLOB = 0x00000004 + }; + + QVariant data(int columnIndex) const + { + int type = m_object.callMethod<jint>("getType", columnIndex); + switch (type) { + case FIELD_TYPE_NULL: + return {}; + case FIELD_TYPE_INTEGER: + return QVariant::fromValue(m_object.callMethod<jlong>("getLong", columnIndex)); + case FIELD_TYPE_FLOAT: + return QVariant::fromValue(m_object.callMethod<jdouble>("getDouble", columnIndex)); + case FIELD_TYPE_STRING: + return QVariant::fromValue(m_object.callMethod<jstring>("getString", + columnIndex).toString()); + case FIELD_TYPE_BLOB: { + auto blob = m_object.callMethod<jbyteArray>("getBlob", columnIndex); + QJniEnvironment env; + const auto blobArray = blob.object<jbyteArray>(); + const int size = env->GetArrayLength(blobArray); + const auto byteArray = env->GetByteArrayElements(blobArray, nullptr); + QByteArray data{reinterpret_cast<const char *>(byteArray), size}; + env->ReleaseByteArrayElements(blobArray, byteArray, 0); + return QVariant::fromValue(data); + } + } + return {}; + } + + static std::unique_ptr<Cursor> queryUri(const QJniObject &uri, + const QStringList &projection = {}, + const QString &selection = {}, + const QStringList &selectionArgs = {}, + const QString &sortOrder = {}) + { + auto cursor = contentResolverInstance().callMethod<QtJniTypes::CursorType>( + "query", + uri.object<QtJniTypes::UriType>(), + projection.isEmpty() ? + nullptr : fromStringList(projection).object<QtJniTypes::StringArray>(), + selection.isEmpty() ? nullptr : QJniObject::fromString(selection).object<jstring>(), + selectionArgs.isEmpty() ? + nullptr : fromStringList(selectionArgs).object<QtJniTypes::StringArray>(), + sortOrder.isEmpty() ? nullptr : QJniObject::fromString(sortOrder).object<jstring>()); + if (!cursor.isValid()) + return {}; + return std::make_unique<Cursor>(cursor); + } + + static QVariant queryColumn(const QJniObject &uri, const QString &column) + { + const auto query = queryUri(uri, {column}); + if (!query) + return {}; + + if (query->rowCount() != 1 || query->columnCount() != 1) + return {}; + query->moveToFirst(); + return query->data(0); + } + + bool isNull(int columnIndex) const + { + return m_object.callMethod<jboolean>("isNull", columnIndex); + } + + int columnCount() const { return m_object.callMethod<jint>("getColumnCount"); } + int rowCount() const { return m_object.callMethod<jint>("getCount"); } + int row() const { return m_object.callMethod<jint>("getPosition"); } + bool isFirst() const { return m_object.callMethod<jboolean>("isFirst"); } + bool isLast() const { return m_object.callMethod<jboolean>("isLast"); } + bool moveToFirst() { return m_object.callMethod<jboolean>("moveToFirst"); } + bool moveToLast() { return m_object.callMethod<jboolean>("moveToLast"); } + bool moveToNext() { return m_object.callMethod<jboolean>("moveToNext"); } + +private: + static QJniObject fromStringList(const QStringList &list) + { + QJniEnvironment env; + auto array = env->NewObjectArray(list.size(), env.findClass("java/lang/String"), nullptr); + for (int i = 0; i < list.size(); ++i) + env->SetObjectArrayElement(array, i, QJniObject::fromString(list[i]).object()); + return QJniObject::fromLocalRef(array); + } + + QJniObject m_object; +}; + +// End of Cursor + +// Start of DocumentsContract + +Q_DECLARE_JNI_CLASS(DocumentsContract, "android/provider/DocumentsContract"); + +/*! + * + * DocumentsContract Api. + * Check https://developer.android.com/reference/android/provider/DocumentsContract + * for more information. + * + * \note This does not implement all facilities of the native API. + * + */ +namespace DocumentsContract +{ + +namespace Document { +const QLatin1String COLUMN_DISPLAY_NAME("_display_name"); +const QLatin1String COLUMN_DOCUMENT_ID("document_id"); +const QLatin1String COLUMN_FLAGS("flags"); +const QLatin1String COLUMN_LAST_MODIFIED("last_modified"); +const QLatin1String COLUMN_MIME_TYPE("mime_type"); +const QLatin1String COLUMN_SIZE("_size"); + +constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008; +constexpr int FLAG_SUPPORTS_DELETE = 0x00000004; +constexpr int FLAG_SUPPORTS_MOVE = 0x00000100; +constexpr int FLAG_SUPPORTS_RENAME = 0x00000040; +constexpr int FLAG_SUPPORTS_WRITE = 0x00000002; +constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200; + +const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory"); +} // namespace Document + +QString documentId(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "getDocumentId", + uri.object()).toString(); +} + +QString treeDocumentId(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "getTreeDocumentId", + uri.object()).toString(); +} + +QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId) +{ + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "buildChildDocumentsUriUsingTree", + uri.object<QtJniTypes::UriType>(), + QJniObject::fromString(parentDocumentId).object<jstring>()); + } + +QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId) +{ + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "buildDocumentUriUsingTree", + treeUri.object<QtJniTypes::UriType>(), + QJniObject::fromString(documentId).object<jstring>()); +} + +bool isDocumentUri(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jboolean>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "isDocumentUri", + QNativeInterface::QAndroidApplication::context(), + uri.object<QtJniTypes::UriType>()); +} + +bool isTreeUri(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jboolean>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "isTreeUri", + uri.object<QtJniTypes::UriType>()); +} + +QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType, + const QString &displayName) +{ + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "createDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + parentDocumentUri.object<QtJniTypes::UriType>(), + QJniObject::fromString(mimeType).object<jstring>(), + QJniObject::fromString(displayName).object<jstring>()); +} + +bool deleteDocument(const QJniObject &documentUri) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_DELETE)) + return {}; + + return QJniObject::callStaticMethod<jboolean>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "deleteDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + documentUri.object<QtJniTypes::UriType>()); +} + +QJniObject moveDocument(const QJniObject &sourceDocumentUri, + const QJniObject &sourceParentDocumentUri, + const QJniObject &targetParentDocumentUri) +{ + const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_MOVE)) + return {}; + + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "moveDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + sourceDocumentUri.object<QtJniTypes::UriType>(), + sourceParentDocumentUri.object<QtJniTypes::UriType>(), + targetParentDocumentUri.object<QtJniTypes::UriType>()); +} + +QJniObject renameDocument(const QJniObject &documentUri, const QString &displayName) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_RENAME)) + return {}; + + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "renameDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + documentUri.object<QtJniTypes::UriType>(), + QJniObject::fromString(displayName).object<jstring>()); +} +} // End DocumentsContract namespace + +// Start of DocumentFile + +using namespace DocumentsContract; + +namespace { +class MakeableDocumentFile : public DocumentFile +{ +public: + MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {}) + : DocumentFile(uri, parent) + {} +}; +} + +DocumentFile::DocumentFile(const QJniObject &uri, + const DocumentFilePtr &parent) + : m_uri{uri} + , m_parent{parent} +{} + +QJniObject parseUri(const QString &uri) +{ + QString uriToParse = uri; + if (uriToParse.contains(' ')) + uriToParse.replace(' ', QUrl::toPercentEncoding(" ")); + + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::Uri>::className(), + "parse", + QJniObject::fromString(uriToParse).object<jstring>()); +} + +DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName) +{ + const QString encodedUri = QUrl(fileName).toEncoded(); + const QJniObject uri = parseUri(encodedUri); + + if (DocumentsContract::isDocumentUri(uri) || !DocumentsContract::isTreeUri(uri)) + return fromSingleUri(uri); + + const QString documentType = "/document/"_L1; + const QString treeType = "/tree/"_L1; + + const int treeIndex = encodedUri.indexOf(treeType); + const int documentIndex = encodedUri.indexOf(documentType); + const int index = fileName.lastIndexOf("/"); + + if (index <= treeIndex + treeType.size() || index <= documentIndex + documentType.size()) + return fromTreeUri(uri); + + const QString parentUrl = encodedUri.left(index); + DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl)); + + const QString baseName = encodedUri.mid(index); + const QString fileUrl = parentUrl + QUrl::toPercentEncoding(baseName); + + DocumentFilePtr docFile = std::make_shared<MakeableDocumentFile>(parseUri(fileUrl)); + if (parentDocFile && parentDocFile->isDirectory()) + docFile->m_parent = parentDocFile; + + return docFile; +} + +DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri) +{ + return std::make_shared<MakeableDocumentFile>(uri); +} + +DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri) +{ + QString docId; + if (isDocumentUri(treeUri)) + docId = documentId(treeUri); + else + docId = treeDocumentId(treeUri); + + return std::make_shared<MakeableDocumentFile>(buildDocumentUriUsingTree(treeUri, docId)); +} + +DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName) +{ + if (isDirectory()) { + return std::make_shared<MakeableDocumentFile>( + createDocument(m_uri, mimeType, displayName), + shared_from_this()); + } + return {}; +} + +DocumentFilePtr DocumentFile::createDirectory(const QString &displayName) +{ + if (isDirectory()) { + return std::make_shared<MakeableDocumentFile>( + createDocument(m_uri, Document::MIME_TYPE_DIR, displayName), + shared_from_this()); + } + return {}; +} + +const QJniObject &DocumentFile::uri() const +{ + return m_uri; +} + +const DocumentFilePtr &DocumentFile::parent() const +{ + return m_parent; +} + +QString DocumentFile::name() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString(); +} + +QString DocumentFile::id() const +{ + return DocumentsContract::documentId(uri()); +} + +QString DocumentFile::mimeType() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_MIME_TYPE).toString(); +} + +bool DocumentFile::isDirectory() const +{ + return mimeType() == Document::MIME_TYPE_DIR; +} + +bool DocumentFile::isFile() const +{ + const QString type = mimeType(); + return type != Document::MIME_TYPE_DIR && !type.isEmpty(); +} + +bool DocumentFile::isVirtual() const +{ + return isDocumentUri(m_uri) && (Cursor::queryColumn(m_uri, + Document::COLUMN_FLAGS).toInt() & Document::FLAG_VIRTUAL_DOCUMENT); +} + +QDateTime DocumentFile::lastModified() const +{ + const auto timeVariant = Cursor::queryColumn(m_uri, Document::COLUMN_LAST_MODIFIED); + if (timeVariant.isValid()) + return QDateTime::fromMSecsSinceEpoch(timeVariant.toLongLong()); + return {}; +} + +int64_t DocumentFile::length() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_SIZE).toLongLong(); +} + +namespace { +constexpr int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001; +constexpr int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002; +} + +bool DocumentFile::canRead() const +{ + const auto context = QJniObject(QNativeInterface::QAndroidApplication::context()); + const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission", + m_uri.object<QtJniTypes::UriType>(), + FLAG_GRANT_READ_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + return !mimeType().isEmpty(); +} + +bool DocumentFile::canWrite() const +{ + const auto context = QJniObject(QNativeInterface::QAndroidApplication::context()); + const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission", + m_uri.object<QtJniTypes::UriType>(), + FLAG_GRANT_WRITE_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + const QString type = mimeType(); + if (type.isEmpty()) + return false; + + const int flags = Cursor::queryColumn(m_uri, Document::COLUMN_FLAGS).toInt(); + if (flags & Document::FLAG_SUPPORTS_DELETE) + return true; + + const bool supportsWrite = (flags & Document::FLAG_SUPPORTS_WRITE); + const bool isDir = (type == Document::MIME_TYPE_DIR); + const bool dirSupportsCreate = (isDir && (flags & Document::FLAG_DIR_SUPPORTS_CREATE)); + + return dirSupportsCreate || supportsWrite; +} + +bool DocumentFile::remove() +{ + return deleteDocument(m_uri); +} + +bool DocumentFile::exists() const +{ + return !name().isEmpty(); +} + +std::vector<DocumentFilePtr> DocumentFile::listFiles() +{ + std::vector<DocumentFilePtr> res; + const auto childrenUri = buildChildDocumentsUriUsingTree(m_uri, documentId(m_uri)); + const auto query = Cursor::queryUri(childrenUri, {Document::COLUMN_DOCUMENT_ID}); + if (!query) + return res; + + while (query->moveToNext()) { + const auto uri = buildDocumentUriUsingTree(m_uri, query->data(0).toString()); + res.push_back(std::make_shared<MakeableDocumentFile>(uri, shared_from_this())); + } + return res; +} + +bool DocumentFile::rename(const QString &newName) +{ + QJniObject uri; + if (newName.startsWith("content://"_L1)) { + auto lastSeparatorIndex = [](const QString &file) { + int posDecoded = file.lastIndexOf("/"); + int posEncoded = file.lastIndexOf(QUrl::toPercentEncoding("/")); + return posEncoded > posDecoded ? posEncoded : posDecoded; + }; + + // first try to see if the new file is under the same tree and thus used rename only + const QString parent = m_uri.toString().left(lastSeparatorIndex(m_uri.toString())); + if (newName.contains(parent)) { + QString displayName = newName.mid(lastSeparatorIndex(newName)); + if (displayName.startsWith('/')) + displayName.remove(0, 1); + else if (displayName.startsWith(QUrl::toPercentEncoding("/"))) + displayName.remove(0, 3); + + uri = renameDocument(m_uri, displayName); + } else { + // Move + QJniObject srcParentUri = fromTreeUri(parseUri(parent))->uri(); + const QString destParent = newName.left(lastSeparatorIndex(newName)); + QJniObject targetParentUri = fromTreeUri(parseUri(destParent))->uri(); + uri = moveDocument(m_uri, srcParentUri, targetParentUri); + } + } else { + uri = renameDocument(m_uri, newName); + } + + if (uri.isValid()) { + m_uri = uri; + return true; + } + + return false; +} + +QT_END_NAMESPACE + +// End of DocumentFile diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index e58c990c51..a5dd1b30f3 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -5,7 +5,13 @@ #define ANDROIDCONTENTFILEENGINE_H #include <private/qfsfileengine_p.h> + #include <QtCore/qjniobject.h> +#include <QtCore/qlist.h> + +QT_BEGIN_NAMESPACE + +using DocumentFilePtr = std::shared_ptr<class DocumentFile>; class AndroidContentFileEngine : public QFSFileEngine { @@ -14,35 +20,93 @@ public: bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override; bool close() override; qint64 size() const override; + bool remove() override; + bool rename(const QString &newName) override; + bool mkdir(const QString &dirName, bool createParentDirectories, + std::optional<QFile::Permissions> permissions = std::nullopt) const override; + bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; + QByteArray id() const override; + bool caseSensitive() const override { return true; } + QDateTime fileTime(QFile::FileTime time) const override; FileFlags fileFlags(FileFlags type = FileInfoAll) const override; QString fileName(FileName file = DefaultName) const override; - QAbstractFileEngine::Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override; - QAbstractFileEngine::Iterator *endEntryList() override; + IteratorUniquePtr beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) override; + private: - QString m_file; - QJniObject m_pfd; + void closeNativeFileDescriptor(); + QString m_initialFile; + QJniObject m_pfd; + DocumentFilePtr m_documentFile; }; class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler { + Q_DISABLE_COPY_MOVE(AndroidContentFileEngineHandler) public: AndroidContentFileEngineHandler(); ~AndroidContentFileEngineHandler(); - QAbstractFileEngine *create(const QString &fileName) const override; + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; }; class AndroidContentFileEngineIterator : public QAbstractFileEngineIterator { public: - AndroidContentFileEngineIterator(QDir::Filters filters, const QStringList &filterNames); + AndroidContentFileEngineIterator(const QString &path, QDir::Filters filters, + const QStringList &filterNames); ~AndroidContentFileEngineIterator(); - QString next() override; - bool hasNext() const override; + + bool advance() override; + QString currentFileName() const override; + QString currentFilePath() const override; private: - mutable QStringList m_entries; - mutable int m_index = -1; + mutable QList<DocumentFilePtr> m_files; + mutable qsizetype m_index = -1; }; +/*! + * + * DocumentFile Api. + * Check https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile + * for more information. + * + */ +class DocumentFile : public std::enable_shared_from_this<DocumentFile> +{ +public: + static DocumentFilePtr parseFromAnyUri(const QString &filename); + static DocumentFilePtr fromSingleUri(const QJniObject &uri); + static DocumentFilePtr fromTreeUri(const QJniObject &treeUri); + + DocumentFilePtr createFile(const QString &mimeType, const QString &displayName); + DocumentFilePtr createDirectory(const QString &displayName); + const QJniObject &uri() const; + const DocumentFilePtr &parent() const; + QString name() const; + QString id() const; + QString mimeType() const; + bool isDirectory() const; + bool isFile() const; + bool isVirtual() const; + QDateTime lastModified() const; + int64_t length() const; + bool canRead() const; + bool canWrite() const; + bool remove(); + bool exists() const; + std::vector<DocumentFilePtr> listFiles(); + bool rename(const QString &newName); + +protected: + DocumentFile(const QJniObject &uri, const std::shared_ptr<DocumentFile> &parent); + +protected: + QJniObject m_uri; + DocumentFilePtr m_parent; +}; + +QT_END_NAMESPACE + #endif // ANDROIDCONTENTFILEENGINE_H diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp index 0523211196..805616e481 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.cpp +++ b/src/plugins/platforms/android/androidjniaccessibility.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" #include "androidjnimain.h" #include "qandroidplatformintegration.h" @@ -14,11 +15,12 @@ #include <QtCore/private/qjnihelpers_p.h> #include <QtCore/QJniObject> #include <QtGui/private/qhighdpiscaling_p.h> + #include <QtCore/QObject> +#include <QtCore/qpointer.h> #include <QtCore/qvarlengtharray.h> static const char m_qtTag[] = "Qt A11Y"; -static const char m_classErrorMsg[] = "Can't find class \"%s\""; QT_BEGIN_NAMESPACE @@ -47,7 +49,7 @@ namespace QtAndroidAccessibility // Because of that almost every method here is split into two parts. // The _helper part is executed in the context of m_accessibilityContext // on the main thread. The other part is executed in Java thread. - static QPointer<QObject> m_accessibilityContext = nullptr; + Q_CONSTINIT static QPointer<QObject> m_accessibilityContext = {}; // This method is called from the Qt main thread, and normally a // QGuiApplication instance will be used as a parent. @@ -61,13 +63,26 @@ namespace QtAndroidAccessibility template <typename Func, typename Ret> void runInObjectContext(QObject *context, Func &&func, Ret *retVal) { - QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal); + AndroidDeadlockProtector protector; + if (!protector.acquire()) { + __android_log_print(ANDROID_LOG_WARN, m_qtTag, + "Could not run accessibility call in object context, accessing " + "main thread could lead to deadlock"); + return; + } + + if (!QtAndroid::blockEventLoopsWhenSuspended() + || QGuiApplication::applicationState() != Qt::ApplicationSuspended) { + QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal); + } else { + __android_log_print(ANDROID_LOG_WARN, m_qtTag, + "Could not run accessibility call in object context, event loop suspended."); + } } void initialize() { - QJniObject::callStaticMethod<void>(QtAndroid::applicationClass(), - "initializeAccessibility"); + QtAndroid::initializeAccessibility(); } bool isActive() @@ -112,6 +127,12 @@ namespace QtAndroidAccessibility QtAndroid::notifyObjectHide(accessibilityObjectId, parentObjectId); } + void notifyObjectShow(uint accessibilityObjectId) + { + const auto parentObjectId = parentId_helper(accessibilityObjectId); + QtAndroid::notifyObjectShow(parentObjectId); + } + void notifyObjectFocus(uint accessibilityObjectId) { QtAndroid::notifyObjectFocus(accessibilityObjectId); @@ -125,6 +146,11 @@ namespace QtAndroidAccessibility QtAndroid::notifyValueChanged(accessibilityObjectId, value); } + void notifyScrolledEvent(uint accessiblityObjectId) + { + QtAndroid::notifyScrolledEvent(accessiblityObjectId); + } + static QVarLengthArray<int, 8> childIdListForAccessibleObject_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); @@ -182,7 +208,7 @@ namespace QtAndroidAccessibility return result; } - static QRect screenRect_helper(int objectId) + static QRect screenRect_helper(int objectId, bool clip = true) { QRect rect; QAccessibleInterface *iface = interfaceFromId(objectId); @@ -190,7 +216,7 @@ namespace QtAndroidAccessibility rect = QHighDpi::toNativePixels(iface->rect(), iface->window()); } // If the widget is not fully in-bound in its parent then we have to clip the rectangle to draw - if (iface && iface->parent() && iface->parent()->isValid()) { + if (clip && iface && iface->parent() && iface->parent()->isValid()) { const auto parentRect = QHighDpi::toNativePixels(iface->parent()->rect(), iface->parent()->window()); rect = rect.intersected(parentRect); } @@ -292,34 +318,44 @@ namespace QtAndroidAccessibility static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) { bool result = false; + + const auto& ids = childIdListForAccessibleObject_helper(objectId); + if (ids.isEmpty()) + return false; + + const int firstChildId = ids.first(); + const QRect oldPosition = screenRect_helper(firstChildId, false); + if (m_accessibilityContext) { runInObjectContext(m_accessibilityContext, [objectId]() { return scroll_helper(objectId, QAccessibleActionInterface::increaseAction()); }, &result); } - return result; + + // Don't check for position change if the call was not successful + return result && oldPosition != screenRect_helper(firstChildId, false); } static jboolean scrollBackward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) { bool result = false; + + const auto& ids = childIdListForAccessibleObject_helper(objectId); + if (ids.isEmpty()) + return false; + + const int firstChildId = ids.first(); + const QRect oldPosition = screenRect_helper(firstChildId, false); + if (m_accessibilityContext) { runInObjectContext(m_accessibilityContext, [objectId]() { return scroll_helper(objectId, QAccessibleActionInterface::decreaseAction()); }, &result); } - return result; - } - -#define FIND_AND_CHECK_CLASS(CLASS_NAME) \ -clazz = env->FindClass(CLASS_NAME); \ -if (!clazz) { \ - __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_classErrorMsg, CLASS_NAME); \ - return JNI_FALSE; \ -} - - //__android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); + // Don't check for position change if the call was not successful + return result && oldPosition != screenRect_helper(firstChildId, false); + } static QString textFromValue(QAccessibleInterface *iface) { @@ -517,7 +553,7 @@ if (!clazz) { \ return true; } - static JNINativeMethod methods[] = { + static const JNINativeMethod methods[] = { {"setActive","(Z)V",(void*)setActive}, {"childIdListForAccessibleObject", "(I)[I", (jintArray)childIdListForAccessibleObject}, {"parentId", "(I)I", (void*)parentId}, @@ -537,13 +573,10 @@ if (!clazz) { \ return false; \ } - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { - jclass clazz; - FIND_AND_CHECK_CLASS("org/qtproject/qt/android/accessibility/QtNativeAccessibility"); - jclass appClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods("org/qtproject/qt/android/QtNativeAccessibility", + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt A11y", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/androidjniaccessibility.h b/src/plugins/platforms/android/androidjniaccessibility.h index 94e64762e4..6e8e059334 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.h +++ b/src/plugins/platforms/android/androidjniaccessibility.h @@ -9,16 +9,19 @@ QT_BEGIN_NAMESPACE class QObject; +class QJniEnvironment; namespace QtAndroidAccessibility { void initialize(); bool isActive(); - bool registerNatives(JNIEnv *env); + bool registerNatives(QJniEnvironment &env); void notifyLocationChange(uint accessibilityObjectId); void notifyObjectHide(uint accessibilityObjectId); + void notifyObjectShow(uint accessibilityObjectId); void notifyObjectFocus(uint accessibilityObjectId); void notifyValueChanged(uint accessibilityObjectId); + void notifyScrolledEvent(uint accessibilityObjectId); void createAccessibilityContextObject(QObject *parent); } diff --git a/src/plugins/platforms/android/androidjniclipboard.cpp b/src/plugins/platforms/android/androidjniclipboard.cpp deleted file mode 100644 index 7b2c2c0443..0000000000 --- a/src/plugins/platforms/android/androidjniclipboard.cpp +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "androidjniclipboard.h" -#include <QtCore/QUrl> -#include <QtCore/QJniObject> -#include <QtCore/QJniEnvironment> - -QT_BEGIN_NAMESPACE - -using namespace QtAndroid; -namespace QtAndroidClipboard -{ - QAndroidPlatformClipboard *m_manager = nullptr; - - static JNINativeMethod methods[] = { - {"onClipboardDataChanged", "()V", (void *)onClipboardDataChanged} - }; - - void setClipboardManager(QAndroidPlatformClipboard *manager) - { - m_manager = manager; - QJniObject::callStaticMethod<void>(applicationClass(), "registerClipboardManager"); - jclass appClass = QtAndroid::applicationClass(); - QJniEnvironment env; - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { - __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); - return; - } - } - void clearClipboardData() - { - QJniObject::callStaticMethod<void>(applicationClass(), "clearClipData"); - } - void setClipboardMimeData(QMimeData *data) - { - clearClipboardData(); - if (data->hasText()) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardText", "(Ljava/lang/String;)V", - QJniObject::fromString(data->text()).object()); - } - if (data->hasHtml()) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardHtml", - "(Ljava/lang/String;Ljava/lang/String;)V", - QJniObject::fromString(data->text()).object(), - QJniObject::fromString(data->html()).object()); - } - if (data->hasUrls()) { - QList<QUrl> urls = data->urls(); - for (const auto &u : qAsConst(urls)) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardUri", - "(Ljava/lang/String;)V", - QJniObject::fromString(u.toEncoded()).object()); - } - } - } - - QMimeData *getClipboardMimeData() - { - QMimeData *data = new QMimeData; - if (QJniObject::callStaticMethod<jboolean>(applicationClass(), "hasClipboardText")) { - data->setText(QJniObject::callStaticObjectMethod(applicationClass(), - "getClipboardText", - "()Ljava/lang/String;").toString()); - } - if (QJniObject::callStaticMethod<jboolean>(applicationClass(), "hasClipboardHtml")) { - data->setHtml(QJniObject::callStaticObjectMethod(applicationClass(), - "getClipboardHtml", - "()Ljava/lang/String;").toString()); - } - if (QJniObject::callStaticMethod<jboolean>(applicationClass(), "hasClipboardUri")) { - QJniObject uris = QJniObject::callStaticObjectMethod(applicationClass(), - "getClipboardUris", - "()[Ljava/lang/String;"); - if (uris.isValid()) { - QList<QUrl> urls; - QJniEnvironment env; - jobjectArray juris = uris.object<jobjectArray>(); - const jint nUris = env->GetArrayLength(juris); - urls.reserve(static_cast<int>(nUris)); - for (int i = 0; i < nUris; ++i) - urls << QUrl(QJniObject(env->GetObjectArrayElement(juris, i)).toString()); - data->setUrls(urls); - } - } - return data; - } - - void onClipboardDataChanged(JNIEnv */*env*/, jobject /*thiz*/) - { - m_manager->emitChanged(QClipboard::Clipboard); - } -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjniclipboard.h b/src/plugins/platforms/android/androidjniclipboard.h deleted file mode 100644 index 24feeef9b3..0000000000 --- a/src/plugins/platforms/android/androidjniclipboard.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef ANDROIDJNICLIPBOARD_H -#define ANDROIDJNICLIPBOARD_H - -#include <QString> -#include "qandroidplatformclipboard.h" -#include "androidjnimain.h" - -QT_BEGIN_NAMESPACE - -class QAndroidPlatformClipboard; -namespace QtAndroidClipboard -{ - // Clipboard support - void setClipboardManager(QAndroidPlatformClipboard *manager); - void setClipboardMimeData(QMimeData *data); - QMimeData *getClipboardMimeData(); - void clearClipboardData(); - void onClipboardDataChanged(JNIEnv */*env*/, jobject /*thiz*/); - // Clipboard support -} - -QT_END_NAMESPACE - -#endif // ANDROIDJNICLIPBOARD_H diff --git a/src/plugins/platforms/android/androidjniinput.cpp b/src/plugins/platforms/android/androidjniinput.cpp index fae8668f63..266d027b3c 100644 --- a/src/plugins/platforms/android/androidjniinput.cpp +++ b/src/plugins/platforms/android/androidjniinput.cpp @@ -1,3 +1,4 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -8,6 +9,7 @@ #include "androidjnimain.h" #include "qandroidplatformintegration.h" +#include <qpa/qplatformwindow.h> #include <qpa/qwindowsysteminterface.h> #include <QTouchEvent> #include <QPointer> @@ -17,66 +19,129 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); + using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtLayout, "org/qtproject/qt/android/QtLayout") +Q_DECLARE_JNI_CLASS(QtLayoutInterface, "org/qtproject/qt/android/QtLayoutInterface") +Q_DECLARE_JNI_CLASS(QtInputInterface, "org/qtproject/qt/android/QtInputInterface") + namespace QtAndroidInput { static bool m_ignoreMouseEvents = false; + static Qt::MouseButtons m_buttons = Qt::NoButton; + static QRect m_softwareKeyboardRect; static QList<QWindowSystemInterface::TouchPoint> m_touchPoints; static QPointer<QWindow> m_mouseGrabber; + GenericMotionEventListener::~GenericMotionEventListener() {} + namespace { + struct GenericMotionEventListeners { + QMutex mutex; + QList<QtAndroidInput::GenericMotionEventListener *> listeners; + }; + } + Q_GLOBAL_STATIC(GenericMotionEventListeners, g_genericMotionEventListeners) + + static jboolean dispatchGenericMotionEvent(JNIEnv *, jclass, jobject event) + { + jboolean ret = JNI_FALSE; + QMutexLocker locker(&g_genericMotionEventListeners()->mutex); + for (auto *listener : std::as_const(g_genericMotionEventListeners()->listeners)) + ret |= listener->handleGenericMotionEvent(event); + return ret; + } + + KeyEventListener::~KeyEventListener() {} + namespace { + struct KeyEventListeners { + QMutex mutex; + QList<QtAndroidInput::KeyEventListener *> listeners; + }; + } + Q_GLOBAL_STATIC(KeyEventListeners, g_keyEventListeners) + + static jboolean dispatchKeyEvent(JNIEnv *, jclass, jobject event) + { + jboolean ret = JNI_FALSE; + QMutexLocker locker(&g_keyEventListeners()->mutex); + for (auto *listener : std::as_const(g_keyEventListeners()->listeners)) + ret |= listener->handleKeyEvent(event); + return ret; + } + + void registerGenericMotionEventListener(QtAndroidInput::GenericMotionEventListener *listener) + { + QMutexLocker locker(&g_genericMotionEventListeners()->mutex); + g_genericMotionEventListeners()->listeners.push_back(listener); + } + + void unregisterGenericMotionEventListener(QtAndroidInput::GenericMotionEventListener *listener) + { + QMutexLocker locker(&g_genericMotionEventListeners()->mutex); + g_genericMotionEventListeners()->listeners.removeOne(listener); + } + + void registerKeyEventListener(QtAndroidInput::KeyEventListener *listener) + { + QMutexLocker locker(&g_keyEventListeners()->mutex); + g_keyEventListeners()->listeners.push_back(listener); + } + + void unregisterKeyEventListener(QtAndroidInput::KeyEventListener *listener) + { + QMutexLocker locker(&g_keyEventListeners()->mutex); + g_keyEventListeners()->listeners.removeOne(listener); + } + + QJniObject qtLayout() + { + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtLayoutInterface, QtJniTypes::QtLayout>( + "getQtLayout"); + } + void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; -#endif - QJniObject::callStaticMethod<void>(applicationClass(), - "updateSelection", - "(IIII)V", - selStart, - selEnd, - candidatesStart, - candidatesEnd); + qCDebug(lcQpaInputMethods) << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("updateSelection", selStart, selEnd, + candidatesStart, candidatesEnd); } void showSoftwareKeyboard(int left, int top, int width, int height, int inputHints, int enterKeyType) { - QJniObject::callStaticMethod<void>(applicationClass(), - "showSoftwareKeyboard", - "(IIIIII)V", - left, - top, - width, - height, - inputHints, - enterKeyType); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>( + "showSoftwareKeyboard", QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), left, top, width, height, inputHints, + enterKeyType); + qCDebug(lcQpaInputMethods) << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; } void resetSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ RESETSOFTWAREKEYBOARD"); -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("resetSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ RESETSOFTWAREKEYBOARD"; } void hideSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "hideSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ HIDESOFTWAREKEYBOARD"); -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("hideSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ HIDESOFTWAREKEYBOARD"; } bool isSoftwareKeyboardVisible() { - return QJniObject::callStaticMethod<jboolean>(applicationClass(), "isSoftwareKeyboardVisible"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtInputInterface, jboolean>( + "isSoftwareKeyboardVisible"); } QRect softwareKeyboardRect() @@ -86,81 +151,150 @@ namespace QtAndroidInput int getSelectHandleWidth() { - return QJniObject::callStaticMethod<jint>(applicationClass(), "getSelectHandleWidth"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtInputInterface, jint>("getSelectHandleWidth"); } void updateHandles(int mode, QPoint editMenuPos, uint32_t editButtons, QPoint cursor, QPoint anchor, bool rtl) { - QJniObject::callStaticMethod<void>(applicationClass(), "updateHandles", "(IIIIIIIIZ)V", - mode, editMenuPos.x(), editMenuPos.y(), editButtons, - cursor.x(), cursor.y(), - anchor.x(), anchor.y(), rtl); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>( + "updateHandles", QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), mode, editMenuPos.x(), editMenuPos.y(), + editButtons, cursor.x(), cursor.y(), anchor.x(), anchor.y(), rtl); + } + + // from https://developer.android.com/reference/android/view/MotionEvent#getButtonState() + enum AndroidMouseButton { + BUTTON_PRIMARY = 0x00000001, + BUTTON_SECONDARY = 0x00000002, + BUTTON_TERTIARY = 0x00000004, + BUTTON_BACK = 0x00000008, + BUTTON_FORWARD = 0x00000010, + BUTTON_STYLUS_PRIMARY = 0x00000020, + BUTTON_STYLUS_SECONDARY = 0x00000040, + }; + Q_DECLARE_FLAGS(AndroidMouseButtons, AndroidMouseButton) + + static Qt::MouseButtons toMouseButtons(jint j_buttons) + { + const auto buttons = static_cast<AndroidMouseButtons>(j_buttons); + Qt::MouseButtons mouseButtons; + if (buttons.testFlag(BUTTON_PRIMARY)) + mouseButtons.setFlag(Qt::LeftButton); + + if (buttons.testFlag(BUTTON_SECONDARY)) + mouseButtons.setFlag(Qt::RightButton); + + if (buttons.testFlag(BUTTON_TERTIARY)) + mouseButtons.setFlag(Qt::MiddleButton); + + if (buttons.testFlag(BUTTON_BACK)) + mouseButtons.setFlag(Qt::BackButton); + + if (buttons.testFlag(BUTTON_FORWARD)) + mouseButtons.setFlag(Qt::ForwardButton); + + if (buttons.testFlag(BUTTON_STYLUS_PRIMARY)) + mouseButtons.setFlag(Qt::LeftButton); + + if (buttons.testFlag(BUTTON_STYLUS_SECONDARY)) + mouseButtons.setFlag(Qt::RightButton); + + // Fall back to left button + if (Q_UNLIKELY(buttons != 0 && mouseButtons == Qt::NoButton)) { + qWarning() << "Unhandled button value:" << buttons << "Falling back to Qt::LeftButton"; + mouseButtons = Qt::LeftButton; + } + return mouseButtons; } - static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void sendMouseButtonEvents(QWindow *topLevel, QPoint localPos, QPoint globalPos, + jint mouseButtonState, QEvent::Type type) + { + const Qt::MouseButtons mouseButtons = toMouseButtons(mouseButtonState); + const Qt::MouseButtons changedButtons = mouseButtons & ~m_buttons; + + if (changedButtons == Qt::NoButton) + return; + + static_assert (sizeof(changedButtons) <= sizeof(uint), "Qt::MouseButtons size changed. Adapt code."); + + for (uint buttonInt = 0x1; static_cast<uint>(changedButtons) >= buttonInt; buttonInt <<= 1) { + const auto button = static_cast<Qt::MouseButton>(buttonInt); + if (changedButtons.testFlag(button)) { + QWindowSystemInterface::handleMouseEvent(topLevel, localPos, globalPos, + mouseButtons, button, type); + } + } + } + + static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jint mouseButtonState) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = topLevelWindowAt(globalPos); - m_mouseGrabber = tlw; - QPoint localPos = tlw ? (globalPos - tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, - Qt::MouseButtons(Qt::LeftButton), - Qt::LeftButton, QEvent::MouseButtonPress); - } - - static void mouseUp(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) - { - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos -tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, - Qt::MouseButtons(Qt::NoButton), - Qt::LeftButton, QEvent::MouseButtonRelease); + const QPoint globalPos(x,y); + QWindow *window = windowFromId(winId); + m_mouseGrabber = window; + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + sendMouseButtonEvents(window, localPos, globalPos, mouseButtonState, QEvent::MouseButtonPress); + } + + static void mouseUp(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jint mouseButtonState) + { + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + + sendMouseButtonEvents(window, localPos, globalPos, mouseButtonState, QEvent::MouseButtonRelease); m_ignoreMouseEvents = false; - m_mouseGrabber = 0; + m_mouseGrabber.clear(); } - static void mouseMove(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void mouseMove(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(m_mouseGrabber ? Qt::LeftButton : Qt::NoButton), Qt::NoButton, QEvent::MouseMove); } - static void mouseWheel(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y, jfloat hdelta, jfloat vdelta) + static void mouseWheel(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jfloat hdelta, jfloat vdelta) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; - QPoint angleDelta(hdelta * 120, vdelta * 120); + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + const QPoint angleDelta(hdelta * 120, vdelta * 120); - QWindowSystemInterface::handleWheelEvent(tlw, + QWindowSystemInterface::handleWheelEvent(window, localPos, globalPos, QPoint(), angleDelta); } - static void longPress(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void longPress(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y) { QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) @@ -171,16 +305,17 @@ namespace QtAndroidInput if (!rightMouseFromLongPress) return; m_ignoreMouseEvents = true; - QPoint globalPos(x,y); - QWindow *tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; + const QPoint globalPos(x,y); + QWindow *window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; // Click right button if no other button is already pressed. if (!m_mouseGrabber) { - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(Qt::RightButton), Qt::RightButton, QEvent::MouseButtonPress); - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(Qt::NoButton), Qt::RightButton, QEvent::MouseButtonRelease); } @@ -218,12 +353,11 @@ namespace QtAndroidInput touchPoint.rotation = qRadiansToDegrees(rotation); touchPoint.normalPosition = QPointF(double(x / dw), double(y / dh)); touchPoint.state = state; - touchPoint.area = QRectF(x - double(minor), - y - double(major), - double(minor * 2), - double(major * 2)); + touchPoint.area = QRectF(x - double(minor * 0.5f), + y - double(major * 0.5f), + double(minor), + double(major)); m_touchPoints.push_back(touchPoint); - if (state == QEventPoint::State::Pressed) { QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) @@ -254,31 +388,35 @@ namespace QtAndroidInput return touchDevice; } - static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/, jint /*action*/) + static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint winId, jint /*action*/) { if (m_touchPoints.isEmpty()) return; QMutexLocker lock(QtAndroid::platformInterfaceMutex()); - QPointingDevice *touchDevice = getTouchDevice(); + const QPointingDevice *touchDevice = getTouchDevice(); if (!touchDevice) return; - QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) + return; QWindowSystemInterface::handleTouchEvent(window, touchDevice, m_touchPoints); } - static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/) + static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint winId) { if (m_touchPoints.isEmpty()) return; QMutexLocker lock(QtAndroid::platformInterfaceMutex()); - QPointingDevice *touchDevice = getTouchDevice(); + const QPointingDevice *touchDevice = getTouchDevice(); if (!touchDevice) return; - QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) + return; QWindowSystemInterface::handleTouchCancelEvent(window, touchDevice); } @@ -291,14 +429,14 @@ namespace QtAndroidInput #endif // QT_CONFIG(tabletevent) } - static void tabletEvent(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint deviceId, jlong time, jint action, + static void tabletEvent(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint deviceId, jlong time, jint action, jint pointerType, jint buttonState, jfloat x, jfloat y, jfloat pressure) { #if QT_CONFIG(tabletevent) - QPointF globalPosF(x, y); - QPoint globalPos((int)x, (int)y); - QWindow *tlw = topLevelWindowAt(globalPos); - QPointF localPos = tlw ? (globalPosF - tlw->position()) : globalPosF; + const QPointF globalPosF(x, y); + QWindow *window = windowFromId(winId); + const QPointF localPos = window && window->handle() ? + window->handle()->mapFromGlobalF(globalPosF) : globalPosF; // Galaxy Note with plain Android: // 0 1 0 stylus press @@ -318,6 +456,7 @@ namespace QtAndroidInput Qt::MouseButtons buttons = Qt::NoButton; switch (action) { case 1: // ACTION_UP + case 6: // ACTION_POINTER_UP, happens if stylus is not the primary pointer case 212: // stylus release while side-button held on Galaxy Note 4 buttons = Qt::NoButton; break; @@ -329,11 +468,9 @@ namespace QtAndroidInput break; } -#ifdef QT_DEBUG_ANDROID_STYLUS - qDebug() << action << pointerType << buttonState << '@' << x << y << "pressure" << pressure << ": buttons" << buttons; -#endif + qCDebug(lcQpaInputMethods) << action << pointerType << buttonState << '@' << x << y << "pressure" << pressure << ": buttons" << buttons; - QWindowSystemInterface::handleTabletEvent(tlw, ulong(time), + QWindowSystemInterface::handleTabletEvent(window, ulong(time), localPos, globalPosF, int(QInputDevice::DeviceType::Stylus), pointerType, buttons, pressure, 0, 0, 0., 0., 0, deviceId, Qt::NoModifier); #endif // QT_CONFIG(tabletevent) @@ -797,9 +934,7 @@ namespace QtAndroidInput QMetaObject::invokeMethod(inputContext, "hideSelectionHandles", Qt::QueuedConnection); } } -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ KEYBOARDVISIBILITYCHANGED" << inputContext; -#endif + qCDebug(lcQpaInputMethods) << "@@@ KEYBOARDVISIBILITYCHANGED" << inputContext; } static void keyboardGeometryChanged(JNIEnv */*env*/, jobject /*thiz*/, jint x, jint y, jint w, jint h) @@ -812,16 +947,12 @@ namespace QtAndroidInput if (inputContext && qGuiApp) inputContext->emitKeyboardRectChanged(); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ KEYBOARDRECTCHANGED" << m_softwareKeyboardRect; -#endif + qCDebug(lcQpaInputMethods) << "@@@ KEYBOARDRECTCHANGED" << m_softwareKeyboardRect; } static void handleLocationChanged(JNIEnv */*env*/, jobject /*thiz*/, int id, int x, int y) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ handleLocationChanged" << id << x << y; -#endif + qCDebug(lcQpaInputMethods) << "@@@ handleLocationChanged" << id << x << y; QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) QMetaObject::invokeMethod(inputContext, "handleLocationChanged", Qt::BlockingQueuedConnection, @@ -829,13 +960,14 @@ namespace QtAndroidInput } - static JNINativeMethod methods[] = { + + static const JNINativeMethod methods[] = { {"touchBegin","(I)V",(void*)touchBegin}, {"touchAdd","(IIIZIIFFFF)V",(void*)touchAdd}, {"touchEnd","(II)V",(void*)touchEnd}, {"touchCancel", "(I)V", (void *)touchCancel}, - {"mouseDown", "(III)V", (void *)mouseDown}, - {"mouseUp", "(III)V", (void *)mouseUp}, + {"mouseDown", "(IIII)V", (void *)mouseDown}, + {"mouseUp", "(IIII)V", (void *)mouseUp}, {"mouseMove", "(III)V", (void *)mouseMove}, {"mouseWheel", "(IIIFF)V", (void *)mouseWheel}, {"longPress", "(III)V", (void *)longPress}, @@ -845,14 +977,15 @@ namespace QtAndroidInput {"keyUp", "(IIIZ)V", (void *)keyUp}, {"keyboardVisibilityChanged", "(Z)V", (void *)keyboardVisibilityChanged}, {"keyboardGeometryChanged", "(IIII)V", (void *)keyboardGeometryChanged}, - {"handleLocationChanged", "(III)V", (void *)handleLocationChanged} + {"handleLocationChanged", "(III)V", (void *)handleLocationChanged}, + {"dispatchGenericMotionEvent", "(Landroid/view/MotionEvent;)Z", reinterpret_cast<void *>(dispatchGenericMotionEvent)}, + {"dispatchKeyEvent", "(Landroid/view/KeyEvent;)Z", reinterpret_cast<void *>(dispatchKeyEvent)}, }; - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { - jclass appClass = QtAndroid::applicationClass(); - - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(QtJniTypes::Traits<QtJniTypes::QtInputDelegate>::className(), + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/androidjniinput.h b/src/plugins/platforms/android/androidjniinput.h index 7ef51ef9f7..28a2665bf6 100644 --- a/src/plugins/platforms/android/androidjniinput.h +++ b/src/plugins/platforms/android/androidjniinput.h @@ -6,10 +6,15 @@ #include <jni.h> #include <QtCore/qglobal.h> +#include <QtCore/QLoggingCategory> #include <QtCore/QRect> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaInputMethods); + +class QJniEnvironment; + namespace QtAndroidInput { // Software keyboard support @@ -26,7 +31,27 @@ namespace QtAndroidInput QPoint cursor = QPoint(), QPoint anchor = QPoint(), bool rtl = false); int getSelectHandleWidth(); - bool registerNatives(JNIEnv *env); + class GenericMotionEventListener + { + public: + virtual ~GenericMotionEventListener(); + virtual bool handleGenericMotionEvent(jobject event) = 0; + }; + + class KeyEventListener + { + public: + virtual ~KeyEventListener(); + virtual bool handleKeyEvent(jobject event) = 0; + }; + + void registerGenericMotionEventListener(GenericMotionEventListener *listener); + void unregisterGenericMotionEventListener(GenericMotionEventListener *listener); + + void registerKeyEventListener(KeyEventListener *listener); + void unregisterKeyEventListener(KeyEventListener *listener); + + bool registerNatives(QJniEnvironment &env); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp index 3b5e656630..5bd2b924fc 100644 --- a/src/plugins/platforms/android/androidjnimain.cpp +++ b/src/plugins/platforms/android/androidjnimain.cpp @@ -10,14 +10,16 @@ #include "androidcontentfileengine.h" #include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" -#include "androidjniclipboard.h" #include "androidjniinput.h" #include "androidjnimain.h" #include "androidjnimenu.h" +#include "androidwindowembedding.h" #include "qandroidassetsfileenginehandler.h" #include "qandroideventdispatcher.h" #include "qandroidplatformdialoghelpers.h" #include "qandroidplatformintegration.h" +#include "qandroidplatformclipboard.h" +#include "qandroidplatformwindow.h" #include <android/api-level.h> #include <android/asset_manager_jni.h> @@ -27,13 +29,18 @@ #include <QtCore/qbasicatomic.h> #include <QtCore/qjnienvironment.h> #include <QtCore/qjniobject.h> +#include <QtCore/qprocess.h> #include <QtCore/qresource.h> +#include <QtCore/qscopeguard.h> #include <QtCore/qthread.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qhighdpiscaling_p.h> #include <qpa/qwindowsysteminterface.h> + +using namespace Qt::StringLiterals; + QT_BEGIN_NAMESPACE static JavaVM *m_javaVM = nullptr; @@ -43,11 +50,9 @@ static jmethodID m_loadClassMethodID = nullptr; static AAssetManager *m_assetManager = nullptr; static jobject m_assets = nullptr; static jobject m_resourcesObj = nullptr; -static QtJniTypes::Activity m_activityObject = nullptr; -static jmethodID m_createSurfaceMethodID = nullptr; -static QtJniTypes::Service m_serviceObject = nullptr; -static jmethodID m_setSurfaceGeometryMethodID = nullptr; -static jmethodID m_destroySurfaceMethodID = nullptr; + +static jclass m_qtActivityClass = nullptr; +static jclass m_qtServiceClass = nullptr; static int m_pendingApplicationState = -1; static QBasicMutex m_platformMutex; @@ -66,10 +71,6 @@ static void *m_mainLibraryHnd = nullptr; static QList<QByteArray> m_applicationParams; static sem_t m_exitSemaphore, m_terminateSemaphore; -QHash<int, AndroidSurfaceClient *> m_surfaces; - -static QBasicMutex m_surfacesMutex; - static QAndroidPlatformIntegration *m_androidPlatformIntegration = nullptr; @@ -81,13 +82,16 @@ static double m_density = 1.0; static AndroidAssetsFileEngineHandler *m_androidAssetsFileEngineHandler = nullptr; static AndroidContentFileEngineHandler *m_androidContentFileEngineHandler = nullptr; - +static AndroidBackendRegister *m_backendRegister = nullptr; static const char m_qtTag[] = "Qt"; static const char m_classErrorMsg[] = "Can't find class \"%s\""; static const char m_methodErrorMsg[] = "Can't find method \"%s%s\""; -static QBasicAtomicInt startQtAndroidPluginCalled = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_CONSTINIT static QBasicAtomicInt startQtAndroidPluginCalled = Q_BASIC_ATOMIC_INITIALIZER(0); + +Q_DECLARE_JNI_CLASS(QtWindowInterface, "org/qtproject/qt/android/QtWindowInterface") +Q_DECLARE_JNI_CLASS(QtAccessibilityInterface, "org/qtproject/qt/android/QtAccessibilityInterface"); namespace QtAndroid { @@ -99,6 +103,7 @@ namespace QtAndroid void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration) { m_androidPlatformIntegration = androidPlatformIntegration; + QtAndroid::notifyNativePluginIntegrationReady((bool)m_androidPlatformIntegration); // flush the pending state if necessary. if (m_androidPlatformIntegration && (m_pendingApplicationState != -1)) { @@ -124,6 +129,21 @@ namespace QtAndroid : 0; } + QWindow *windowFromId(int windowId) + { + if (!qGuiApp) + return nullptr; + + for (QWindow *w : qGuiApp->allWindows()) { + if (!w->handle()) + continue; + QAndroidPlatformWindow *window = static_cast<QAndroidPlatformWindow *>(w->handle()); + if (window->nativeViewId() == windowId) + return w; + } + return nullptr; + } + int availableWidthPixels() { return m_availableWidthPixels; @@ -159,47 +179,79 @@ namespace QtAndroid return m_applicationClass; } - QtJniTypes::Activity activity() + // TODO move calls from here to where they logically belong + void setSystemUiVisibility(SystemUiVisibility uiVisibility) { - return m_activityObject; + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("setSystemUiVisibility", + jint(uiVisibility)); } - QtJniTypes::Service service() + bool isQtApplication() { - return m_serviceObject; + // Returns true if the app is a Qt app, i.e. Qt controls the whole app and + // the Activity/Service is created by Qt. Returns false if instead Qt is + // embedded into a native Android app, where the Activity/Service is created + // by the user, outside of Qt, and Qt content is added as a view. + JNIEnv *env = QJniEnvironment::getJniEnv(); + auto activity = QtAndroidPrivate::activity(); + if (activity.isValid()) + return env->IsInstanceOf(activity.object(), m_qtActivityClass); + auto service = QtAndroidPrivate::service(); + if (service.isValid()) + return env->IsInstanceOf(QtAndroidPrivate::service().object(), m_qtServiceClass); + // return true as default as Qt application is our default use case. + // famous last words: we should not end up here + return true; } - void setSystemUiVisibility(SystemUiVisibility uiVisibility) + void initializeAccessibility() { - QJniObject::callStaticMethod<void>(m_applicationClass, "setSystemUiVisibility", "(I)V", jint(uiVisibility)); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "initializeAccessibility"); } void notifyAccessibilityLocationChange(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyAccessibilityLocationChange", - "(I)V", accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyLocationChange", accessibilityObjectId); } void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectHide", "(II)V", - accessibilityObjectId, parentObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectHide", accessibilityObjectId, parentObjectId); + } + + void notifyObjectShow(uint parentObjectId) + { + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectShow", parentObjectId); } void notifyObjectFocus(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectFocus","(I)V", accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectFocus", accessibilityObjectId); } void notifyValueChanged(uint accessibilityObjectId, jstring value) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyValueChanged", - "(ILjava/lang/String;)V", accessibilityObjectId, value); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyValueChanged", accessibilityObjectId, value); + } + + void notifyScrolledEvent(uint accessibilityObjectId) + { + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyScrolledEvent", accessibilityObjectId); } - void notifyQtAndroidPluginRunning(bool running) + void notifyNativePluginIntegrationReady(bool ready) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyQtAndroidPluginRunning","(Z)V", running); + QJniObject::callStaticMethod<void>(m_applicationClass, + "notifyNativePluginIntegrationReady", + ready); } jobject createBitmap(QImage img, JNIEnv *env) @@ -208,7 +260,7 @@ namespace QtAndroid return 0; if (img.format() != QImage::Format_RGBA8888 && img.format() != QImage::Format_RGB16) - img = img.convertToFormat(QImage::Format_RGBA8888); + img = std::move(img).convertToFormat(QImage::Format_RGBA8888); jobject bitmap = env->CallStaticObjectMethod(m_bitmapClass, m_createBitmapMethodID, @@ -296,62 +348,6 @@ namespace QtAndroid return manufacturer + u' ' + model; } - jint generateViewId() - { - return QJniObject::callStaticMethod<jint>("android/view/View", "generateViewId", "()I"); - } - - int createSurface(AndroidSurfaceClient *client, const QRect &geometry, bool onTop, int imageDepth) - { - QJniEnvironment env; - if (!env.jniEnv()) - return -1; - - m_surfacesMutex.lock(); - jint surfaceId = generateViewId(); - m_surfaces[surfaceId] = client; - m_surfacesMutex.unlock(); - - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) { - x = geometry.x(); - y = geometry.y(); - w = std::max(geometry.width(), 1); - h = std::max(geometry.height(), 1); - } - env->CallStaticVoidMethod(m_applicationClass, - m_createSurfaceMethodID, - surfaceId, - jboolean(onTop), - x, y, w, h, - imageDepth); - return surfaceId; - } - - int insertNativeView(jobject view, const QRect &geometry) - { - m_surfacesMutex.lock(); - jint surfaceId = generateViewId(); - m_surfaces[surfaceId] = nullptr; // dummy - m_surfacesMutex.unlock(); - - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) - geometry.getRect(&x, &y, &w, &h); - - QJniObject::callStaticMethod<void>(m_applicationClass, - "insertNativeView", - "(ILandroid/view/View;IIII)V", - surfaceId, - view, - x, - y, - qMax(w, 1), - qMax(h, 1)); - - return surfaceId; - } - void setViewVisibility(jobject view, bool visible) { QJniObject::callStaticMethod<void>(m_applicationClass, @@ -361,69 +357,6 @@ namespace QtAndroid visible); } - void setSurfaceGeometry(int surfaceId, const QRect &geometry) - { - if (surfaceId == -1) - return; - - QJniEnvironment env; - if (!env.jniEnv()) - return; - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) { - x = geometry.x(); - y = geometry.y(); - w = geometry.width(); - h = geometry.height(); - } - env->CallStaticVoidMethod(m_applicationClass, - m_setSurfaceGeometryMethodID, - surfaceId, - x, y, w, h); - } - - - void destroySurface(int surfaceId) - { - if (surfaceId == -1) - return; - - { - QMutexLocker lock(&m_surfacesMutex); - const auto &it = m_surfaces.find(surfaceId); - if (it != m_surfaces.end()) - m_surfaces.erase(it); - } - - QJniEnvironment env; - if (env.jniEnv()) - env->CallStaticVoidMethod(m_applicationClass, - m_destroySurfaceMethodID, - surfaceId); - } - - void bringChildToFront(int surfaceId) - { - if (surfaceId == -1) - return; - - QJniObject::callStaticMethod<void>(m_applicationClass, - "bringChildToFront", - "(I)V", - surfaceId); - } - - void bringChildToBack(int surfaceId) - { - if (surfaceId == -1) - return; - - QJniObject::callStaticMethod<void>(m_applicationClass, - "bringChildToBack", - "(I)V", - surfaceId); - } - bool blockEventLoopsWhenSuspended() { static bool block = qEnvironmentVariableIntValue("QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED"); @@ -435,21 +368,27 @@ namespace QtAndroid return m_assets; } + AndroidBackendRegister *backendRegister() + { + return m_backendRegister; + } + } // namespace QtAndroid static jboolean startQtAndroidPlugin(JNIEnv *env, jobject /*object*/, jstring paramsString) { + Q_UNUSED(env) + m_androidPlatformIntegration = nullptr; m_androidAssetsFileEngineHandler = new AndroidAssetsFileEngineHandler(); m_androidContentFileEngineHandler = new AndroidContentFileEngineHandler(); m_mainLibraryHnd = nullptr; + m_backendRegister = new AndroidBackendRegister(); - const char *nativeString = env->GetStringUTFChars(paramsString, 0); - QByteArray string = nativeString; - env->ReleaseStringUTFChars(paramsString, nativeString); + const QStringList argsList = QProcess::splitCommand(QJniObject(paramsString).toString()); - for (auto str : string.split('\t')) - m_applicationParams.append(str.split(' ')); + for (const QString &arg : argsList) + m_applicationParams.append(arg.toUtf8()); // Go home QDir::setCurrent(QDir::homePath()); @@ -489,7 +428,7 @@ static void waitForServiceSetup(JNIEnv *env, jclass /*clazz*/) Q_UNUSED(env); // The service must wait until the QCoreApplication starts otherwise onBind will be // called too early - if (m_serviceObject) + if (QtAndroidPrivate::service().isValid() && QtAndroid::isQtApplication()) QtAndroidPrivate::waitForServiceSetup(); } @@ -520,7 +459,8 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) argv[argc] = nullptr; startQtAndroidPluginCalled.fetchAndAddRelease(1); - int ret = m_main(argc, argv.data()); + const int ret = m_main(argc, argv.data()); + qInfo() << "main() returned" << ret; if (m_mainLibraryHnd) { int res = dlclose(m_mainLibraryHnd); @@ -528,10 +468,8 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) qWarning() << "dlclose failed:" << dlerror(); } - if (m_applicationClass) { - qWarning("exit app 0"); + if (m_applicationClass) QJniObject::callStaticMethod<void>(m_applicationClass, "quitApp", "()V"); - } sem_post(&m_terminateSemaphore); sem_wait(&m_exitSemaphore); @@ -576,10 +514,6 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) env->DeleteGlobalRef(m_classLoaderObject); if (m_resourcesObj) env->DeleteGlobalRef(m_resourcesObj); - if (m_activityObject) - env->DeleteGlobalRef(m_activityObject); - if (m_serviceObject) - env->DeleteGlobalRef(m_serviceObject); if (m_bitmapClass) env->DeleteGlobalRef(m_bitmapClass); if (m_ARGB_8888_BitmapConfigValue) @@ -590,53 +524,51 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) env->DeleteGlobalRef(m_bitmapDrawableClass); if (m_assets) env->DeleteGlobalRef(m_assets); + if (m_qtActivityClass) + env->DeleteGlobalRef(m_qtActivityClass); + if (m_qtServiceClass) + env->DeleteGlobalRef(m_qtServiceClass); m_androidPlatformIntegration = nullptr; delete m_androidAssetsFileEngineHandler; m_androidAssetsFileEngineHandler = nullptr; + delete m_backendRegister; + m_backendRegister = nullptr; sem_post(&m_exitSemaphore); } -static void setSurface(JNIEnv *env, jobject /*thiz*/, jint id, jobject jSurface, jint w, jint h) -{ - QMutexLocker lock(&m_surfacesMutex); - const auto &it = m_surfaces.find(id); - if (it == m_surfaces.end()) - return; - - auto surfaceClient = it.value(); - if (surfaceClient) - surfaceClient->surfaceChanged(env, jSurface, w, h); -} - static void setDisplayMetrics(JNIEnv * /*env*/, jclass /*clazz*/, jint screenWidthPixels, jint screenHeightPixels, jint availableLeftPixels, jint availableTopPixels, jint availableWidthPixels, jint availableHeightPixels, jdouble xdpi, jdouble ydpi, jdouble scaledDensity, jdouble density, jfloat refreshRate) { + Q_UNUSED(availableLeftPixels) + Q_UNUSED(availableTopPixels) + m_availableWidthPixels = availableWidthPixels; m_availableHeightPixels = availableHeightPixels; m_scaledDensity = scaledDensity; m_density = density; + const QSize screenSize(screenWidthPixels, screenHeightPixels); + // available geometry always starts from top left + const QRect availableGeometry(0, 0, availableWidthPixels, availableHeightPixels); + const QSize physicalSize(qRound(double(screenWidthPixels) / xdpi * 25.4), + qRound(double(screenHeightPixels) / ydpi * 25.4)); + QMutexLocker lock(&m_platformMutex); if (!m_androidPlatformIntegration) { QAndroidPlatformIntegration::setDefaultDisplayMetrics( - availableLeftPixels, availableTopPixels, availableWidthPixels, - availableHeightPixels, qRound(double(screenWidthPixels) / xdpi * 25.4), - qRound(double(screenHeightPixels) / ydpi * 25.4), screenWidthPixels, - screenHeightPixels); + availableGeometry.left(), availableGeometry.top(), availableGeometry.width(), + availableGeometry.height(), physicalSize.width(), physicalSize.height(), + screenSize.width(), screenSize.height()); } else { - const QSize physicalSize(qRound(double(screenWidthPixels) / xdpi * 25.4), - qRound(double(screenHeightPixels) / ydpi * 25.4)); - const QSize screenSize(screenWidthPixels, screenHeightPixels); - const QRect availableGeometry(availableLeftPixels, availableTopPixels, - availableWidthPixels, availableHeightPixels); m_androidPlatformIntegration->setScreenSizeParameters(physicalSize, screenSize, availableGeometry); m_androidPlatformIntegration->setRefreshRate(refreshRate); } } +Q_DECLARE_JNI_NATIVE_METHOD(setDisplayMetrics) static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/) { @@ -656,10 +588,6 @@ static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/) QWindowSystemInterface::handleExposeEvent(w, QRegion(QRect(QPoint(), w->geometry().size()))); } } - - QAndroidPlatformScreen *screen = static_cast<QAndroidPlatformScreen *>(m_androidPlatformIntegration->screen()); - if (screen->rasterSurfaces()) - QMetaObject::invokeMethod(screen, "setDirty", Qt::QueuedConnection, Q_ARG(QRect,screen->geometry())); } static void updateApplicationState(JNIEnv */*env*/, jobject /*thiz*/, jint state) @@ -738,12 +666,42 @@ static void handleOrientationChanged(JNIEnv */*env*/, jobject /*thiz*/, jint new } } } +Q_DECLARE_JNI_NATIVE_METHOD(handleOrientationChanged) static void handleRefreshRateChanged(JNIEnv */*env*/, jclass /*cls*/, jfloat refreshRate) { if (m_androidPlatformIntegration) m_androidPlatformIntegration->setRefreshRate(refreshRate); } +Q_DECLARE_JNI_NATIVE_METHOD(handleRefreshRateChanged) + +static void handleScreenAdded(JNIEnv */*env*/, jclass /*cls*/, jint displayId) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->handleScreenAdded(displayId); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenAdded) + +static void handleScreenChanged(JNIEnv */*env*/, jclass /*cls*/, jint displayId) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->handleScreenChanged(displayId); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenChanged) + +static void handleScreenRemoved(JNIEnv */*env*/, jclass /*cls*/, jint displayId) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->handleScreenRemoved(displayId); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenRemoved) + +static void handleUiDarkModeChanged(JNIEnv */*env*/, jobject /*thiz*/, jint newUiMode) +{ + QAndroidPlatformIntegration::updateColorScheme( + (newUiMode == 1 ) ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleUiDarkModeChanged) static void onActivityResult(JNIEnv */*env*/, jclass /*cls*/, jint requestCode, @@ -770,112 +728,131 @@ static JNINativeMethod methods[] = { { "quitQtCoreApplication", "()V", (void *)quitQtCoreApplication }, { "terminateQt", "()V", (void *)terminateQt }, { "waitForServiceSetup", "()V", (void *)waitForServiceSetup }, - { "setDisplayMetrics", "(IIIIIIDDDDF)V", (void *)setDisplayMetrics }, - { "setSurface", "(ILjava/lang/Object;II)V", (void *)setSurface }, { "updateWindow", "()V", (void *)updateWindow }, { "updateApplicationState", "(I)V", (void *)updateApplicationState }, - { "handleOrientationChanged", "(II)V", (void *)handleOrientationChanged }, { "onActivityResult", "(IILandroid/content/Intent;)V", (void *)onActivityResult }, { "onNewIntent", "(Landroid/content/Intent;)V", (void *)onNewIntent }, - { "onBind", "(Landroid/content/Intent;)Landroid/os/IBinder;", (void *)onBind }, - { "handleRefreshRateChanged", "(F)V", (void *)handleRefreshRateChanged } + { "onBind", "(Landroid/content/Intent;)Landroid/os/IBinder;", (void *)onBind } }; #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ clazz = env->FindClass(CLASS_NAME); \ if (!clazz) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_classErrorMsg, CLASS_NAME); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetStaticMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \ VAR = env->GetFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, FIELD_NAME, FIELD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_STATIC_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \ VAR = env->GetStaticFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, FIELD_NAME, FIELD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } -static int registerNatives(JNIEnv *env) +Q_DECLARE_JNI_CLASS(QtDisplayManager, "org/qtproject/qt/android/QtDisplayManager") + +static bool registerNatives(QJniEnvironment &env) { jclass clazz; FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtNative"); m_applicationClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - if (env->RegisterNatives(m_applicationClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(m_applicationClass, + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); - return JNI_FALSE; + return false; } - GET_AND_CHECK_STATIC_METHOD(m_createSurfaceMethodID, m_applicationClass, "createSurface", "(IZIIIII)V"); - GET_AND_CHECK_STATIC_METHOD(m_setSurfaceGeometryMethodID, m_applicationClass, "setSurfaceGeometry", "(IIIII)V"); - GET_AND_CHECK_STATIC_METHOD(m_destroySurfaceMethodID, m_applicationClass, "destroySurface", "(I)V"); + bool success = env.registerNativeMethods( + QtJniTypes::Traits<QtJniTypes::QtDisplayManager>::className(), + { + Q_JNI_NATIVE_METHOD(setDisplayMetrics), + Q_JNI_NATIVE_METHOD(handleOrientationChanged), + Q_JNI_NATIVE_METHOD(handleRefreshRateChanged), + Q_JNI_NATIVE_METHOD(handleScreenAdded), + Q_JNI_NATIVE_METHOD(handleScreenChanged), + Q_JNI_NATIVE_METHOD(handleScreenRemoved), + Q_JNI_NATIVE_METHOD(handleUiDarkModeChanged) + }); + + if (!success) { + qCritical() << "QtDisplayManager: registerNativeMethods() failed"; + return JNI_FALSE; + } jmethodID methodID; GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "activity", "()Landroid/app/Activity;"); - jobject activityObject = env->CallStaticObjectMethod(m_applicationClass, methodID); - GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "service", "()Landroid/app/Service;"); - jobject serviceObject = env->CallStaticObjectMethod(m_applicationClass, methodID); + jobject contextObject = env->CallStaticObjectMethod(m_applicationClass, methodID); + if (!contextObject) { + GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "service", "()Landroid/app/Service;"); + contextObject = env->CallStaticObjectMethod(m_applicationClass, methodID); + } + + if (!contextObject) { + __android_log_print(ANDROID_LOG_FATAL,"Qt", "Failed to get Activity or Service object"); + return false; + } + const auto releaseContextObject = qScopeGuard([&env, contextObject]{ + env->DeleteLocalRef(contextObject); + }); + GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "classLoader", "()Ljava/lang/ClassLoader;"); m_classLoaderObject = env->NewGlobalRef(env->CallStaticObjectMethod(m_applicationClass, methodID)); clazz = env->GetObjectClass(m_classLoaderObject); GET_AND_CHECK_METHOD(m_loadClassMethodID, clazz, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); - if (serviceObject) - m_serviceObject = env->NewGlobalRef(serviceObject); - - if (activityObject) - m_activityObject = env->NewGlobalRef(activityObject); - - jobject object = activityObject ? activityObject : serviceObject; - if (object) { - FIND_AND_CHECK_CLASS("android/content/ContextWrapper"); - GET_AND_CHECK_METHOD(methodID, clazz, "getAssets", "()Landroid/content/res/AssetManager;"); - m_assets = env->NewGlobalRef(env->CallObjectMethod(object, methodID)); - m_assetManager = AAssetManager_fromJava(env, m_assets); - - GET_AND_CHECK_METHOD(methodID, clazz, "getResources", "()Landroid/content/res/Resources;"); - m_resourcesObj = env->NewGlobalRef(env->CallObjectMethod(object, methodID)); - - FIND_AND_CHECK_CLASS("android/graphics/Bitmap"); - m_bitmapClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - GET_AND_CHECK_STATIC_METHOD(m_createBitmapMethodID, m_bitmapClass - , "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); - FIND_AND_CHECK_CLASS("android/graphics/Bitmap$Config"); - jfieldID fieldId; - GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "ARGB_8888", "Landroid/graphics/Bitmap$Config;"); - m_ARGB_8888_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); - GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "RGB_565", "Landroid/graphics/Bitmap$Config;"); - m_RGB_565_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); - - FIND_AND_CHECK_CLASS("android/graphics/drawable/BitmapDrawable"); - m_bitmapDrawableClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - GET_AND_CHECK_METHOD(m_bitmapDrawableConstructorMethodID, - m_bitmapDrawableClass, - "<init>", - "(Landroid/content/res/Resources;Landroid/graphics/Bitmap;)V"); - } - - return JNI_TRUE; + + FIND_AND_CHECK_CLASS("android/content/ContextWrapper"); + GET_AND_CHECK_METHOD(methodID, clazz, "getAssets", "()Landroid/content/res/AssetManager;"); + m_assets = env->NewGlobalRef(env->CallObjectMethod(contextObject, methodID)); + m_assetManager = AAssetManager_fromJava(env.jniEnv(), m_assets); + + GET_AND_CHECK_METHOD(methodID, clazz, "getResources", "()Landroid/content/res/Resources;"); + m_resourcesObj = env->NewGlobalRef(env->CallObjectMethod(contextObject, methodID)); + + FIND_AND_CHECK_CLASS("android/graphics/Bitmap"); + m_bitmapClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + GET_AND_CHECK_STATIC_METHOD(m_createBitmapMethodID, m_bitmapClass, + "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); + FIND_AND_CHECK_CLASS("android/graphics/Bitmap$Config"); + jfieldID fieldId; + GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "ARGB_8888", "Landroid/graphics/Bitmap$Config;"); + m_ARGB_8888_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); + GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "RGB_565", "Landroid/graphics/Bitmap$Config;"); + m_RGB_565_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); + + FIND_AND_CHECK_CLASS("android/graphics/drawable/BitmapDrawable"); + m_bitmapDrawableClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + GET_AND_CHECK_METHOD(m_bitmapDrawableConstructorMethodID, + m_bitmapDrawableClass, + "<init>", "(Landroid/content/res/Resources;Landroid/graphics/Bitmap;)V"); + + FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtActivityBase"); + m_qtActivityClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtServiceBase"); + m_qtServiceClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + + return true; } QT_END_NAMESPACE @@ -888,36 +865,30 @@ Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void */*reserved*/) initialized = true; QT_USE_NAMESPACE - typedef union { - JNIEnv *nativeEnvironment; - void *venv; - } UnionJNIEnvToVoid; - - UnionJNIEnvToVoid uenv; - uenv.venv = nullptr; - m_javaVM = nullptr; - - if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) { - __android_log_print(ANDROID_LOG_FATAL, "Qt", "GetEnv failed"); + m_javaVM = vm; + QJniEnvironment env; + if (!env.isValid()) { + m_javaVM = nullptr; + __android_log_print(ANDROID_LOG_FATAL, "Qt", "Failed to initialize the JNI Environment"); return -1; } - JNIEnv *env = uenv.nativeEnvironment; if (!registerNatives(env) || !QtAndroidInput::registerNatives(env) || !QtAndroidMenu::registerNatives(env) || !QtAndroidAccessibility::registerNatives(env) - || !QtAndroidDialogHelpers::registerNatives(env)) { + || !QtAndroidDialogHelpers::registerNatives(env) + || !QAndroidPlatformClipboard::registerNatives(env) + || !QAndroidPlatformWindow::registerNatives(env) + || !QtAndroidWindowEmbedding::registerNatives(env) + || !AndroidBackendRegister::registerNatives()) { __android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed"); return -1; } QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); - m_javaVM = vm; // attach qt main thread data to this thread - QObject threadSetter; - if (threadSetter.thread()) - threadSetter.thread()->setObjectName("QtMainLoopThread"); + QThread::currentThread()->setObjectName("QtMainLoopThread"); __android_log_print(ANDROID_LOG_INFO, "Qt", "qt started"); return JNI_VERSION_1_6; } diff --git a/src/plugins/platforms/android/androidjnimain.h b/src/plugins/platforms/android/androidjnimain.h index 8d05e31f66..9d616b18fb 100644 --- a/src/plugins/platforms/android/androidjnimain.h +++ b/src/plugins/platforms/android/androidjnimain.h @@ -12,6 +12,8 @@ #include <QImage> #include <private/qjnihelpers_p.h> +#include <QtCore/QJniObject> +#include <androidbackendregister.h> QT_BEGIN_NAMESPACE @@ -22,26 +24,23 @@ class QAndroidPlatformIntegration; class QWidget; class QString; class QWindow; -class AndroidSurfaceClient; +class QAndroidPlatformWindow; class QBasicMutex; +Q_DECLARE_JNI_CLASS(QtActivityDelegateBase, "org/qtproject/qt/android/QtActivityDelegateBase") +Q_DECLARE_JNI_CLASS(QtInputDelegate, "org/qtproject/qt/android/QtInputDelegate") + namespace QtAndroid { QBasicMutex *platformInterfaceMutex(); QAndroidPlatformIntegration *androidPlatformIntegration(); + AndroidBackendRegister *backendRegister(); void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration); void setQtThread(QThread *thread); - - - int createSurface(AndroidSurfaceClient * client, const QRect &geometry, bool onTop, int imageDepth); - int insertNativeView(jobject view, const QRect &geometry); void setViewVisibility(jobject view, bool visible); - void setSurfaceGeometry(int surfaceId, const QRect &geometry); - void destroySurface(int surfaceId); - void bringChildToFront(int surfaceId); - void bringChildToBack(int surfaceId); QWindow *topLevelWindowAt(const QPoint &globalPos); + QWindow *windowFromId(int windowId); int availableWidthPixels(); int availableHeightPixels(); double scaledDensity(); @@ -50,8 +49,6 @@ namespace QtAndroid jobject assets(); AAssetManager *assetManager(); jclass applicationClass(); - QtJniTypes::Activity activity(); - QtJniTypes::Service service(); // Keep synchronized with flags in ActivityDelegate.java enum SystemUiVisibility { @@ -65,11 +62,14 @@ namespace QtAndroid jobject createBitmap(int width, int height, QImage::Format format, JNIEnv *env); jobject createBitmapDrawable(jobject bitmap, JNIEnv *env = nullptr); + void initializeAccessibility(); void notifyAccessibilityLocationChange(uint accessibilityObjectId); void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId); + void notifyObjectShow(uint parentObjectId); void notifyObjectFocus(uint accessibilityObjectId); void notifyValueChanged(uint accessibilityObjectId, jstring value); - void notifyQtAndroidPluginRunning(bool running); + void notifyScrolledEvent(uint accessibilityObjectId); + void notifyNativePluginIntegrationReady(bool ready); const char *classErrorMsgFmt(); const char *methodErrorMsgFmt(); @@ -77,6 +77,8 @@ namespace QtAndroid QString deviceName(); bool blockEventLoopsWhenSuspended(); + + bool isQtApplication(); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjnimenu.cpp b/src/plugins/platforms/android/androidjnimenu.cpp index 7b0091d277..5f8a0b92e9 100644 --- a/src/plugins/platforms/android/androidjnimenu.cpp +++ b/src/plugins/platforms/android/androidjnimenu.cpp @@ -20,18 +20,18 @@ QT_BEGIN_NAMESPACE using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtMenuInterface, "org/qtproject/qt/android/QtMenuInterface"); + namespace QtAndroidMenu { static QList<QAndroidPlatformMenu *> pendingContextMenus; static QAndroidPlatformMenu *visibleMenu = nullptr; - static QRecursiveMutex visibleMenuMutex; + Q_CONSTINIT static QRecursiveMutex visibleMenuMutex; static QSet<QAndroidPlatformMenuBar *> menuBars; static QAndroidPlatformMenuBar *visibleMenuBar = nullptr; static QWindow *activeTopLevelWindow = nullptr; - static QRecursiveMutex menuBarMutex; - - static jmethodID openContextMenuMethodID = 0; + Q_CONSTINIT static QRecursiveMutex menuBarMutex; static jmethodID clearMenuMethodID = 0; static jmethodID addMenuItemMethodID = 0; @@ -46,29 +46,35 @@ namespace QtAndroidMenu void resetMenuBar() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetOptionsMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("resetOptionsMenu"); } void openOptionsMenu() { - QJniObject::callStaticMethod<void>(applicationClass(), "openOptionsMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("openOptionsMenu"); } - void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env) + void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu) pendingContextMenus.append(visibleMenu); visibleMenu = menu; menu->aboutToShow(); - env->CallStaticVoidMethod(applicationClass(), openContextMenuMethodID, anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height()); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("openContextMenu", anchorRect.x(), + anchorRect.y(), anchorRect.width(), + anchorRect.height()); } void hideContextMenu(QAndroidPlatformMenu *menu) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu == menu) { - QJniObject::callStaticMethod<void>(applicationClass(), "closeContextMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("closeContextMenu"); pendingContextMenus.clear(); } else { pendingContextMenus.removeOne(menu); @@ -117,7 +123,7 @@ namespace QtAndroidMenu visibleMenuBar = 0; activeTopLevelWindow = window; - for (QAndroidPlatformMenuBar *menuBar : qAsConst(menuBars)) { + for (QAndroidPlatformMenuBar *menuBar : std::as_const(menuBars)) { if (menuBar->parentWindow() == window) { visibleMenuBar = menuBar; resetMenuBar(); @@ -211,8 +217,10 @@ namespace QtAndroidMenu return order; } - static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) + env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) @@ -249,8 +257,11 @@ namespace QtAndroidMenu return order ? JNI_TRUE : JNI_FALSE; } - static jboolean onOptionsItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) + static jboolean onOptionsItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { + Q_UNUSED(env) + Q_UNUSED(thiz) + QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) return JNI_FALSE; @@ -260,7 +271,7 @@ namespace QtAndroidMenu QAndroidPlatformMenuItem *item = static_cast<QAndroidPlatformMenuItem *>(menus.front()->menuItemForId(menuId)); if (item) { if (item->menu()) { - showContextMenu(item->menu(), QRect(), env); + showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); @@ -270,18 +281,23 @@ namespace QtAndroidMenu } else { QAndroidPlatformMenu *menu = static_cast<QAndroidPlatformMenu *>(visibleMenuBar->menuForId(menuId)); if (menu) - showContextMenu(menu, QRect(), env); + showContextMenu(menu, QRect()); } return JNI_TRUE; } - static void onOptionsMenuClosed(JNIEnv */*env*/, jobject /*thiz*/, jobject /*menu*/) + static void onOptionsMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(env) + Q_UNUSED(thiz) + Q_UNUSED(menu) } - static void onCreateContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static void onCreateContextMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) + env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) @@ -295,8 +311,9 @@ namespace QtAndroidMenu addAllMenuItemsToMenu(env, menu, visibleMenu); } - static void fillContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static void fillContextMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) @@ -305,20 +322,23 @@ namespace QtAndroidMenu addAllMenuItemsToMenu(env, menu, visibleMenu); } - static jboolean onContextItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) + static jboolean onContextItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { + Q_UNUSED(env) + Q_UNUSED(thiz) + QMutexLocker lock(&visibleMenuMutex); QAndroidPlatformMenuItem * item = static_cast<QAndroidPlatformMenuItem *>(visibleMenu->menuItemForId(menuId)); if (item) { if (item->menu()) { - showContextMenu(item->menu(), QRect(), env); + showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); item->activated(); visibleMenu->aboutToHide(); visibleMenu = 0; - for (QAndroidPlatformMenu *menu : qAsConst(pendingContextMenus)) { + for (QAndroidPlatformMenu *menu : std::as_const(pendingContextMenus)) { if (menu->isVisible()) menu->aboutToHide(); } @@ -328,8 +348,12 @@ namespace QtAndroidMenu return JNI_TRUE; } - static void onContextMenuClosed(JNIEnv *env, jobject /*thiz*/, jobject /*menu*/) + static void onContextMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(env) + Q_UNUSED(thiz) + Q_UNUSED(menu) + QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) return; @@ -337,7 +361,7 @@ namespace QtAndroidMenu visibleMenu->aboutToHide(); visibleMenu = 0; if (!pendingContextMenus.empty()) - showContextMenu(pendingContextMenus.takeLast(), QRect(), env); + showContextMenu(pendingContextMenus.takeLast(), QRect()); } static JNINativeMethod methods[] = { @@ -378,17 +402,15 @@ namespace QtAndroidMenu return false; \ } - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { jclass appClass = applicationClass(); - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(appClass, methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } - GET_AND_CHECK_STATIC_METHOD(openContextMenuMethodID, appClass, "openContextMenu", "(IIII)V"); - jclass clazz; FIND_AND_CHECK_CLASS("android/view/Menu"); GET_AND_CHECK_METHOD(clearMenuMethodID, clazz, "clear", "()V"); diff --git a/src/plugins/platforms/android/androidjnimenu.h b/src/plugins/platforms/android/androidjnimenu.h index 1645ce750b..e10ad930d9 100644 --- a/src/plugins/platforms/android/androidjnimenu.h +++ b/src/plugins/platforms/android/androidjnimenu.h @@ -15,12 +15,13 @@ class QAndroidPlatformMenuItem; class QWindow; class QRect; class QPoint; +class QJniEnvironment; namespace QtAndroidMenu { // Menu support void openOptionsMenu(); - void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env); + void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect); void hideContextMenu(QAndroidPlatformMenu *menu); void syncMenu(QAndroidPlatformMenu *menu); void androidPlatformMenuDestroyed(QAndroidPlatformMenu *menu); @@ -31,7 +32,7 @@ namespace QtAndroidMenu void removeMenuBar(QAndroidPlatformMenuBar *menuBar); // Menu support - bool registerNatives(JNIEnv *env); + bool registerNatives(QJniEnvironment &env); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidsurfaceclient.h b/src/plugins/platforms/android/androidsurfaceclient.h deleted file mode 100644 index dded9a1f66..0000000000 --- a/src/plugins/platforms/android/androidsurfaceclient.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef ANDROIDSURFACECLIENT_H -#define ANDROIDSURFACECLIENT_H -#include <QMutex> -#include <jni.h> - -QT_BEGIN_NAMESPACE - -class AndroidSurfaceClient -{ -public: - virtual void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) = 0; - void lockSurface() { m_surfaceMutex.lock(); } - void unlockSurface() { m_surfaceMutex.unlock(); } - -protected: - QMutex m_surfaceMutex; -}; - -QT_END_NAMESPACE - -#endif // ANDROIDSURFACECLIENT_H diff --git a/src/plugins/platforms/android/androidwindowembedding.cpp b/src/plugins/platforms/android/androidwindowembedding.cpp new file mode 100644 index 0000000000..65dabcac66 --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "androidwindowembedding.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtGui/qwindow.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(QtView, "org/qtproject/qt/android/QtView"); + +namespace QtAndroidWindowEmbedding { + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView, + jint x, jint y, jint width, jint height) + { + // QWindow should be constructed on the Qt thread rather than directly in the caller thread + // To avoid hitting checkReceiverThread assert in QCoreApplication::doNotify + QMetaObject::invokeMethod(qApp, [rootView, x, y, width, height] { + QWindow *parentWindow = QWindow::fromWinId(reinterpret_cast<WId>(rootView.object())); + parentWindow->setGeometry(x, y, width, height); + rootView.callMethod<void>("createWindow", reinterpret_cast<jlong>(parentWindow)); + }); + } + + void deleteWindow(JNIEnv *, jclass, jlong windowRef) + { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + window->deleteLater(); + } + + void setWindowVisible(JNIEnv *, jclass, jlong windowRef, jboolean visible) + { + QMetaObject::invokeMethod(qApp, [windowRef, visible] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + if (visible) { + window->showNormal(); + if (!window->parent()->isVisible()) + window->parent()->showNormal(); + } else { + window->hide(); + } + }); + } + + void resizeWindow(JNIEnv *, jclass, jlong windowRef, jint x, jint y, jint width, jint height) + { + QMetaObject::invokeMethod(qApp, [windowRef, x, y, width, height] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + QWindow *parent = window->parent(); + if (parent) + parent->setGeometry(x, y, width, height); + window->setGeometry(0, 0, width, height); + }); + } + + bool registerNatives(QJniEnvironment& env) { + return env.registerNativeMethods( + QtJniTypes::Traits<QtJniTypes::QtView>::className(), + { Q_JNI_NATIVE_SCOPED_METHOD(createRootWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(deleteWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(setWindowVisible, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(resizeWindow, QtAndroidWindowEmbedding) }); + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidwindowembedding.h b/src/plugins/platforms/android/androidwindowembedding.h new file mode 100644 index 0000000000..b7b0e1205f --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.h @@ -0,0 +1,41 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTANDROIDWINDOWEMBEDDING_H +#define QTANDROIDWINDOWEMBEDDING_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(View, "android/view/View"); + +namespace QtAndroidWindowEmbedding +{ + bool registerNatives(QJniEnvironment& env); + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView, + jint x, jint y,jint width, jint height); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(createRootWindow) + void deleteWindow(JNIEnv *, jclass, jlong window); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(deleteWindow) + void setWindowVisible(JNIEnv *, jclass, jlong window, jboolean visible); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setWindowVisible) + void resizeWindow(JNIEnv *, jclass, jlong windowRef, jint x, jint y, jint width, jint height); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(resizeWindow) +}; + +QT_END_NAMESPACE + +#endif // QTANDROIDWINDOWEMBEDDING_H diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 36fa2dd945..4ea6536cef 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -108,6 +108,8 @@ public: FolderIterator(const QString &path) : m_path(path) { + // Note that empty dirs in the assets dir before the build are not going to be + // included in the final apk, so no empty folders should expected to be listed. QJniObject files = QJniObject::callStaticObjectMethod(QtAndroid::applicationClass(), "listAssetContent", "(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;", @@ -140,17 +142,13 @@ public: return m_path + at(m_index).name; } - bool hasNext() const + bool advance() { - return !empty() && m_index + 1 < size(); - } - - std::optional<std::pair<QString, AssetItem>> next() - { - if (!hasNext()) - return {}; - ++m_index; - return std::pair<QString, AssetItem>(currentFileName(), at(m_index)); + if (!empty() && m_index + 1 < size()) { + ++m_index; + return true; + } + return false; } private: @@ -161,7 +159,7 @@ private: }; QCache<QString, QSharedPointer<FolderIterator>> FolderIterator::m_assetsCache(std::max(50, qEnvironmentVariableIntValue("QT_ANDROID_MAX_ASSETS_CACHE_SIZE"))); -QMutex FolderIterator::m_assetsCacheMutex; +Q_CONSTINIT QMutex FolderIterator::m_assetsCacheMutex; class AndroidAbstractFileEngineIterator: public QAbstractFileEngineIterator { @@ -169,7 +167,7 @@ public: AndroidAbstractFileEngineIterator(QDir::Filters filters, const QStringList &nameFilters, const QString &path) - : QAbstractFileEngineIterator(filters, nameFilters) + : QAbstractFileEngineIterator(path, filters, nameFilters) { m_currentIterator = FolderIterator::fromCache(cleanedAssetPath(path), true); } @@ -186,28 +184,16 @@ public: return m_currentIterator->currentFileName(); } - virtual QString currentFilePath() const + QString currentFilePath() const override { if (!m_currentIterator) return {}; return m_currentIterator->currentFilePath(); } - bool hasNext() const override - { - if (!m_currentIterator) - return false; - return m_currentIterator->hasNext(); - } - - QString next() override + bool advance() override { - if (!m_currentIterator) - return {}; - auto res = m_currentIterator->next(); - if (!res) - return {}; - return res->first; + return m_currentIterator ? m_currentIterator->advance() : false; } private: @@ -305,9 +291,9 @@ public: return prefixedPath(m_fileName); case BaseName: if ((pos = m_fileName.lastIndexOf(u'/')) != -1) - return prefixedPath(m_fileName.mid(pos)); + return m_fileName.mid(pos + 1); else - return prefixedPath(m_fileName); + return m_fileName; case PathName: case AbsolutePathName: case CanonicalPathName: @@ -350,8 +336,13 @@ public: } else { auto *assetDir = AAssetManager_openDir(m_assetManager, m_fileName.toUtf8()); if (assetDir) { - if (AAssetDir_getNextFileName(assetDir)) + if (AAssetDir_getNextFileName(assetDir) + || (!FolderIterator::fromCache(m_fileName, false)->empty())) { + // If AAssetDir_getNextFileName is not valid, it still can be a directory that + // contains only other directories (no files). FolderIterator will not be called + // on the directory containing files so it should not be too time consuming now. m_assetInfo->type = AssetItem::Type::Folder; + } AAssetDir_close(assetDir); } } @@ -360,10 +351,12 @@ public: m_assetsInfoCache.insert(m_fileName, newAssetInfoPtr); } - Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override + IteratorUniquePtr + beginEntryList(const QString &, QDir::Filters filters, const QStringList &filterNames) override { + // AndroidAbstractFileEngineIterator use `m_fileName` as the path if (m_assetInfo && m_assetInfo->type == AssetItem::Type::Folder) - return new AndroidAbstractFileEngineIterator(filters, filterNames, m_fileName); + return std::make_unique<AndroidAbstractFileEngineIterator>(filters, filterNames, m_fileName); return nullptr; } @@ -379,20 +372,21 @@ private: }; QCache<QString, QSharedPointer<AssetItem>> AndroidAbstractFileEngine::m_assetsInfoCache(std::max(200, qEnvironmentVariableIntValue("QT_ANDROID_MAX_FILEINFO_ASSETS_CACHE_SIZE"))); -QMutex AndroidAbstractFileEngine::m_assetsInfoCacheMutex; +Q_CONSTINIT QMutex AndroidAbstractFileEngine::m_assetsInfoCacheMutex; AndroidAssetsFileEngineHandler::AndroidAssetsFileEngineHandler() { m_assetManager = QtAndroid::assetManager(); } -QAbstractFileEngine * AndroidAssetsFileEngineHandler::create(const QString &fileName) const +std::unique_ptr<QAbstractFileEngine> +AndroidAssetsFileEngineHandler::create(const QString &fileName) const { if (fileName.isEmpty()) - return nullptr; + return {}; if (!fileName.startsWith(assetsPrefix)) - return nullptr; + return {}; QString path = fileName.mid(prefixSize); path.replace("//"_L1, "/"_L1); @@ -400,7 +394,7 @@ QAbstractFileEngine * AndroidAssetsFileEngineHandler::create(const QString &file path.remove(0, 1); if (path.endsWith(u'/')) path.chop(1); - return new AndroidAbstractFileEngine(m_assetManager, path); + return std::make_unique<AndroidAbstractFileEngine>(m_assetManager, path); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.h b/src/plugins/platforms/android/qandroidassetsfileenginehandler.h index 50c6914c24..973a61fbfa 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.h +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.h @@ -15,9 +15,10 @@ QT_BEGIN_NAMESPACE class AndroidAssetsFileEngineHandler: public QAbstractFileEngineHandler { + Q_DISABLE_COPY_MOVE(AndroidAssetsFileEngineHandler) public: AndroidAssetsFileEngineHandler(); - QAbstractFileEngine *create(const QString &fileName) const override; + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; private: AAssetManager *m_assetManager; diff --git a/src/plugins/platforms/android/qandroideventdispatcher.cpp b/src/plugins/platforms/android/qandroideventdispatcher.cpp index 238addee58..8d1a085844 100644 --- a/src/plugins/platforms/android/qandroideventdispatcher.cpp +++ b/src/plugins/platforms/android/qandroideventdispatcher.cpp @@ -65,7 +65,7 @@ bool QAndroidEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags QAndroidEventDispatcherStopper *QAndroidEventDispatcherStopper::instance() { - static QAndroidEventDispatcherStopper androidEventDispatcherStopper; + Q_CONSTINIT static QAndroidEventDispatcherStopper androidEventDispatcherStopper; return &androidEventDispatcherStopper; } @@ -75,7 +75,7 @@ void QAndroidEventDispatcherStopper::startAll() if (!m_started.testAndSetOrdered(0, 1)) return; - for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers)) + for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers)) d->start(); } @@ -85,7 +85,7 @@ void QAndroidEventDispatcherStopper::stopAll() if (!m_started.testAndSetOrdered(1, 0)) return; - for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers)) + for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers)) d->stop(); } @@ -104,6 +104,6 @@ void QAndroidEventDispatcherStopper::removeEventDispatcher(QAndroidEventDispatch void QAndroidEventDispatcherStopper::goingToStop(bool stop) { QMutexLocker lock(&m_mutex); - for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers)) + for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers)) d->goingToStop(stop); } diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 679c142289..62212ff63d 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -80,9 +80,7 @@ static jboolean beginBatchEdit(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ BEGINBATCH"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ BEGINBATCH"; jboolean res = JNI_FALSE; runOnQtThread([&res]{res = m_androidInputContext->beginBatchEdit();}); return res; @@ -93,9 +91,7 @@ static jboolean endBatchEdit(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ ENDBATCH"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ ENDBATCH"; jboolean res = JNI_FALSE; runOnQtThread([&res]{res = m_androidInputContext->endBatchEdit();}); @@ -113,9 +109,7 @@ static jboolean commitText(JNIEnv *env, jobject /*thiz*/, jstring text, jint new QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text)); env->ReleaseStringChars(text, jstr); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ COMMIT" << str << newCursorPosition; -#endif + qCDebug(lcQpaInputMethods) << "@@@ COMMIT" << str << newCursorPosition; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->commitText(str, newCursorPosition);}); return res; @@ -126,9 +120,7 @@ static jboolean deleteSurroundingText(JNIEnv */*env*/, jobject /*thiz*/, jint le if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ DELETE" << leftLength << rightLength; -#endif + qCDebug(lcQpaInputMethods) << "@@@ DELETE" << leftLength << rightLength; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->deleteSurroundingText(leftLength, rightLength);}); return res; @@ -139,9 +131,7 @@ static jboolean finishComposingText(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ FINISH"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ FINISH"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->finishComposingText();}); return res; @@ -165,9 +155,7 @@ static jobject getExtractedText(JNIEnv *env, jobject /*thiz*/, int hintMaxChars, QAndroidInputContext::ExtractedText extractedText; runOnQtThread([&]{extractedText = m_androidInputContext->getExtractedText(hintMaxChars, hintMaxLines, flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETEX" << hintMaxChars << hintMaxLines << QString::fromLatin1("0x") + QString::number(flags,16) << extractedText.text << "partOff:" << extractedText.partialStartOffset << extractedText.partialEndOffset << "sel:" << extractedText.selectionStart << extractedText.selectionEnd << "offset:" << extractedText.startOffset; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETEX" << hintMaxChars << hintMaxLines << QString::fromLatin1("0x") + QString::number(flags,16) << extractedText.text << "partOff:" << extractedText.partialStartOffset << extractedText.partialEndOffset << "sel:" << extractedText.selectionStart << extractedText.selectionEnd << "offset:" << extractedText.startOffset; jobject object = env->NewObject(m_extractedTextClass, m_classConstructorMethodID); env->SetIntField(object, m_partialStartOffsetFieldID, extractedText.partialStartOffset); @@ -190,9 +178,7 @@ static jstring getSelectedText(JNIEnv *env, jobject /*thiz*/, jint flags) QString text; runOnQtThread([&]{text = m_androidInputContext->getSelectedText(flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETSEL" << text; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETSEL" << text; if (text.isEmpty()) return 0; return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length())); @@ -205,9 +191,7 @@ static jstring getTextAfterCursor(JNIEnv *env, jobject /*thiz*/, jint length, ji QString text; runOnQtThread([&]{text = m_androidInputContext->getTextAfterCursor(length, flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETA" << length << text; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETA" << length << text; return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length())); } @@ -218,9 +202,7 @@ static jstring getTextBeforeCursor(JNIEnv *env, jobject /*thiz*/, jint length, j QString text; runOnQtThread([&]{text = m_androidInputContext->getTextBeforeCursor(length, flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETB" << length << text; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETB" << length << text; return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length())); } @@ -234,9 +216,7 @@ static jboolean setComposingText(JNIEnv *env, jobject /*thiz*/, jstring text, ji QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text)); env->ReleaseStringChars(text, jstr); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SET" << str << newCursorPosition; -#endif + qCDebug(lcQpaInputMethods) << "@@@ SET" << str << newCursorPosition; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->setComposingText(str, newCursorPosition);}); return res; @@ -247,9 +227,7 @@ static jboolean setComposingRegion(JNIEnv */*env*/, jobject /*thiz*/, jint start if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SETR" << start << end; -#endif + qCDebug(lcQpaInputMethods) << "@@@ SETR" << start << end; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->setComposingRegion(start, end);}); return res; @@ -261,9 +239,7 @@ static jboolean setSelection(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SETSEL" << start << end; -#endif + qCDebug(lcQpaInputMethods) << "@@@ SETSEL" << start << end; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->setSelection(start, end);}); return res; @@ -275,9 +251,7 @@ static jboolean selectAll(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ SELALL"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ SELALL"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->selectAll();}); return res; @@ -288,9 +262,7 @@ static jboolean cut(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@"); -#endif + qCDebug(lcQpaInputMethods) << "@@@"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->cut();}); return res; @@ -301,9 +273,7 @@ static jboolean copy(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@"); -#endif + qCDebug(lcQpaInputMethods) << "@@@"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->copy();}); return res; @@ -314,9 +284,7 @@ static jboolean copyURL(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@"); -#endif + qCDebug(lcQpaInputMethods) << "@@@"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->copyURL();}); return res; @@ -327,9 +295,7 @@ static jboolean paste(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ PASTE"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ PASTE"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->paste();}); return res; @@ -340,14 +306,24 @@ static jboolean updateCursorPosition(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ UPDATECURSORPOS"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ UPDATECURSORPOS"; runOnQtThread([&]{m_androidInputContext->updateCursorPosition();}); return true; } +static void reportFullscreenMode(JNIEnv */*env*/, jobject /*thiz*/, jboolean enabled) +{ + if (!m_androidInputContext) + return; + + runOnQtThread([&]{m_androidInputContext->reportFullscreenMode(enabled);}); +} + +static jboolean fullscreenMode(JNIEnv */*env*/, jobject /*thiz*/) +{ + return m_androidInputContext ? m_androidInputContext->fullscreenMode() : false; +} static JNINativeMethod methods[] = { {"beginBatchEdit", "()Z", (void *)beginBatchEdit}, @@ -368,7 +344,9 @@ static JNINativeMethod methods[] = { {"copy", "()Z", (void *)copy}, {"copyURL", "()Z", (void *)copyURL}, {"paste", "()Z", (void *)paste}, - {"updateCursorPosition", "()Z", (void *)updateCursorPosition} + {"updateCursorPosition", "()Z", (void *)updateCursorPosition}, + {"reportFullscreenMode", "(Z)V", (void *)reportFullscreenMode}, + {"fullscreenMode", "()Z", (void *)fullscreenMode} }; static QRect screenInputItemRectangle() @@ -385,6 +363,7 @@ QAndroidInputContext::QAndroidInputContext() , m_handleMode(Hidden) , m_batchEditNestingLevel(0) , m_focusObject(0) + , m_fullScreenMode(false) { QJniEnvironment env; jclass clazz = env.findClass(QtNativeInputConnectionClassName); @@ -567,12 +546,29 @@ void QAndroidInputContext::updateCursorPosition() } } +bool QAndroidInputContext::isImhNoTextHandlesSet() +{ + QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(); + if (query.isNull()) + return false; + return query->value(Qt::ImHints).toUInt() & Qt::ImhNoTextHandles; +} + void QAndroidInputContext::updateSelectionHandles() { + if (m_fullScreenMode) { + QtAndroidInput::updateHandles(Hidden); + return; + } static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES"); if (noHandles || !m_focusObject) return; + if (isImhNoTextHandlesSet()) { + QtAndroidInput::updateHandles(Hidden); + return; + } + auto im = qGuiApp->inputMethod(); QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImEnabled @@ -586,6 +582,11 @@ void QAndroidInputContext::updateSelectionHandles() bool readOnly = readOnlyVariant.toBool(); QPlatformWindow *qPlatformWindow = qGuiApp->focusWindow()->handle(); + if (!readOnly && ((m_handleMode & 0xff) == Hidden)) { + QtAndroidInput::updateHandles(Hidden); + return; + } + if ( cpos == anchor && (!readOnlyVariant.isValid() || readOnly)) { QtAndroidInput::updateHandles(Hidden); return; @@ -612,9 +613,8 @@ void QAndroidInputContext::updateSelectionHandles() if (!query.value(Qt::ImSurroundingText).toString().isEmpty()) buttons |= EditContext::SelectAllButton; QtAndroidInput::updateHandles(m_handleMode, editMenuPoint, buttons, cursorPointGlobal); - // The VK is hidden, reset the timer - if (m_hideCursorHandleTimer.isActive()) - m_hideCursorHandleTimer.start(); + m_hideCursorHandleTimer.start(); + return; } @@ -788,7 +788,15 @@ void QAndroidInputContext::touchDown(int x, int y) focusObjectStopComposing(); } - updateSelectionHandles(); + // Check if cursor is visible in focused window before updating handles + QPlatformWindow *window = qGuiApp->focusWindow()->handle(); + const QRectF curRect = cursorRectangle(); + const QPoint cursorGlobalPoint = window->mapToGlobal(QPoint(curRect.x(), curRect.y())); + const QRect windowRect = QPlatformInputContext::inputItemClipRectangle().toRect(); + const QRect windowGlobalRect = QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size()); + + if (windowGlobalRect.contains(cursorGlobalPoint.x(), cursorGlobalPoint.y())) + updateSelectionHandles(); } } @@ -895,10 +903,15 @@ void QAndroidInputContext::showInputPanel() if (query.isNull()) return; + if (!qGuiApp->focusWindow()->handle()) + return; // not a real window, probably VR/XR + disconnect(m_updateCursorPosConnection); + m_updateCursorPosConnection = {}; + if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition())); - else + else if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged()") >= 0) m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition())); QRect rect = screenInputItemRectangle(); @@ -1110,6 +1123,25 @@ jboolean QAndroidInputContext::finishComposingText() return JNI_TRUE; } +void QAndroidInputContext::reportFullscreenMode(jboolean enabled) +{ + m_fullScreenMode = enabled; + BatchEditLock batchEditLock(this); + if (!focusObjectStopComposing()) + return; + + if (enabled) + m_handleMode = Hidden; + + updateSelectionHandles(); +} + +// Called in calling thread's context +jboolean QAndroidInputContext::fullscreenMode() +{ + return m_fullScreenMode; +} + bool QAndroidInputContext::focusObjectIsComposing() const { return m_composingCursor != -1; @@ -1532,9 +1564,7 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end) } if (start < textOffset || end - textOffset > text.length()) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qWarning("setComposingRegion: failed to retrieve text from composing region"); -#endif + qCDebug(lcQpaInputMethods) << "Warning: setComposingRegion: failed to retrieve text from composing region"; return JNI_TRUE; } diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h index c93aae142a..038286c4b8 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.h +++ b/src/plugins/platforms/android/qandroidinputcontext.h @@ -9,6 +9,8 @@ #include <functional> #include <jni.h> #include <qevent.h> + +#include <QtCore/qpointer.h> #include <QTimer> QT_BEGIN_NAMESPACE @@ -98,6 +100,8 @@ public: jboolean copy(); jboolean copyURL(); jboolean paste(); + void reportFullscreenMode(jboolean enabled); + jboolean fullscreenMode(); public slots: void safeCall(const std::function<void()> &func, Qt::ConnectionType conType = Qt::BlockingQueuedConnection); @@ -113,6 +117,7 @@ private slots: void showInputPanelLater(Qt::ApplicationState); private: + bool isImhNoTextHandlesSet(); void sendInputMethodEvent(QInputMethodEvent *event); QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll); bool focusObjectIsComposing() const; @@ -129,6 +134,7 @@ private: int m_batchEditNestingLevel; QPointer<QObject> m_focusObject; QTimer m_hideCursorHandleTimer; + bool m_fullScreenMode; }; Q_DECLARE_OPERATORS_FOR_FLAGS(QAndroidInputContext::HandleModes) QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp index 33eafd54aa..ea7f22295d 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp @@ -28,10 +28,14 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent * QtAndroidAccessibility::notifyLocationChange(event->uniqueId()); } else if (event->type() == QAccessible::ObjectHide) { QtAndroidAccessibility::notifyObjectHide(event->uniqueId()); + } else if (event->type() == QAccessible::ObjectShow) { + QtAndroidAccessibility::notifyObjectShow(event->uniqueId()); } else if (event->type() == QAccessible::Focus) { QtAndroidAccessibility::notifyObjectFocus(event->uniqueId()); } else if (event->type() == QAccessible::ValueChanged) { QtAndroidAccessibility::notifyValueChanged(event->uniqueId()); + } else if (event->type() == QAccessible::ScrollingEnd) { + QtAndroidAccessibility::notifyScrolledEvent(event->uniqueId()); } } diff --git a/src/plugins/platforms/android/qandroidplatformbackingstore.cpp b/src/plugins/platforms/android/qandroidplatformbackingstore.cpp deleted file mode 100644 index 07a1c835d3..0000000000 --- a/src/plugins/platforms/android/qandroidplatformbackingstore.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qandroidplatformbackingstore.h" -#include "qandroidplatformscreen.h" -#include "qandroidplatformwindow.h" -#include <qpa/qplatformscreen.h> - -QT_BEGIN_NAMESPACE - -QAndroidPlatformBackingStore::QAndroidPlatformBackingStore(QWindow *window) - : QPlatformBackingStore(window) -{ - if (window->handle()) - setBackingStore(window); -} - -QPaintDevice *QAndroidPlatformBackingStore::paintDevice() -{ - return &m_image; -} - -void QAndroidPlatformBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) -{ - Q_UNUSED(offset); - - if (!m_backingStoreSet) - setBackingStore(window); - - (static_cast<QAndroidPlatformWindow *>(window->handle()))->repaint(region); -} - -void QAndroidPlatformBackingStore::resize(const QSize &size, const QRegion &staticContents) -{ - Q_UNUSED(staticContents); - - if (m_image.size() != size) - m_image = QImage(size, window()->screen()->handle()->format()); -} - -void QAndroidPlatformBackingStore::setBackingStore(QWindow *window) -{ - if (window->surfaceType() == QSurface::RasterSurface || window->surfaceType() == QSurface::RasterGLSurface) { - (static_cast<QAndroidPlatformWindow *>(window->handle()))->setBackingStore(this); - m_backingStoreSet = true; - } else { - qWarning("QAndroidPlatformBackingStore does not support OpenGL-only windows."); - } -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformbackingstore.h b/src/plugins/platforms/android/qandroidplatformbackingstore.h deleted file mode 100644 index 810305ac45..0000000000 --- a/src/plugins/platforms/android/qandroidplatformbackingstore.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QANDROIDPLATFORMBACKINGSTORE_H -#define QANDROIDPLATFORMBACKINGSTORE_H - -#include <qpa/qplatformbackingstore.h> -#include <qpa/qwindowsysteminterface.h> - -QT_BEGIN_NAMESPACE - -class QAndroidPlatformBackingStore : public QPlatformBackingStore -{ -public: - explicit QAndroidPlatformBackingStore(QWindow *window); - QPaintDevice *paintDevice() override; - void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; - void resize(const QSize &size, const QRegion &staticContents) override; - QImage toImage() const override { return m_image; } - void setBackingStore(QWindow *window); -protected: - QImage m_image; - bool m_backingStoreSet = false; -}; - -QT_END_NAMESPACE - -#endif // QANDROIDPLATFORMBACKINGSTORE_H diff --git a/src/plugins/platforms/android/qandroidplatformclipboard.cpp b/src/plugins/platforms/android/qandroidplatformclipboard.cpp index 39a508a1b3..e5ed33b9b0 100644 --- a/src/plugins/platforms/android/qandroidplatformclipboard.cpp +++ b/src/plugins/platforms/android/qandroidplatformclipboard.cpp @@ -1,15 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qandroidplatformclipboard.h" -#include "androidjniclipboard.h" + +#include <QtCore/QUrl> +#include <QtCore/QJniEnvironment> +#include <QtCore/QJniObject> +#include <QtCore/private/qjnihelpers_p.h> + #ifndef QT_NO_CLIPBOARD +using namespace QtJniTypes; + QT_BEGIN_NAMESPACE +void QAndroidPlatformClipboard::onClipboardDataChanged(JNIEnv *env, jobject obj, jlong nativePointer) +{ + Q_UNUSED(env) + Q_UNUSED(obj) + + auto *clipboardManager = reinterpret_cast<QAndroidPlatformClipboard *>(nativePointer); + if (clipboardManager) + clipboardManager->emitChanged(QClipboard::Clipboard); +} + QAndroidPlatformClipboard::QAndroidPlatformClipboard() { - QtAndroidClipboard::setClipboardManager(this); + m_clipboardManager = QtClipboardManager::construct(QtAndroidPrivate::context(), + reinterpret_cast<jlong>(this)); } QAndroidPlatformClipboard::~QAndroidPlatformClipboard() @@ -18,24 +37,66 @@ QAndroidPlatformClipboard::~QAndroidPlatformClipboard() delete data; } +QMimeData *QAndroidPlatformClipboard::getClipboardMimeData() +{ + QMimeData *data = new QMimeData; + if (m_clipboardManager.callMethod<jboolean>("hasClipboardText")) { + data->setText(m_clipboardManager.callMethod<QString>("getClipboardText")); + } + if (m_clipboardManager.callMethod<jboolean>("hasClipboardHtml")) { + data->setHtml(m_clipboardManager.callMethod<QString>("getClipboardHtml")); + } + if (m_clipboardManager.callMethod<jboolean>("hasClipboardUri")) { + auto uris = m_clipboardManager.callMethod<QString[]>("getClipboardUris"); + if (uris.isValid()) { + QList<QUrl> urls; + for (const QString &uri : uris) + urls << QUrl(uri); + data->setUrls(urls); + } + } + return data; +} + QMimeData *QAndroidPlatformClipboard::mimeData(QClipboard::Mode mode) { Q_UNUSED(mode); Q_ASSERT(supportsMode(mode)); if (data) data->deleteLater(); - data = QtAndroidClipboard::getClipboardMimeData(); + data = getClipboardMimeData(); return data; } +void QAndroidPlatformClipboard::clearClipboardData() +{ + m_clipboardManager.callMethod<void>("clearClipData"); +} + +void QAndroidPlatformClipboard::setClipboardMimeData(QMimeData *data) +{ + clearClipboardData(); + auto context = QtAndroidPrivate::context(); + if (data->hasUrls()) { + QList<QUrl> urls = data->urls(); + for (const auto &u : std::as_const(urls)) + m_clipboardManager.callMethod<void>("setClipboardUri", context, u.toEncoded()); + } else if (data->hasHtml()) { // html can contain text + m_clipboardManager.callMethod<void>("setClipboardHtml", + context, data->text(), data->html()); + } else if (data->hasText()) { // hasText must be the last (the order matter here) + m_clipboardManager.callMethod<void>("setClipboardText", context, data->text()); + } +} + void QAndroidPlatformClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) { if (!data) { - QtAndroidClipboard::clearClipboardData(); + clearClipboardData(); return; } if (data && supportsMode(mode)) - QtAndroidClipboard::setClipboardMimeData(data); + setClipboardMimeData(data); if (data != 0) data->deleteLater(); } @@ -45,6 +106,18 @@ bool QAndroidPlatformClipboard::supportsMode(QClipboard::Mode mode) const return QClipboard::Clipboard == mode; } +bool QAndroidPlatformClipboard::registerNatives(QJniEnvironment &env) +{ + bool success = env.registerNativeMethods(Traits<QtClipboardManager>::className(), + { Q_JNI_NATIVE_SCOPED_METHOD(onClipboardDataChanged, QAndroidPlatformClipboard) }); + if (!success) { + qCritical() << "QtClipboardManager: registerNativeMethods() failed"; + return false; + } + + return true; +} + QT_END_NAMESPACE #endif // QT_NO_CLIPBOARD diff --git a/src/plugins/platforms/android/qandroidplatformclipboard.h b/src/plugins/platforms/android/qandroidplatformclipboard.h index 1778ca5b28..ab5c527f88 100644 --- a/src/plugins/platforms/android/qandroidplatformclipboard.h +++ b/src/plugins/platforms/android/qandroidplatformclipboard.h @@ -1,3 +1,4 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -6,10 +7,14 @@ #include <qpa/qplatformclipboard.h> #include <QMimeData> +#include <QtCore/qjnitypes.h> #ifndef QT_NO_CLIPBOARD + QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(QtClipboardManager, "org/qtproject/qt/android/QtClipboardManager"); + class QAndroidPlatformClipboard : public QPlatformClipboard { public: @@ -18,8 +23,19 @@ public: QMimeData *mimeData(QClipboard::Mode mode = QClipboard::Clipboard) override; void setMimeData(QMimeData *data, QClipboard::Mode mode = QClipboard::Clipboard) override; bool supportsMode(QClipboard::Mode mode) const override; + + static bool registerNatives(QJniEnvironment &env); + private: + QMimeData *getClipboardMimeData(); + void setClipboardMimeData(QMimeData *data); + void clearClipboardData(); + + static void onClipboardDataChanged(JNIEnv *env, jobject obj, jlong nativePointer); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(onClipboardDataChanged) + QMimeData *data = nullptr; + QtJniTypes::QtClipboardManager m_clipboardManager = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp b/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp index 97ae5064bd..2b9a1194d2 100644 --- a/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp +++ b/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp @@ -19,7 +19,7 @@ static jclass g_messageDialogHelperClass = nullptr; QAndroidPlatformMessageDialogHelper::QAndroidPlatformMessageDialogHelper() : m_javaMessageDialog(g_messageDialogHelperClass, "(Landroid/app/Activity;)V", - QtAndroid::activity()) + QtAndroidPrivate::activity().object()) { } @@ -54,7 +54,10 @@ bool QAndroidPlatformMessageDialogHelper::show(Qt::WindowFlags windowFlags, if (!opt.data()) return false; - m_javaMessageDialog.callMethod<void>("setIcon", "(I)V", opt->icon()); + if (!opt->checkBoxLabel().isNull()) + return false; // Can't support + + m_javaMessageDialog.callMethod<void>("setStandardIcon", "(I)V", opt->standardIcon()); QString str = htmlText(opt->windowTitle()); if (!str.isEmpty()) { @@ -147,7 +150,7 @@ static void dialogResult(JNIEnv * /*env*/, jobject /*thiz*/, jlong handler, int QMetaObject::invokeMethod(object, "dialogResult", Qt::QueuedConnection, Q_ARG(int, buttonID)); } -static JNINativeMethod methods[] = { +static const JNINativeMethod methods[] = { {"dialogResult", "(JI)V", (void *)dialogResult} }; @@ -159,21 +162,19 @@ static JNINativeMethod methods[] = { return false; \ } -bool registerNatives(JNIEnv *env) +bool registerNatives(QJniEnvironment &env) { const char QtMessageHandlerHelperClassName[] = "org/qtproject/qt/android/QtMessageDialogHelper"; - QJniEnvironment qenv; - jclass clazz = qenv.findClass(QtMessageHandlerHelperClassName); + jclass clazz = env.findClass(QtMessageHandlerHelperClassName); if (!clazz) { __android_log_print(ANDROID_LOG_FATAL, QtAndroid::qtTagText(), QtAndroid::classErrorMsgFmt() , QtMessageHandlerHelperClassName); return false; } g_messageDialogHelperClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtNativeDialogHelper"); - jclass appClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods("org/qtproject/qt/android/QtNativeDialogHelper", + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL, "Qt", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/qandroidplatformdialoghelpers.h b/src/plugins/platforms/android/qandroidplatformdialoghelpers.h index f06d2a9990..86f1cf77e7 100644 --- a/src/plugins/platforms/android/qandroidplatformdialoghelpers.h +++ b/src/plugins/platforms/android/qandroidplatformdialoghelpers.h @@ -8,12 +8,13 @@ #include <jni.h> #include <QEventLoop> -#include <QtCore/QJniEnvironment> #include <QtCore/QJniObject> #include <qpa/qplatformdialoghelper.h> QT_BEGIN_NAMESPACE +class QJniEnvironment; + namespace QtAndroidDialogHelpers { class QAndroidPlatformMessageDialogHelper: public QPlatformMessageDialogHelper @@ -41,7 +42,7 @@ private: }; -bool registerNatives(JNIEnv *env); +bool registerNatives(QJniEnvironment &env); } diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp index 6d28bd2388..d8a5c58d2c 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp @@ -25,7 +25,7 @@ const char JniIntentClass[] = "android/content/Intent"; QAndroidPlatformFileDialogHelper::QAndroidPlatformFileDialogHelper() : QPlatformFileDialogHelper(), - m_activity(QtAndroid::activity()) + m_activity(QtAndroidPrivate::activity()) { } @@ -45,8 +45,8 @@ bool QAndroidPlatformFileDialogHelper::handleActivityResult(jint requestCode, ji if (uri.isValid()) { takePersistableUriPermission(uri); m_selectedFile.append(QUrl(uri.toString())); - Q_EMIT fileSelected(m_selectedFile.first()); - Q_EMIT currentChanged(m_selectedFile.first()); + Q_EMIT fileSelected(m_selectedFile.constFirst()); + Q_EMIT currentChanged(m_selectedFile.constFirst()); Q_EMIT accept(); return true; @@ -65,7 +65,7 @@ bool QAndroidPlatformFileDialogHelper::handleActivityResult(jint requestCode, ji m_selectedFile.append(itemUri.toString()); } Q_EMIT filesSelected(m_selectedFile); - Q_EMIT currentChanged(m_selectedFile.first()); + Q_EMIT currentChanged(m_selectedFile.constFirst()); Q_EMIT accept(); } @@ -97,6 +97,22 @@ void QAndroidPlatformFileDialogHelper::setInitialFileName(const QString &title) extraTitle.object(), QJniObject::fromString(title).object()); } +void QAndroidPlatformFileDialogHelper::setInitialDirectoryUri(const QString &directory) +{ + if (directory.isEmpty()) + return; + + if (QNativeInterface::QAndroidApplication::sdkVersion() < 26) + return; + + const auto extraInitialUri = QJniObject::getStaticObjectField( + "android/provider/DocumentsContract", "EXTRA_INITIAL_URI", "Ljava/lang/String;"); + m_intent.callObjectMethod("putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + extraInitialUri.object(), + QJniObject::fromString(directory).object()); +} + void QAndroidPlatformFileDialogHelper::setOpenableCategory() { const QJniObject CATEGORY_OPENABLE = QJniObject::getStaticObjectField( @@ -130,12 +146,15 @@ void QAndroidPlatformFileDialogHelper::setMimeTypes() { QStringList mimeTypes = options()->mimeTypeFilters(); const QStringList nameFilters = options()->nameFilters(); - const QString nameFilter = nameFilters.isEmpty() ? QString() : nameFilters.first(); - if (!nameFilter.isEmpty()) { + if (!nameFilters.isEmpty()) { QMimeDatabase db; - for (const QString &filter : nameFilterExtensions(nameFilter)) - mimeTypes.append(db.mimeTypeForFile(filter, QMimeDatabase::MatchExtension).name()); + for (auto filter : nameFilters) { + if (!filter.isEmpty()) { + for (const QString &filter : nameFilterExtensions(filter)) + mimeTypes.append(db.mimeTypeForFile(filter, QMimeDatabase::MatchExtension).name()); + } + } } const QString initialType = mimeTypes.size() == 1 ? mimeTypes.at(0) : "*/*"_L1; @@ -179,11 +198,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win if (options()->acceptMode() == QFileDialogOptions::AcceptSave) { m_intent = getFileDialogIntent("ACTION_CREATE_DOCUMENT"); const QList<QUrl> selectedFiles = options()->initiallySelectedFiles(); - if (selectedFiles.size() > 0) { - // TODO: The initial folder to show at the start should be handled by EXTRA_INITIAL_URI - // Take only the file name. + if (selectedFiles.size() > 0) setInitialFileName(selectedFiles.first().fileName()); - } } else if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) { switch (options()->fileMode()) { case QFileDialogOptions::FileMode::DirectoryOnly: @@ -207,6 +223,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win setMimeTypes(); } + setInitialDirectoryUri(m_directory.toString()); + QtAndroidPrivate::registerActivityResultListener(this); m_activity.callMethod<void>("startActivityForResult", "(Landroid/content/Intent;I)V", m_intent.object(), REQUEST_CODE); @@ -220,6 +238,11 @@ void QAndroidPlatformFileDialogHelper::hide() QtAndroidPrivate::unregisterActivityResultListener(this); } +void QAndroidPlatformFileDialogHelper::setDirectory(const QUrl &directory) +{ + m_directory = directory; +} + void QAndroidPlatformFileDialogHelper::exec() { m_eventLoop.exec(QEventLoop::DialogExec); diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h index 4281cb8198..156eda9142 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h @@ -32,8 +32,8 @@ public: void setFilter() override {} QList<QUrl> selectedFiles() const override { return m_selectedFile; } void selectFile(const QUrl &) override {} - QUrl directory() const override { return QUrl(); } - void setDirectory(const QUrl &) override {} + QUrl directory() const override { return m_directory; } + void setDirectory(const QUrl &directory) override; bool defaultNameFilterDisables() const override { return false; } bool handleActivityResult(jint requestCode, jint resultCode, jobject data) override; @@ -41,12 +41,14 @@ private: QJniObject getFileDialogIntent(const QString &intentType); void takePersistableUriPermission(const QJniObject &uri); void setInitialFileName(const QString &title); + void setInitialDirectoryUri(const QString &directory); void setOpenableCategory(); void setAllowMultipleSelections(bool allowMultiple); void setMimeTypes(); QEventLoop m_eventLoop; QList<QUrl> m_selectedFile; + QUrl m_directory; QJniObject m_intent; const QJniObject m_activity; }; diff --git a/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp b/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp index 01636d2ba5..82a10dac07 100644 --- a/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp +++ b/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QDir> +#include <QLocale> #include "qandroidplatformfontdatabase.h" @@ -47,6 +48,38 @@ QStringList QAndroidPlatformFontDatabase::fallbacksForFamily(const QString &fami QChar::Script script) const { QStringList result; + + // Prepend CJK fonts by the locale. + QLocale locale = QLocale::system(); + switch (locale.language()) { + case QLocale::Chinese: { + switch (locale.territory()) { + case QLocale::China: + case QLocale::Singapore: + result.append(QStringLiteral("Noto Sans Mono CJK SC")); + break; + case QLocale::Taiwan: + case QLocale::HongKong: + case QLocale::Macao: + result.append(QStringLiteral("Noto Sans Mono CJK TC")); + break; + default: + // no modifications. + break; + } + break; + } + case QLocale::Japanese: + result.append(QStringLiteral("Noto Sans Mono CJK JP")); + break; + case QLocale::Korean: + result.append(QStringLiteral("Noto Sans Mono CJK KR")); + break; + default: + // no modifications. + break; + } + if (styleHint == QFont::Monospace || styleHint == QFont::Courier) result.append(QString(qgetenv("QT_ANDROID_FONTS_MONOSPACE")).split(u';')); else if (styleHint == QFont::Serif) diff --git a/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp b/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp index c2ce2c84b2..e84a481a2b 100644 --- a/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp @@ -6,84 +6,101 @@ #include <QtCore/qvariant.h> #include <qpa/qwindowsysteminterface.h> #include <QtCore/private/qjnihelpers_p.h> +#include <QtCore/qjnitypes.h> QT_BEGIN_NAMESPACE QAndroidPlatformForeignWindow::QAndroidPlatformForeignWindow(QWindow *window, WId nativeHandle) - : QAndroidPlatformWindow(window), - m_surfaceId(-1) + : QAndroidPlatformWindow(window), m_view(nullptr), m_nativeViewInserted(false) { m_view = reinterpret_cast<jobject>(nativeHandle); - if (m_view.isValid()) - QtAndroid::setViewVisibility(m_view.object(), false); -} + if (isEmbeddingContainer()) { + m_nativeViewId = m_view.callMethod<jint>("getId"); + return; + } -QAndroidPlatformForeignWindow::~QAndroidPlatformForeignWindow() -{ if (m_view.isValid()) QtAndroid::setViewVisibility(m_view.object(), false); - if (m_surfaceId != -1) - QtAndroid::destroySurface(m_surfaceId); } -void QAndroidPlatformForeignWindow::lower() +QAndroidPlatformForeignWindow::~QAndroidPlatformForeignWindow() { - if (m_surfaceId == -1) + if (isEmbeddingContainer()) return; - QAndroidPlatformWindow::lower(); - QtAndroid::bringChildToBack(m_surfaceId); -} + if (m_view.isValid()) + QtAndroid::setViewVisibility(m_view.object(), false); -void QAndroidPlatformForeignWindow::raise() -{ - if (m_surfaceId == -1) - return; + m_nativeQtWindow.callMethod<void>("removeNativeView"); - QAndroidPlatformWindow::raise(); - QtAndroid::bringChildToFront(m_surfaceId); } void QAndroidPlatformForeignWindow::setGeometry(const QRect &rect) { QAndroidPlatformWindow::setGeometry(rect); - if (m_surfaceId != -1) - QtAndroid::setSurfaceGeometry(m_surfaceId, rect); + if (isEmbeddingContainer()) + return; + + if (m_nativeViewInserted) + setNativeGeometry(rect); } void QAndroidPlatformForeignWindow::setVisible(bool visible) { + if (isEmbeddingContainer()) { + QAndroidPlatformWindow::setVisible(visible); + return; + } + if (!m_view.isValid()) return; QtAndroid::setViewVisibility(m_view.object(), visible); - QAndroidPlatformWindow::setVisible(visible); - if (!visible && m_surfaceId != -1) { - QtAndroid::destroySurface(m_surfaceId); - m_surfaceId = -1; - } else if (m_surfaceId == -1) { - m_surfaceId = QtAndroid::insertNativeView(m_view.object(), geometry()); + if (!visible && m_nativeViewInserted) { + m_nativeQtWindow.callMethod<void>("removeNativeView"); + m_nativeViewInserted = false; + } else if (!m_nativeViewInserted) { + addViewToWindow(); } } void QAndroidPlatformForeignWindow::applicationStateChanged(Qt::ApplicationState state) { - if (state <= Qt::ApplicationHidden - && m_surfaceId != -1) { - QtAndroid::destroySurface(m_surfaceId); - m_surfaceId = -1; - } else if (m_view.isValid() && m_surfaceId == -1){ - m_surfaceId = QtAndroid::insertNativeView(m_view.object(), geometry()); + if (!isEmbeddingContainer()) { + if (state <= Qt::ApplicationHidden + && m_nativeViewInserted) { + m_nativeQtWindow.callMethod<void>("removeNativeView"); + m_nativeViewInserted = false; + } else if (m_view.isValid() && !m_nativeViewInserted){ + addViewToWindow(); + } } QAndroidPlatformWindow::applicationStateChanged(state); } -void QAndroidPlatformForeignWindow::setParent(const QPlatformWindow *window) +WId QAndroidPlatformForeignWindow::winId() const { - Q_UNUSED(window); + if (isEmbeddingContainer() && m_view.isValid()) + return reinterpret_cast<WId>(m_view.object()); + if (m_nativeQtWindow.isValid()) + return reinterpret_cast<WId>(m_nativeQtWindow.object()); + return 0L; +} + +void QAndroidPlatformForeignWindow::addViewToWindow() +{ + if (isEmbeddingContainer()) + return; + + jint x = 0, y = 0, w = -1, h = -1; + if (!geometry().isNull()) + geometry().getRect(&x, &y, &w, &h); + + m_nativeQtWindow.callMethod<void>("setNativeView", m_view, x, y, qMax(w, 1), qMax(h, 1)); + m_nativeViewInserted = true; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformforeignwindow.h b/src/plugins/platforms/android/qandroidplatformforeignwindow.h index 7b576ecec4..503524cced 100644 --- a/src/plugins/platforms/android/qandroidplatformforeignwindow.h +++ b/src/plugins/platforms/android/qandroidplatformforeignwindow.h @@ -4,29 +4,31 @@ #ifndef QANDROIDPLATFORMFOREIGNWINDOW_H #define QANDROIDPLATFORMFOREIGNWINDOW_H -#include "androidsurfaceclient.h" #include "qandroidplatformwindow.h" #include <QtCore/QJniObject> QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(View, "android/view/View") + class QAndroidPlatformForeignWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformForeignWindow(QWindow *window, WId nativeHandle); ~QAndroidPlatformForeignWindow(); - void lower() override; - void raise() override; void setGeometry(const QRect &rect) override; void setVisible(bool visible) override; void applicationStateChanged(Qt::ApplicationState state) override; - void setParent(const QPlatformWindow *window) override; bool isForeignWindow() const override { return true; } + WId winId() const override; + private: - int m_surfaceId; - QJniObject m_view; + void addViewToWindow(); + + QtJniTypes::View m_view; + bool m_nativeViewInserted; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformiconengine.cpp b/src/plugins/platforms/android/qandroidplatformiconengine.cpp new file mode 100644 index 0000000000..faa63dcca1 --- /dev/null +++ b/src/plugins/platforms/android/qandroidplatformiconengine.cpp @@ -0,0 +1,616 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidplatformiconengine.h" +#include "androidjnimain.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qjniarray.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qfile.h> +#include <QtCore/qset.h> + +#include <QtGui/qfontdatabase.h> +#include <QtGui/qpainter.h> +#include <QtGui/qpalette.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +Q_LOGGING_CATEGORY(lcIconEngineFontDownload, "qt.qpa.iconengine.fontdownload") + +// the primary types to work with the FontRequest API +Q_DECLARE_JNI_CLASS(FontRequest, "androidx/core/provider/FontRequest") +Q_DECLARE_JNI_CLASS(FontsContractCompat, "androidx/core/provider/FontsContractCompat") +Q_DECLARE_JNI_CLASS(FontFamilyResult, "androidx/core/provider/FontsContractCompat$FontFamilyResult") +Q_DECLARE_JNI_CLASS(FontInfo, "androidx/core/provider/FontsContractCompat$FontInfo") + +// various utility types +Q_DECLARE_JNI_CLASS(List, "java/util/List"); // List is just an Interface +Q_DECLARE_JNI_CLASS(ArrayList, "java/util/ArrayList"); +Q_DECLARE_JNI_CLASS(HashSet, "java/util/HashSet"); +Q_DECLARE_JNI_CLASS(Uri, "android/net/Uri") +Q_DECLARE_JNI_CLASS(CancellationSignal, "android/os/CancellationSignal") +Q_DECLARE_JNI_CLASS(ParcelFileDescriptor, "android/os/ParcelFileDescriptor") +Q_DECLARE_JNI_CLASS(ContentResolver, "android/content/ContentResolver") +Q_DECLARE_JNI_CLASS(PackageManager, "android/content/pm/PackageManager") +Q_DECLARE_JNI_CLASS(ProviderInfo, "android/content/pm/ProviderInfo") +Q_DECLARE_JNI_CLASS(PackageInfo, "android/content/pm/PackageInfo") +Q_DECLARE_JNI_CLASS(Signature, "android/content/pm/Signature") + +namespace FontProvider { + +static QString fetchFont(const QString &query) +{ + using namespace QtJniTypes; + + static QMap<QString, QString> triedFonts; + const auto it = triedFonts.find(query); + if (it != triedFonts.constEnd()) + return it.value(); + + QString fontFamily; + triedFonts[query] = fontFamily; // mark as tried + + QStringList loadedFamilies; + if (QFile file(query); file.open(QIODevice::ReadOnly)) { + qCDebug(lcIconEngineFontDownload) << "Loading font from resource" << query; + const QByteArray fontData = file.readAll(); + int fontId = QFontDatabase::addApplicationFontFromData(fontData); + loadedFamilies << QFontDatabase::applicationFontFamilies(fontId); + } else if (!query.startsWith(u":/"_s)) { + const QString package = u"com.google.android.gms"_s; + const QString authority = u"com.google.android.gms.fonts"_s; + + // First we access the content provider to get the signatures of the authority for the package + const auto context = QtAndroidPrivate::context(); + + auto packageManager = context.callMethod<PackageManager>("getPackageManager"); + if (!packageManager.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to instantiate PackageManager"); + return fontFamily; + } + const int signaturesField = PackageManager::getStaticField<int>("GET_SIGNATURES"); + auto providerInfo = packageManager.callMethod<ProviderInfo>("resolveContentProvider", + authority, 0); + if (!providerInfo.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to resolve content provider"); + return fontFamily; + } + const QString packageName = providerInfo.getField<QString>("packageName"); + if (packageName != package) { + qCWarning(lcIconEngineFontDownload, "Mismatched provider package - expected '%s', got '%s'", + package.toUtf8().constData(), packageName.toUtf8().constData()); + return fontFamily; + } + auto packageInfo = packageManager.callMethod<PackageInfo>("getPackageInfo", + package, signaturesField); + if (!packageInfo.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to get package info with signature field %d", + signaturesField); + return fontFamily; + } + const auto signatures = packageInfo.getField<Signature[]>("signatures"); + if (!signatures.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to get signature array from package info"); + return fontFamily; + } + + // FontRequest wants a list of sets for the certificates + ArrayList outerList; + HashSet innerSet; + Q_ASSERT(outerList.isValid() && innerSet.isValid()); + + for (const auto &signature : signatures) { + const QJniArray<jbyte> byteArray = signature.callMethod<jbyte[]>("toByteArray"); + + // add takes an Object, not an Array + if (!innerSet.callMethod<jboolean>("add", byteArray.object<jobject>())) + qCWarning(lcIconEngineFontDownload, "Failed to add signature to set"); + } + // Add the set to the list + if (!outerList.callMethod<jboolean>("add", innerSet.object())) + qCWarning(lcIconEngineFontDownload, "Failed to add set to certificate list"); + + // FontRequest constructor wants a List interface, not an ArrayList + FontRequest fontRequest(authority, package, query, outerList.object<List>()); + if (!fontRequest.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to create font request for '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + // Call FontsContractCompat::fetchFonts with the FontRequest object + auto fontFamilyResult = FontsContractCompat::callStaticMethod<FontFamilyResult>( + "fetchFonts", + context, + CancellationSignal(nullptr), + fontRequest); + if (!fontFamilyResult.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to fetch fonts for query '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + enum class StatusCode { + OK = 0, + UNEXPECTED_DATA_PROVIDED = 1, + WRONG_CERTIFICATES = 2, + }; + + const StatusCode statusCode = fontFamilyResult.callMethod<StatusCode>("getStatusCode"); + switch (statusCode) { + case StatusCode::OK: + break; + case StatusCode::UNEXPECTED_DATA_PROVIDED: + qCWarning(lcIconEngineFontDownload, "Provider returned unexpected data for query '%s'", + query.toUtf8().constData()); + return fontFamily; + case StatusCode::WRONG_CERTIFICATES: + qCWarning(lcIconEngineFontDownload, "Wrong Certificates provided in query '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + const auto fontInfos = fontFamilyResult.callMethod<FontInfo[]>("getFonts"); + if (!fontInfos.isValid()) { + qCWarning(lcIconEngineFontDownload, "FontFamilyResult::getFonts returned null object for '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + auto contentResolver = context.callMethod<ContentResolver>("getContentResolver"); + + for (QJniObject fontInfo : fontInfos) { + if (!fontInfo.isValid()) { + qCDebug(lcIconEngineFontDownload, "Received null-fontInfo object, skipping"); + continue; + } + enum class ResultCode { + OK = 0, + FONT_NOT_FOUND = 1, + FONT_UNAVAILABLE = 2, + MALFORMED_QUERY = 3, + }; + const ResultCode resultCode = fontInfo.callMethod<ResultCode>("getResultCode"); + switch (resultCode) { + case ResultCode::OK: + break; + case ResultCode::FONT_NOT_FOUND: + qCWarning(lcIconEngineFontDownload, "Font '%s' could not be found", + query.toUtf8().constData()); + return fontFamily; + case ResultCode::FONT_UNAVAILABLE: + qCWarning(lcIconEngineFontDownload, "Font '%s' is unavailable at", + query.toUtf8().constData()); + return fontFamily; + case ResultCode::MALFORMED_QUERY: + qCWarning(lcIconEngineFontDownload, "Query string '%s' is malformed", + query.toUtf8().constData()); + return fontFamily; + } + auto fontUri = fontInfo.callMethod<Uri>("getUri"); + // in this case the Font URI is always a content scheme file, made + // so the app requesting it has permissions to open + auto fileDescriptor = contentResolver.callMethod<ParcelFileDescriptor>("openFileDescriptor", + fontUri, u"r"_s); + if (!fileDescriptor.isValid()) { + qCWarning(lcIconEngineFontDownload, "Font file '%s' not accessible", + fontUri.toString().toUtf8().constData()); + continue; + } + + int fd = fileDescriptor.callMethod<int>("detachFd"); + QFile file; + file.open(fd, QFile::OpenModeFlag::ReadOnly, QFile::FileHandleFlag::AutoCloseHandle); + const QByteArray fontData = file.readAll(); + qCDebug(lcIconEngineFontDownload) << "Font file read:" << fontData.size() << "bytes"; + int fontId = QFontDatabase::addApplicationFontFromData(fontData); + loadedFamilies << QFontDatabase::applicationFontFamilies(fontId); + } + } + + qCDebug(lcIconEngineFontDownload) << "Query '" << query << "' added families" << loadedFamilies; + if (!loadedFamilies.isEmpty()) + fontFamily = loadedFamilies.first(); + triedFonts[query] = fontFamily; + return fontFamily; +} +} + +QString QAndroidPlatformIconEngine::glyphs() const +{ + if (!QFontInfo(m_iconFont).exactMatch()) + return {}; + + static constexpr std::pair<QLatin1StringView, QStringView> glyphMap[] = { + {"address-book-new"_L1, u"\ue0e0"}, + {"application-exit"_L1, u"\ue5cd"}, + {"appointment-new"_L1, u"\ue878"}, + {"call-start"_L1, u"\ue0b0"}, + {"call-stop"_L1, u"\ue0b1"}, + {"contact-new"_L1, u"\uf22e"}, + {"document-new"_L1, u"\ue89c"}, + {"document-open"_L1, u"\ue2c8"}, + {"document-open-recent"_L1, u"\ue4a7"}, + {"document-page-setup"_L1, u"\uf88c"}, + {"document-print"_L1, u"\ue8ad"}, + {"document-print-preview"_L1, u"\uefb2"}, + {"document-properties"_L1, u"\uf775"}, + {"document-revert"_L1, u"\ue929"}, + {"document-save"_L1, u"\ue161"}, + {"document-save-as"_L1, u"\ueb60"}, + {"document-send"_L1, u"\uf09b"}, + {"edit-clear"_L1, u"\ue872"}, + {"edit-copy"_L1, u"\ue14d"}, + {"edit-cut"_L1, u"\ue14e"}, + {"edit-delete"_L1, u"\ue14a"}, + {"edit-find"_L1, u"\ue8b6"}, + {"edit-find-replace"_L1, u"\ue881"}, + {"edit-paste"_L1, u"\ue14f"}, + {"edit-redo"_L1, u"\ue15a"}, + {"edit-select-all"_L1, u"\ue162"}, + {"edit-undo"_L1, u"\ue166"}, + {"folder-new"_L1, u"\ue2cc"}, + {"format-indent-less"_L1, u"\ue23d"}, + {"format-indent-more"_L1, u"\ue23e"}, + {"format-justify-center"_L1, u"\ue234"}, + {"format-justify-fill"_L1, u"\ue235"}, + {"format-justify-left"_L1, u"\ue236"}, + {"format-justify-right"_L1, u"\ue237"}, + {"format-text-direction-ltr"_L1, u"\ue247"}, + {"format-text-direction-rtl"_L1, u"\ue248"}, + {"format-text-bold"_L1, u"\ue238"}, + {"format-text-italic"_L1, u"\ue23f"}, + {"format-text-underline"_L1, u"\ue249"}, + {"format-text-strikethrough"_L1, u"\ue246"}, + {"go-bottom"_L1,u"\ue258"}, + {"go-down"_L1,u"\uf1e3"}, + {"go-first"_L1, u"\ue5dc"}, + {"go-home"_L1, u"\ue88a"}, + {"go-jump"_L1, u"\uf719"}, + {"go-last"_L1, u"\ue5dd"}, + {"go-next"_L1, u"\ue5c8"}, + {"go-previous"_L1, u"\ue5c4"}, + {"go-top"_L1, u"\ue25a"}, + {"go-up"_L1, u"\uf1e0"}, + {"help-about"_L1, u"\ue88e"}, + {"help-contents"_L1, u"\ue8de"}, + {"help-faq"_L1, u"\uf04c"}, + {"insert-image"_L1, u"\ue43e"}, + {"insert-link"_L1, u"\ue178"}, + //{"insert-object"_L1, u"\u"}, + {"insert-text"_L1, u"\uf827"}, + {"list-add"_L1, u"\ue145"}, + {"list-remove"_L1, u"\ue15b"}, + {"mail-forward"_L1, u"\ue154"}, + {"mail-mark-important"_L1, u"\ue937"}, + //{"mail-mark-junk"_L1, u"\u"}, + //{"mail-mark-notjunk"_L1, u"\u"}, + {"mail-mark-read"_L1, u"\uf18c"}, + {"mail-mark-unread"_L1, u"\ue9bc"}, + {"mail-message-new"_L1, u"\ue3c9"}, + {"mail-reply-all"_L1, u"\ue15f"}, + {"mail-reply-sender"_L1, u"\ue15e"}, + {"mail-send"_L1, u"\ue163"}, + //{"mail-send-receive"_L1, u"\u"}, + {"media-eject"_L1, u"\ue8fb"}, + {"media-playback-pause"_L1, u"\ue034"}, + {"media-playback-start"_L1, u"\ue037"}, + {"media-playback-stop"_L1, u"\ue047"}, + {"media-record"_L1, u"\uf679"}, + {"media-seek-backward"_L1, u"\ue020"}, + {"media-seek-forward"_L1, u"\ue01f"}, + {"media-skip-backward"_L1, u"\ue045"}, + {"media-skip-forward"_L1, u"\ue044"}, + //{"object-flip-horizontal"_L1, u"\u"}, + //{"object-flip-vertical"_L1, u"\u"}, + {"object-rotate-left"_L1, u"\ue419"}, + {"object-rotate-right"_L1, u"\ue41a"}, + {"process-stop"_L1, u"\ue5c9"}, + {"system-lock-screen"_L1, u"\ue897"}, + {"system-log-out"_L1, u"\ue9ba"}, + //{"system-run"_L1, u"\u"}, + {"system-search"_L1, u"\uef70"}, + {"system-reboot"_L1, u"\uf053"}, + {"system-shutdown"_L1, u"\ue8ac"}, + {"tools-check-spelling"_L1, u"\ue8ce"}, + {"view-fullscreen"_L1, u"\ue5d0"}, + {"view-refresh"_L1, u"\ue5d5"}, + {"view-restore"_L1, u"\uf1cf"}, + {"view-sort-ascending"_L1, u"\ue25a"}, + {"view-sort-descending"_L1, u"\ue258"}, + {"window-close"_L1, u"\ue5cd"}, + {"window-new"_L1, u"\uf710"}, + {"zoom-fit-best"_L1, u"\uea10"}, + {"zoom-in"_L1, u"\ue8ff"}, + {"zoom-original"_L1, u"\ue5d1"}, + {"zoom-out"_L1, u"\ue900"}, + {"process-working"_L1, u"\uef64"}, + {"accessories-calculator"_L1, u"\uea5f"}, + {"accessories-character-map"_L1, u"\uf8a3"}, + {"accessories-dictionary"_L1, u"\uf539"}, + {"accessories-text-editor"_L1, u"\ue262"}, + {"help-browser"_L1, u"\ue887"}, + {"multimedia-volume-control"_L1, u"\ue050"}, + {"preferences-desktop-accessibility"_L1, u"\uf05d"}, + {"preferences-desktop-font"_L1, u"\ue165"}, + {"preferences-desktop-keyboard"_L1, u"\ue312"}, + //{"preferences-desktop-locale"_L1, u"\u"}, + {"preferences-desktop-multimedia"_L1, u"\uea75"}, + //{"preferences-desktop-screensaver"_L1, u"\u"}, + {"preferences-desktop-theme"_L1, u"\uf560"}, + {"preferences-desktop-wallpaper"_L1, u"\ue1bc"}, + {"system-file-manager"_L1, u"\ue2c7"}, + {"system-software-install"_L1, u"\ueb71"}, + {"system-software-update"_L1, u"\ue8d7"}, + {"utilities-system-monitor"_L1, u"\uef5b"}, + {"utilities-terminal"_L1, u"\ueb8e"}, + //{"applications-accessories"_L1, u"\u"}, + {"applications-development"_L1, u"\ue720"}, + {"applications-engineering"_L1, u"\uea3d"}, + {"applications-games"_L1, u"\uf135"}, + //{"applications-graphics"_L1, u"\u"}, + {"applications-internet"_L1, u"\ue80b"}, + {"applications-multimedia"_L1, u"\uf06a"}, + //{"applications-office"_L1, u"\u"}, + //{"applications-other"_L1, u"\u"}, + {"applications-science"_L1, u"\uea4b"}, + //{"applications-system"_L1, u"\u"}, + //{"applications-utilities"_L1, u"\u"}, + {"preferences-desktop"_L1, u"\ueb97"}, + //{"preferences-desktop-peripherals"_L1, u"\u"}, + {"preferences-desktop-personal"_L1, u"\uf835"}, + //{"preferences-other"_L1, u"\u"}, + {"preferences-system"_L1, u"\ue8b8"}, + {"preferences-system-network"_L1, u"\ue894"}, + {"system-help"_L1, u"\ue887"}, + //{"audio-card"_L1, u"\u"}, + {"audio-input-microphone"_L1, u"\ue029"}, + {"battery"_L1, u"\ue1a4"}, + {"camera-photo"_L1, u"\ue412"}, + {"camera-video"_L1, u"\ue04b"}, + {"camera-web"_L1, u"\uf7a6"}, + {"computer"_L1, u"\ue30a"}, + {"drive-harddisk"_L1, u"\uf80e"}, + {"drive-optical"_L1, u"\ue019"}, // same as media-optical + //{"drive-removable-media"_L1, u"\u"}, + {"input-gaming"_L1, u"\uf5ee"}, + {"input-keyboard"_L1, u"\ue312"}, + {"input-mouse"_L1, u"\ue323"}, + //{"input-tablet"_L1, u"\u"}, + //{"media-flash"_L1, u"\u"}, + //{"media-floppy"_L1, u"\u"}, + {"media-optical"_L1, u"\ue019"}, + //{"media-tape"_L1, u"\u"}, + //{"modem"_L1, u"\u"}, + //{"multimedia-player"_L1, u"\u"}, + //{"network-wired"_L1, u"\u"}, + {"network-wireless"_L1, u"\ue63e"}, + //{"pda"_L1, u"\u"}, + {"phone"_L1, u"\ue32c"}, + {"printer"_L1, u"\ue8ad"}, + {"scanner"_L1, u"\ue329"}, + {"video-display"_L1, u"\uf06a"}, + //{"emblem-default"_L1, u"\u"}, + {"emblem-documents"_L1, u"\ue873"}, + {"emblem-downloads"_L1, u"\uf090"}, + {"emblem-favorite"_L1, u"\uf090"}, + {"emblem-important"_L1, u"\ue645"}, + {"emblem-mail"_L1, u"\ue158"}, + {"emblem-photos"_L1, u"\ue413"}, + //{"emblem-readonly"_L1, u"\u"}, + {"emblem-shared"_L1, u"\ue413"}, + //{"emblem-symbolic-link"_L1, u"\u"}, + //{"emblem-synchronized"_L1, u"\u"}, + {"emblem-system"_L1, u"\ue8b8"}, + //{"emblem-unreadable"_L1, u"\u"}, + {"folder"_L1, u"\ue2c7"}, + //{"folder-remote"_L1, u"\u"}, + {"network-server"_L1, u"\ue875"}, + {"network-workgroup"_L1, u"\ue1a0"}, + {"start-here"_L1, u"\ue089"}, + {"user-bookmarks"_L1, u"\ue98b"}, + {"user-desktop"_L1, u"\ue30a"}, + {"user-home"_L1, u"\ue88a"}, + {"user-trash"_L1, u"\ue872"}, + {"appointment-missed"_L1, u"\ue615"}, + {"appointment-soon"_L1, u"\uf540"}, + {"audio-volume-high"_L1, u"\ue050"}, + {"audio-volume-low"_L1, u"\ue04d"}, + //{"audio-volume-medium"_L1, u"\u"}, + {"audio-volume-muted"_L1, u"\ue04e"}, + {"battery-caution"_L1, u"\ue19c"}, + {"battery-low"_L1, u"\uf147"}, + {"dialog-error"_L1, u"\ue000"}, + {"dialog-information"_L1, u"\ue88e"}, + {"dialog-password"_L1, u"\uf042"}, + {"dialog-question"_L1, u"\ueb8b"}, + {"dialog-warning"_L1, u"\ue002"}, + {"folder-drag-accept"_L1, u"\ue9a3"}, + {"folder-open"_L1, u"\ue2c8"}, + {"folder-visiting"_L1, u"\ue8a7"}, + {"image-loading"_L1, u"\ue41a"}, + {"image-missing"_L1, u"\ue3ad"}, + {"mail-attachment"_L1, u"\ue2bc"}, + {"mail-unread"_L1, u"\uf18a"}, + {"mail-read"_L1, u"\uf18c"}, + //{"mail-replied"_L1, u"\u"}, + //{"mail-signed"_L1, u"\u"}, + //{"mail-signed-verified"_L1, u"\u"}, + {"media-playlist-repeat"_L1, u"\ue040"}, + {"media-playlist-shuffle"_L1, u"\ue043"}, + {"network-error"_L1, u"\uead9"}, + {"network-idle"_L1, u"\ue51f"}, + {"network-offline"_L1, u"\uf239"}, + {"network-receive"_L1, u"\ue2c0"}, + {"network-transmit"_L1, u"\ue2c3"}, + {"network-transmit-receive"_L1, u"\uea18"}, + {"printer-error"_L1, u"\uf7a0"}, + {"printer-printing"_L1, u"\uf7a1"}, + {"security-high"_L1, u"\ue32a"}, + {"security-medium"_L1, u"\ue9e0"}, + {"security-low"_L1, u"\uf012"}, + {"software-update-available"_L1, u"\ue923"}, + {"software-update-urgent"_L1, u"\uf05a"}, + {"sync-error"_L1, u"\ue629"}, + {"sync-synchronizing"_L1, u"\ue627"}, + //{"task-due"_L1, u"\u"}, + //{"task-past-due"_L1, u"\u"}, + {"user-available"_L1, u"\uf565"}, + {"user-away"_L1, u"\ue510"}, + //{"user-idle"_L1, u"\u"}, + {"user-offline"_L1, u"\uf7b3"}, + {"user-trash-full"_L1, u"\ue872"}, //delete + //{"user-trash-full"_L1, u"\ue92b"}, //delete_forever + {"weather-clear"_L1, u"\uf157"}, + {"weather-clear-night"_L1, u"\uf159"}, + {"weather-few-clouds"_L1, u"\uf172"}, + {"weather-few-clouds-night"_L1, u"\uf174"}, + {"weather-fog"_L1, u"\ue818"}, + //{"weather-overcast"_L1, u"\u"}, + {"weather-severe-alert"_L1, u"\ue002"}, //warning + //{"weather-severe-alert"_L1, u"\uebd3"},//severe_cold + {"weather-showers"_L1, u"\uf176"}, + //{"weather-showers-scattered"_L1, u"\u"}, + {"weather-snow"_L1, u"\ue80f"}, //snowing + //{"weather-snow"_L1, u"\ue2cd"}, //weather_snowy + //{"weather-snow"_L1, u"\ue810"},//cloudy_snowing + {"weather-storm"_L1, u"\uf070"}, + }; + + const auto it = std::find_if(std::begin(glyphMap), std::end(glyphMap), [this](const auto &c){ + return c.first == m_iconName; + }); + return it != std::end(glyphMap) ? it->second.toString() + : (m_iconName.length() == 1 ? m_iconName : QString()); +} + +QAndroidPlatformIconEngine::QAndroidPlatformIconEngine(const QString &iconName) + : m_iconName(iconName) + , m_glyphs(glyphs()) +{ + QString fontFamily; + // The MaterialIcons-*.ttf and MaterialSymbols* font files are available from + // https://github.com/google/material-design-icons/tree/master. If one of them is + // packaged as a resource with the application, then we use it. We prioritize + // a variable font. + const QStringList fontCandidates = { + "MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf", + "MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf", + "MaterialSymbolsSharp[FILL,GRAD,opsz,wght].ttf", + "MaterialIcons-Regular.ttf", + "MaterialIconsOutlined-Regular.otf", + "MaterialIconsRound-Regular.otf", + "MaterialIconsSharp-Regular.otf", + "MaterialIconsTwoTone-Regular.otf", + }; + for (const auto &fontCandidate : fontCandidates) { + fontFamily = FontProvider::fetchFont(u":/qt-project.org/icons/%1"_s.arg(fontCandidate)); + if (!fontFamily.isEmpty()) + break; + } + + // Otherwise we try to download the Outlined version of Material Symbols + const QString key = qEnvironmentVariable("QT_GOOGLE_FONTS_KEY"); + if (fontFamily.isEmpty() && !key.isEmpty()) + fontFamily = FontProvider::fetchFont(u"key=%1&name=Material+Symbols+Outlined"_s.arg(key)); + + // last resort - use any Material Icons + if (fontFamily.isEmpty()) + fontFamily = u"Material Icons"_s; + m_iconFont = QFont(fontFamily); +} + +QAndroidPlatformIconEngine::~QAndroidPlatformIconEngine() +{} + +QIconEngine *QAndroidPlatformIconEngine::clone() const +{ + return new QAndroidPlatformIconEngine(m_iconName); +} + +QString QAndroidPlatformIconEngine::key() const +{ + return u"QAndroidPlatformIconEngine"_s; +} + +QString QAndroidPlatformIconEngine::iconName() +{ + return m_iconName; +} + +bool QAndroidPlatformIconEngine::isNull() +{ + if (m_glyphs.isEmpty()) + return true; + const QChar c0 = m_glyphs.at(0); + const QFontMetrics fontMetrics(m_iconFont); + if (c0.category() == QChar::Other_Surrogate && m_glyphs.size() > 1) + return !fontMetrics.inFontUcs4(QChar::surrogateToUcs4(c0, m_glyphs.at(1))); + return !fontMetrics.inFont(c0); +} + +QList<QSize> QAndroidPlatformIconEngine::availableSizes(QIcon::Mode, QIcon::State) +{ + return {{16, 16}, {24, 24}, {48, 48}, {128, 128}}; +} + +QSize QAndroidPlatformIconEngine::actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return QIconEngine::actualSize(size, mode, state); +} + +QPixmap QAndroidPlatformIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return scaledPixmap(size, mode, state, 1.0); +} + +QPixmap QAndroidPlatformIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) +{ + const quint64 cacheKey = calculateCacheKey(mode, state); + if (cacheKey != m_cacheKey || m_pixmap.size() != size || m_pixmap.devicePixelRatio() != scale) { + m_pixmap = QPixmap(size * scale); + m_pixmap.fill(Qt::transparent); + m_pixmap.setDevicePixelRatio(scale); + + QPainter painter(&m_pixmap); + paint(&painter, QRect(QPoint(), size), mode, state); + + m_cacheKey = cacheKey; + } + + return m_pixmap; +} + +void QAndroidPlatformIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) +{ + Q_UNUSED(state); + + painter->save(); + QFont renderFont(m_iconFont); + renderFont.setPixelSize(rect.height()); + painter->setFont(renderFont); + + QPalette palette; + switch (mode) { + case QIcon::Active: + painter->setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Normal: + painter->setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Disabled: + painter->setPen(palette.color(QPalette::Disabled, QPalette::Text)); + break; + case QIcon::Selected: + painter->setPen(palette.color(QPalette::Active, QPalette::HighlightedText)); + break; + } + + painter->drawText(rect, Qt::AlignCenter, m_glyphs); + painter->restore(); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformiconengine.h b/src/plugins/platforms/android/qandroidplatformiconengine.h new file mode 100644 index 0000000000..cac54481d9 --- /dev/null +++ b/src/plugins/platforms/android/qandroidplatformiconengine.h @@ -0,0 +1,44 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QANDROIDPLATFORMICONENGINE_H +#define QANDROIDPLATFORMICONENGINE_H + +#include <QtGui/qiconengine.h> +#include <QtGui/qfont.h> + +QT_BEGIN_NAMESPACE + +class QAndroidPlatformIconEngine : public QIconEngine +{ +public: + QAndroidPlatformIconEngine(const QString &iconName); + ~QAndroidPlatformIconEngine(); + QIconEngine *clone() const override; + QString key() const override; + QString iconName() override; + bool isNull() override; + + QList<QSize> availableSizes(QIcon::Mode, QIcon::State) override; + QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) override; + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + +private: + static constexpr quint64 calculateCacheKey(QIcon::Mode mode, QIcon::State state) + { + return (quint64(mode) << 32) | state; + } + QString glyphs() const; + + const QString m_iconName; + QFont m_iconFont; + const QString m_glyphs; + mutable QPixmap m_pixmap; + mutable quint64 m_cacheKey = {}; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDPLATFORMICONENGINE_H diff --git a/src/plugins/platforms/android/qandroidplatformintegration.cpp b/src/plugins/platforms/android/qandroidplatformintegration.cpp index 19a7326115..6efd3fc631 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.cpp +++ b/src/plugins/platforms/android/qandroidplatformintegration.cpp @@ -9,7 +9,6 @@ #include "qabstracteventdispatcher.h" #include "qandroideventdispatcher.h" #include "qandroidplatformaccessibility.h" -#include "qandroidplatformbackingstore.h" #include "qandroidplatformclipboard.h" #include "qandroidplatformfontdatabase.h" #include "qandroidplatformforeignwindow.h" @@ -29,6 +28,7 @@ #include <QtGui/private/qeglpbuffer_p.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qoffscreensurface_p.h> +#include <QtGui/private/qrhibackingstore_p.h> #include <qpa/qplatformoffscreensurface.h> #include <qpa/qplatformwindow.h> #include <qpa/qwindowsysteminterface.h> @@ -46,24 +46,52 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -QSize QAndroidPlatformIntegration::m_defaultScreenSize = QSize(320, 455); -QRect QAndroidPlatformIntegration::m_defaultAvailableGeometry = QRect(0, 0, 320, 455); -QSize QAndroidPlatformIntegration::m_defaultPhysicalSize = QSize(50, 71); +Q_CONSTINIT QSize QAndroidPlatformIntegration::m_defaultScreenSize = QSize(320, 455); +Q_CONSTINIT QRect QAndroidPlatformIntegration::m_defaultAvailableGeometry = QRect(0, 0, 320, 455); +Q_CONSTINIT QSize QAndroidPlatformIntegration::m_defaultPhysicalSize = QSize(50, 71); Qt::ScreenOrientation QAndroidPlatformIntegration::m_orientation = Qt::PrimaryOrientation; Qt::ScreenOrientation QAndroidPlatformIntegration::m_nativeOrientation = Qt::PrimaryOrientation; bool QAndroidPlatformIntegration::m_showPasswordEnabled = false; -static bool m_running = false; + +Q_DECLARE_JNI_CLASS(QtNative, "org/qtproject/qt/android/QtNative") +Q_DECLARE_JNI_CLASS(QtDisplayManager, "org/qtproject/qt/android/QtDisplayManager") +Q_DECLARE_JNI_CLASS(Display, "android/view/Display") + +Q_DECLARE_JNI_CLASS(List, "java/util/List") + +namespace { + +QAndroidPlatformScreen* createScreenForDisplayId(int displayId) +{ + const QJniObject display = QtJniTypes::QtDisplayManager::callStaticMethod<QtJniTypes::Display>( + "getDisplay", QtAndroidPrivate::context(), displayId); + if (!display.isValid()) + return nullptr; + return new QAndroidPlatformScreen(display); +} + +static bool isValidAndroidContextForRendering() +{ + return QtAndroid::isQtApplication() ? QtAndroidPrivate::activity().isValid() + : QtAndroidPrivate::context().isValid(); +} + +} // anonymous namespace void *QAndroidPlatformNativeInterface::nativeResourceForIntegration(const QByteArray &resource) { if (resource=="JavaVM") return QtAndroid::javaVM(); - if (resource == "QtActivity") - return QtAndroid::activity(); - if (resource == "QtService") - return QtAndroid::service(); + if (resource == "QtActivity") { + extern Q_CORE_EXPORT jobject qt_androidActivity(); + return qt_androidActivity(); + } + if (resource == "QtService") { + extern Q_CORE_EXPORT jobject qt_androidService(); + return qt_androidService(); + } if (resource == "AndroidStyleData") { if (m_androidStyle) { if (m_androidStyle->m_styleData.isEmpty()) @@ -136,10 +164,6 @@ void QAndroidPlatformNativeInterface::customEvent(QEvent *event) api->accessibility()->setActive(QtAndroidAccessibility::isActive()); #endif // QT_CONFIG(accessibility) - if (!m_running) { - m_running = true; - QtAndroid::notifyQtAndroidPluginRunning(m_running); - } api->flushPendingUpdates(); } @@ -163,10 +187,32 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ if (Q_UNLIKELY(!eglBindAPI(EGL_OPENGL_ES_API))) qFatal("Could not bind GL_ES API"); - m_primaryScreen = new QAndroidPlatformScreen(); - QWindowSystemInterface::handleScreenAdded(m_primaryScreen); - m_primaryScreen->setSizeParameters(m_defaultPhysicalSize, m_defaultScreenSize, - m_defaultAvailableGeometry); + using namespace QtJniTypes; + m_primaryDisplayId = Display::getStaticField<jint>("DEFAULT_DISPLAY"); + const QJniObject nativeDisplaysList = QtDisplayManager::callStaticMethod<List>( + "getAvailableDisplays", QtAndroidPrivate::context()); + + const int numberOfAvailableDisplays = nativeDisplaysList.callMethod<jint>("size"); + for (int i = 0; i < numberOfAvailableDisplays; ++i) { + const QJniObject display = + nativeDisplaysList.callObjectMethod<jobject, jint>("get", jint(i)); + const int displayId = display.callMethod<jint>("getDisplayId"); + const bool isPrimary = (m_primaryDisplayId == displayId); + auto screen = new QAndroidPlatformScreen(display); + + if (isPrimary) + m_primaryScreen = screen; + + QWindowSystemInterface::handleScreenAdded(screen, isPrimary); + m_screens[displayId] = screen; + } + + if (numberOfAvailableDisplays == 0) { + // If no displays are found, add a dummy display + auto defaultScreen = new QAndroidPlatformScreen(QJniObject {}); + m_primaryScreen = defaultScreen; + QWindowSystemInterface::handleScreenAdded(defaultScreen, true); + } m_mainThread = QThread::currentThread(); @@ -183,9 +229,9 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ m_accessibility = new QAndroidPlatformAccessibility(); #endif // QT_CONFIG(accessibility) - QJniObject javaActivity(QtAndroid::activity()); + QJniObject javaActivity = QtAndroidPrivate::activity(); if (!javaActivity.isValid()) - javaActivity = QtAndroid::service(); + javaActivity = QtAndroidPrivate::service(); if (javaActivity.isValid()) { QJniObject resources = javaActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;"); @@ -225,6 +271,10 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ maxTouchPoints, 0); QWindowSystemInterface::registerInputDevice(m_touchDevice); + + QWindowSystemInterface::registerInputDevice( + new QInputDevice("Virtual keyboard"_L1, 0, QInputDevice::DeviceType::Keyboard, + {}, qApp)); } auto contentResolver = javaActivity.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); @@ -260,11 +310,11 @@ static bool needsBasicRenderloopWorkaround() void QAndroidPlatformIntegration::initialize() { - const QString icStr = QPlatformInputContextFactory::requested(); - if (icStr.isNull()) + const auto icStrs = QPlatformInputContextFactory::requested(); + if (icStrs.isEmpty()) m_inputContext.reset(new QAndroidInputContext); else - m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); + m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); } bool QAndroidPlatformIntegration::hasCapability(Capability cap) const @@ -272,13 +322,19 @@ bool QAndroidPlatformIntegration::hasCapability(Capability cap) const switch (cap) { case ApplicationState: return true; case ThreadedPixmaps: return true; - case NativeWidgets: return QtAndroid::activity(); - case OpenGL: return QtAndroid::activity(); - case ForeignWindows: return QtAndroid::activity(); - case ThreadedOpenGL: return !needsBasicRenderloopWorkaround() && QtAndroid::activity(); - case RasterGLSurface: return QtAndroid::activity(); + case NativeWidgets: return QtAndroidPrivate::activity().isValid(); + case OpenGL: + return isValidAndroidContextForRendering(); + case ForeignWindows: + return isValidAndroidContextForRendering(); + case ThreadedOpenGL: + return !needsBasicRenderloopWorkaround() && isValidAndroidContextForRendering(); + case RasterGLSurface: return QtAndroidPrivate::activity().isValid(); case TopStackedNativeChildWindows: return false; case MaximizeUsingFullscreenGeometry: return true; + // FIXME QTBUG-118849 - we do not implement grabWindow() anymore, calling it will return + // a null QPixmap also for raster windows - for OpenGL windows this was always true + case ScreenWindowGrabbing: return false; default: return QPlatformIntegration::hasCapability(cap); } @@ -286,15 +342,15 @@ bool QAndroidPlatformIntegration::hasCapability(Capability cap) const QPlatformBackingStore *QAndroidPlatformIntegration::createPlatformBackingStore(QWindow *window) const { - if (!QtAndroid::activity()) + if (!QtAndroidPrivate::activity().isValid()) return nullptr; - return new QAndroidPlatformBackingStore(window); + return new QRhiBackingStore(window); } QPlatformOpenGLContext *QAndroidPlatformIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - if (!QtAndroid::activity()) + if (!isValidAndroidContextForRendering()) return nullptr; QSurfaceFormat format(context->format()); format.setAlphaBufferSize(8); @@ -312,7 +368,7 @@ QOpenGLContext *QAndroidPlatformIntegration::createOpenGLContext(EGLContext cont QPlatformOffscreenSurface *QAndroidPlatformIntegration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const { - if (!QtAndroid::activity()) + if (!QtAndroidPrivate::activity().isValid()) return nullptr; QSurfaceFormat format(surface->requestedFormat()); @@ -326,7 +382,7 @@ QPlatformOffscreenSurface *QAndroidPlatformIntegration::createPlatformOffscreenS QOffscreenSurface *QAndroidPlatformIntegration::createOffscreenSurface(ANativeWindow *nativeSurface) const { - if (!QtAndroid::activity() || !nativeSurface) + if (!QtAndroidPrivate::activity().isValid() || !nativeSurface) return nullptr; auto *surface = new QOffscreenSurface; @@ -337,7 +393,7 @@ QOffscreenSurface *QAndroidPlatformIntegration::createOffscreenSurface(ANativeWi QPlatformWindow *QAndroidPlatformIntegration::createPlatformWindow(QWindow *window) const { - if (!QtAndroid::activity()) + if (!isValidAndroidContextForRendering()) return nullptr; #if QT_CONFIG(vulkan) @@ -432,7 +488,7 @@ QStringList QAndroidPlatformIntegration::themeNames() const QPlatformTheme *QAndroidPlatformIntegration::createPlatformTheme(const QString &name) const { if (androidThemeName == name) - return new QAndroidPlatformTheme(m_androidPlatformNativeInterface); + return QAndroidPlatformTheme::instance(m_androidPlatformNativeInterface); return 0; } @@ -488,6 +544,18 @@ void QAndroidPlatformIntegration::setScreenSize(int width, int height) QMetaObject::invokeMethod(m_primaryScreen, "setSize", Qt::AutoConnection, Q_ARG(QSize, QSize(width, height))); } +Qt::ColorScheme QAndroidPlatformIntegration::m_colorScheme = Qt::ColorScheme::Light; + +void QAndroidPlatformIntegration::updateColorScheme(Qt::ColorScheme colorScheme) +{ + if (m_colorScheme == colorScheme) + return; + m_colorScheme = colorScheme; + + QMetaObject::invokeMethod(qGuiApp, + [] () { QAndroidPlatformTheme::instance()->updateColorScheme();}); +} + void QAndroidPlatformIntegration::setScreenSizeParameters(const QSize &physicalSize, const QSize &screenSize, const QRect &availableGeometry) @@ -505,6 +573,49 @@ void QAndroidPlatformIntegration::setRefreshRate(qreal refreshRate) QMetaObject::invokeMethod(m_primaryScreen, "setRefreshRate", Qt::AutoConnection, Q_ARG(qreal, refreshRate)); } + +void QAndroidPlatformIntegration::handleScreenAdded(int displayId) +{ + auto result = m_screens.insert(displayId, nullptr); + if (result.first->second == nullptr) { + auto it = result.first; + it->second = createScreenForDisplayId(displayId); + if (it->second == nullptr) + return; + const bool isPrimary = (m_primaryDisplayId == displayId); + if (isPrimary) + m_primaryScreen = it->second; + QWindowSystemInterface::handleScreenAdded(it->second, isPrimary); + } else { + qWarning() << "Display with id" << displayId << "already exists."; + } +} + +void QAndroidPlatformIntegration::handleScreenChanged(int displayId) +{ + auto it = m_screens.find(displayId); + if (it == m_screens.end() || it->second == nullptr) { + handleScreenAdded(displayId); + } + // We do not do anything more here as handling of change of + // rotation and refresh rate is done in QtActivityDelegate java class + // which calls QAndroidPlatformIntegration::setOrientation, and + // QAndroidPlatformIntegration::setRefreshRate accordingly. +} + +void QAndroidPlatformIntegration::handleScreenRemoved(int displayId) +{ + auto it = m_screens.find(displayId); + + if (it == m_screens.end()) + return; + + if (it->second != nullptr) + QWindowSystemInterface::handleScreenRemoved(it->second); + + m_screens.erase(it); +} + #if QT_CONFIG(vulkan) QPlatformVulkanInstance *QAndroidPlatformIntegration::createPlatformVulkanInstance(QVulkanInstance *instance) const diff --git a/src/plugins/platforms/android/qandroidplatformintegration.h b/src/plugins/platforms/android/qandroidplatformintegration.h index 6e87c9c02b..b7bfb58d1d 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.h +++ b/src/plugins/platforms/android/qandroidplatformintegration.h @@ -13,6 +13,9 @@ #include <qpa/qplatformnativeinterface.h> #include <qpa/qplatformopenglcontext.h> #include <qpa/qplatformoffscreensurface.h> +#include <qpa/qplatformtheme.h> +#include <private/qflatmap_p.h> +#include <QtCore/qvarlengtharray.h> #include <EGL/egl.h> #include <memory> @@ -74,6 +77,10 @@ public: QPlatformFontDatabase *fontDatabase() const override; + void handleScreenAdded(int displayId); + void handleScreenChanged(int displayId); + void handleScreenRemoved(int displayId); + #ifndef QT_NO_CLIPBOARD QPlatformClipboard *clipboard() const override; #endif @@ -103,6 +110,8 @@ public: void flushPendingUpdates(); + static void updateColorScheme(Qt::ColorScheme colorScheme); + static Qt::ColorScheme colorScheme() { return m_colorScheme; } #if QT_CONFIG(vulkan) QPlatformVulkanInstance *createPlatformVulkanInstance(QVulkanInstance *instance) const override; #endif @@ -115,6 +124,8 @@ private: QThread *m_mainThread; + static Qt::ColorScheme m_colorScheme; + static QRect m_defaultAvailableGeometry; static QSize m_defaultPhysicalSize; static QSize m_defaultScreenSize; @@ -127,6 +138,17 @@ private: QAndroidPlatformNativeInterface *m_androidPlatformNativeInterface; QAndroidPlatformServices *m_androidPlatformServices; + // Handling the multiple screens connected. Every display is identified + // with an unique (autoincremented) displayID. The values of this ID will + // not repeat during the OS runtime. We use this value as the key in the + // storage of screens. + QFlatMap<int, QAndroidPlatformScreen *, std::less<int> + , QVarLengthArray<int, 10> + , QVarLengthArray<QAndroidPlatformScreen *, 10> > m_screens; + // ID of the primary display, in documentation it is said to be always 0, + // but nevertheless it is retrieved + int m_primaryDisplayId = 0; + #ifndef QT_NO_CLIPBOARD QPlatformClipboard *m_androidPlatformClipboard; #endif diff --git a/src/plugins/platforms/android/qandroidplatformmenu.cpp b/src/plugins/platforms/android/qandroidplatformmenu.cpp index 4ddd6ea29a..e59fd2089d 100644 --- a/src/plugins/platforms/android/qandroidplatformmenu.cpp +++ b/src/plugins/platforms/android/qandroidplatformmenu.cpp @@ -119,7 +119,7 @@ void QAndroidPlatformMenu::showPopup(const QWindow *parentWindow, const QRect &t Q_UNUSED(parentWindow); Q_UNUSED(item); setVisible(true); - QtAndroidMenu::showContextMenu(this, targetRect, QJniEnvironment().jniEnv()); + QtAndroidMenu::showContextMenu(this, targetRect); } QPlatformMenuItem *QAndroidPlatformMenu::menuItemForTag(quintptr tag) const diff --git a/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp b/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp index c1ec2fbdd6..13d14eb391 100644 --- a/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp @@ -24,41 +24,19 @@ QT_BEGIN_NAMESPACE QAndroidPlatformOpenGLWindow::QAndroidPlatformOpenGLWindow(QWindow *window, EGLDisplay display) :QAndroidPlatformWindow(window), m_eglDisplay(display) { + if (window->surfaceType() == QSurface::RasterSurface) + window->setSurfaceType(QSurface::OpenGLSurface); } QAndroidPlatformOpenGLWindow::~QAndroidPlatformOpenGLWindow() { m_surfaceWaitCondition.wakeOne(); lockSurface(); - if (m_nativeSurfaceId != -1) - QtAndroid::destroySurface(m_nativeSurfaceId); + destroySurface(); clearEgl(); unlockSurface(); } -void QAndroidPlatformOpenGLWindow::repaint(const QRegion ®ion) -{ - // This is only for real raster top-level windows. Stop in all other cases. - if ((window()->surfaceType() == QSurface::RasterGLSurface && qt_window_private(window())->compositing) - || window()->surfaceType() == QSurface::OpenGLSurface - || QAndroidPlatformWindow::parent()) - return; - - QRect currentGeometry = geometry(); - - QRect dirtyClient = region.boundingRect(); - QRect dirtyRegion(currentGeometry.left() + dirtyClient.left(), - currentGeometry.top() + dirtyClient.top(), - dirtyClient.width(), - dirtyClient.height()); - QRect mOldGeometryLocal = m_oldGeometry; - m_oldGeometry = currentGeometry; - // If this is a move, redraw the previous location - if (mOldGeometryLocal != currentGeometry) - platformScreen()->setDirty(mOldGeometryLocal); - platformScreen()->setDirty(dirtyRegion); -} - void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect) { if (rect == geometry()) @@ -67,8 +45,9 @@ void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect) m_oldGeometry = geometry(); QAndroidPlatformWindow::setGeometry(rect); - if (m_nativeSurfaceId != -1) - QtAndroid::setSurfaceGeometry(m_nativeSurfaceId, rect); + + + setNativeGeometry(rect); QRect availableGeometry = screen()->availableGeometry(); if (rect.width() > 0 @@ -77,25 +56,23 @@ void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect) && availableGeometry.height() > 0) { QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), rect.size())); } - - if (rect.topLeft() != m_oldGeometry.topLeft()) - repaint(QRegion(rect)); } EGLSurface QAndroidPlatformOpenGLWindow::eglSurface(EGLConfig config) { - if (QAndroidEventDispatcherStopper::stopped() || QGuiApplication::applicationState() == Qt::ApplicationSuspended) + if (QAndroidEventDispatcherStopper::stopped() || + QGuiApplication::applicationState() == Qt::ApplicationSuspended) { return m_eglSurface; + } QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1) { + if (!m_surfaceCreated) { AndroidDeadlockProtector protector; if (!protector.acquire()) return m_eglSurface; - const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); - m_nativeSurfaceId = QtAndroid::createSurface(this, geometry(), windowStaysOnTop, 32); + createSurface(); m_surfaceWaitCondition.wait(&m_surfaceMutex); } @@ -110,7 +87,7 @@ EGLSurface QAndroidPlatformOpenGLWindow::eglSurface(EGLConfig config) bool QAndroidPlatformOpenGLWindow::checkNativeSurface(EGLConfig config) { QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1 || !m_androidSurfaceObject.isValid()) + if (!m_surfaceCreated || !m_androidSurfaceObject.isValid()) return false; // makeCurrent is NOT needed. createEgl(config); @@ -127,10 +104,7 @@ void QAndroidPlatformOpenGLWindow::applicationStateChanged(Qt::ApplicationState QAndroidPlatformWindow::applicationStateChanged(state); if (state <= Qt::ApplicationHidden) { lockSurface(); - if (m_nativeSurfaceId != -1) { - QtAndroid::destroySurface(m_nativeSurfaceId); - m_nativeSurfaceId = -1; - } + destroySurface(); clearEgl(); unlockSurface(); } @@ -173,24 +147,4 @@ void QAndroidPlatformOpenGLWindow::clearEgl() } } -void QAndroidPlatformOpenGLWindow::surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) -{ - Q_UNUSED(jniEnv); - Q_UNUSED(w); - Q_UNUSED(h); - - lockSurface(); - m_androidSurfaceObject = surface; - if (surface) // wait until we have a valid surface to draw into - m_surfaceWaitCondition.wakeOne(); - unlockSurface(); - - if (surface) { - // repaint the window, when we have a valid surface - QRect availableGeometry = screen()->availableGeometry(); - if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) - QWindowSystemInterface::handleExposeEvent(window(), QRegion(QRect(QPoint(), geometry().size()))); - } -} - QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformopenglwindow.h b/src/plugins/platforms/android/qandroidplatformopenglwindow.h index 8c31368b65..c1ae57fe85 100644 --- a/src/plugins/platforms/android/qandroidplatformopenglwindow.h +++ b/src/plugins/platforms/android/qandroidplatformopenglwindow.h @@ -5,7 +5,6 @@ #ifndef QANDROIDPLATFORMOPENGLWINDOW_H #define QANDROIDPLATFORMOPENGLWINDOW_H -#include "androidsurfaceclient.h" #include "qandroidplatformwindow.h" #include <QWaitCondition> @@ -16,7 +15,7 @@ QT_BEGIN_NAMESPACE -class QAndroidPlatformOpenGLWindow : public QAndroidPlatformWindow, public AndroidSurfaceClient +class QAndroidPlatformOpenGLWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformOpenGLWindow(QWindow *window, EGLDisplay display); @@ -30,10 +29,7 @@ public: void applicationStateChanged(Qt::ApplicationState) override; - void repaint(const QRegion ®ion) override; - protected: - void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) override; void createEgl(EGLConfig config); void clearEgl(); @@ -42,9 +38,6 @@ private: EGLSurface m_eglSurface = EGL_NO_SURFACE; EGLNativeWindowType m_nativeWindow = nullptr; - int m_nativeSurfaceId = -1; - QJniObject m_androidSurfaceObject; - QWaitCondition m_surfaceWaitCondition; QSurfaceFormat m_format; QRect m_oldGeometry; }; diff --git a/src/plugins/platforms/android/qandroidplatformscreen.cpp b/src/plugins/platforms/android/qandroidplatformscreen.cpp index d11b7b197b..4d752a3cc3 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.cpp +++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp @@ -8,7 +8,6 @@ #include <qpa/qwindowsysteminterface.h> #include "qandroidplatformscreen.h" -#include "qandroidplatformbackingstore.h" #include "qandroidplatformintegration.h" #include "qandroidplatformwindow.h" #include "androidjnimain.h" @@ -52,11 +51,23 @@ private: # define PROFILE_SCOPE #endif -QAndroidPlatformScreen::QAndroidPlatformScreen() +Q_DECLARE_JNI_CLASS(Display, "android/view/Display") +Q_DECLARE_JNI_CLASS(DisplayMetrics, "android/util/DisplayMetrics") +Q_DECLARE_JNI_CLASS(Resources, "android/content/res/Resources") +Q_DECLARE_JNI_CLASS(Size, "android/util/Size") +Q_DECLARE_JNI_CLASS(QtNative, "org/qtproject/qt/android/QtNative") +Q_DECLARE_JNI_CLASS(QtDisplayManager, "org/qtproject/qt/android/QtDisplayManager") +Q_DECLARE_JNI_CLASS(QtWindowInterface, "org/qtproject/qt/android/QtWindowInterface") + +Q_DECLARE_JNI_CLASS(DisplayMode, "android/view/Display$Mode") + +QAndroidPlatformScreen::QAndroidPlatformScreen(const QJniObject &displayObject) : QObject(), QPlatformScreen() { m_availableGeometry = QAndroidPlatformIntegration::m_defaultAvailableGeometry; m_size = QAndroidPlatformIntegration::m_defaultScreenSize; + m_physicalSize = QAndroidPlatformIntegration::m_defaultPhysicalSize; + // Raster only apps should set QT_ANDROID_RASTER_IMAGE_DEPTH to 16 // is way much faster than 32 if (qEnvironmentVariableIntValue("QT_ANDROID_RASTER_IMAGE_DEPTH") == 16) { @@ -66,69 +77,72 @@ QAndroidPlatformScreen::QAndroidPlatformScreen() m_format = QImage::Format_ARGB32_Premultiplied; m_depth = 32; } - m_physicalSize = QAndroidPlatformIntegration::m_defaultPhysicalSize; - connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QAndroidPlatformScreen::applicationStateChanged); - - QJniObject activity(QtAndroid::activity()); - if (!activity.isValid()) - return; - QJniObject display; - if (QNativeInterface::QAndroidApplication::sdkVersion() < 30) { - display = activity.callObjectMethod("getWindowManager", "()Landroid/view/WindowManager;") - .callObjectMethod("getDefaultDisplay", "()Landroid/view/Display;"); - } else { - display = activity.callObjectMethod("getDisplay", "()Landroid/view/Display;"); - } - if (!display.isValid()) - return; - m_name = display.callObjectMethod("getName", "()Ljava/lang/String;").toString(); - m_refreshRate = display.callMethod<jfloat>("getRefreshRate"); + connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, + &QAndroidPlatformScreen::applicationStateChanged); - if (QNativeInterface::QAndroidApplication::sdkVersion() < 23) { - m_modes << Mode { .size = m_physicalSize.toSize(), .refreshRate = m_refreshRate }; + if (!displayObject.isValid()) return; - } - QJniEnvironment env; - const jint currentMode = display.callObjectMethod("getMode", "()Landroid/view/Display$Mode;") - .callMethod<jint>("getModeId"); - const auto modes = display.callObjectMethod("getSupportedModes", - "()[Landroid/view/Display$Mode;"); - const auto modesArray = jobjectArray(modes.object()); - const auto sz = env->GetArrayLength(modesArray); - for (jsize i = 0; i < sz; ++i) { - auto mode = QJniObject::fromLocalRef(env->GetObjectArrayElement(modesArray, i)); - if (currentMode == mode.callMethod<jint>("getModeId")) - m_currentMode = m_modes.size(); - m_modes << Mode { .size = QSize { mode.callMethod<jint>("getPhysicalHeight"), - mode.callMethod<jint>("getPhysicalWidth") }, - .refreshRate = mode.callMethod<jfloat>("getRefreshRate") }; + m_name = displayObject.callObjectMethod<jstring>("getName").toString(); + m_refreshRate = displayObject.callMethod<jfloat>("getRefreshRate"); + m_displayId = displayObject.callMethod<jint>("getDisplayId"); + + const QJniObject context = QNativeInterface::QAndroidApplication::context(); + const auto displayContext = context.callMethod<QtJniTypes::Context>("createDisplayContext", + displayObject.object<QtJniTypes::Display>()); + + const auto sizeObj = QtJniTypes::QtDisplayManager::callStaticMethod<QtJniTypes::Size>( + "getDisplaySize", displayContext, + displayObject.object<QtJniTypes::Display>()); + m_size = QSize(sizeObj.callMethod<int>("getWidth"), sizeObj.callMethod<int>("getHeight")); + + const auto resources = displayContext.callMethod<QtJniTypes::Resources>("getResources"); + const auto metrics = resources.callMethod<QtJniTypes::DisplayMetrics>("getDisplayMetrics"); + const float xdpi = metrics.getField<float>("xdpi"); + const float ydpi = metrics.getField<float>("ydpi"); + + // Potentially densityDpi could be used instead of xpdi/ydpi to do the calculation, + // but the results are not consistent with devices specs. + // (https://issuetracker.google.com/issues/194120500) + m_physicalSize.setWidth(qRound(m_size.width() / xdpi * 25.4)); + m_physicalSize.setHeight(qRound(m_size.height() / ydpi * 25.4)); + + if (QNativeInterface::QAndroidApplication::sdkVersion() >= 23) { + const QJniObject currentMode = displayObject.callObjectMethod<QtJniTypes::DisplayMode>("getMode"); + m_currentMode = currentMode.callMethod<jint>("getModeId"); + + const QJniObject supportedModes = displayObject.callObjectMethod<QtJniTypes::DisplayMode[]>( + "getSupportedModes"); + const auto modeArray = jobjectArray(supportedModes.object()); + + QJniEnvironment env; + const auto size = env->GetArrayLength(modeArray); + for (jsize i = 0; i < size; ++i) { + const auto mode = QJniObject::fromLocalRef(env->GetObjectArrayElement(modeArray, i)); + m_modes << QPlatformScreen::Mode { + .size = QSize { mode.callMethod<jint>("getPhysicalWidth"), + mode.callMethod<jint>("getPhysicalHeight") }, + .refreshRate = mode.callMethod<jfloat>("getRefreshRate") + }; + } } - - if (m_modes.isEmpty()) - m_modes << Mode { .size = m_physicalSize.toSize(), .refreshRate = m_refreshRate }; } QAndroidPlatformScreen::~QAndroidPlatformScreen() { - if (m_id != -1) { - QtAndroid::destroySurface(m_id); - m_surfaceWaitCondition.wakeOne(); - releaseSurface(); - } } -QWindow *QAndroidPlatformScreen::topWindow() const +QWindow *QAndroidPlatformScreen::topVisibleWindow() const { for (QAndroidPlatformWindow *w : m_windowStack) { - if (w->window()->type() == Qt::Window || - w->window()->type() == Qt::Popup || - w->window()->type() == Qt::Dialog) { + Qt::WindowType type = w->window()->type(); + if (w->window()->isVisible() && + (type == Qt::Window || type == Qt::Popup || type == Qt::Dialog)) { return w->window(); } } - return 0; + return nullptr; } QWindow *QAndroidPlatformScreen::topLevelAt(const QPoint &p) const @@ -140,16 +154,6 @@ QWindow *QAndroidPlatformScreen::topLevelAt(const QPoint &p) const return 0; } -bool QAndroidPlatformScreen::event(QEvent *event) -{ - if (event->type() == QEvent::UpdateRequest) { - doRedraw(); - m_updatePending = false; - return true; - } - return QObject::event(event); -} - void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) { if (window->parent() && window->isRaster()) @@ -159,83 +163,56 @@ void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) return; m_windowStack.prepend(window); - if (window->isRaster()) { - m_rasterSurfaces.ref(); - setDirty(window->geometry()); - } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("addTopLevelWindow", + window->nativeWindow()); + + if (window->window()->isVisible()) + topVisibleWindowChanged(); } void QAndroidPlatformScreen::removeWindow(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - m_windowStack.removeOne(window); if (m_windowStack.contains(window)) qWarning() << "Failed to remove window"; - if (window->isRaster()) { - m_rasterSurfaces.deref(); - setDirty(window->geometry()); - } + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("removeTopLevelWindow", + window->nativeViewId()); - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::raise(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - int index = m_windowStack.indexOf(window); - if (index <= 0) + if (index < 0) return; - m_windowStack.move(index, 0); - if (window->isRaster()) { - setDirty(window->geometry()); + if (index > 0) { + m_windowStack.move(index, 0); + + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("bringChildToFront", + window->nativeViewId()); } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::lower(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - int index = m_windowStack.indexOf(window); if (index == -1 || index == (m_windowStack.size() - 1)) return; m_windowStack.move(index, m_windowStack.size() - 1); - if (window->isRaster()) { - setDirty(window->geometry()); - } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); -} -void QAndroidPlatformScreen::scheduleUpdate() -{ - if (!m_updatePending) { - m_updatePending = true; - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); - } -} + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("bringChildToBack", + window->nativeViewId()); -void QAndroidPlatformScreen::setDirty(const QRect &rect) -{ - QRect intersection = rect.intersected(m_availableGeometry); - m_dirtyRect |= intersection; - scheduleUpdate(); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::setPhysicalSize(const QSize &size) @@ -267,6 +244,11 @@ void QAndroidPlatformScreen::setSizeParameters(const QSize &physicalSize, const } } +int QAndroidPlatformScreen::displayId() const +{ + return m_displayId; +} + void QAndroidPlatformScreen::setRefreshRate(qreal refreshRate) { if (refreshRate == m_refreshRate) @@ -282,7 +264,6 @@ void QAndroidPlatformScreen::setOrientation(Qt::ScreenOrientation orientation) void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) { - QMutexLocker lock(&m_surfaceMutex); if (m_availableGeometry == rect) return; @@ -303,170 +284,31 @@ void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) } } } - - if (m_id != -1) { - releaseSurface(); - QtAndroid::setSurfaceGeometry(m_id, rect); - } } void QAndroidPlatformScreen::applicationStateChanged(Qt::ApplicationState state) { - for (QAndroidPlatformWindow *w : qAsConst(m_windowStack)) + for (QAndroidPlatformWindow *w : std::as_const(m_windowStack)) w->applicationStateChanged(state); - - if (state <= Qt::ApplicationHidden) { - lockSurface(); - QtAndroid::destroySurface(m_id); - m_id = -1; - releaseSurface(); - unlockSurface(); - } } -void QAndroidPlatformScreen::topWindowChanged(QWindow *w) +void QAndroidPlatformScreen::topVisibleWindowChanged() { + QWindow *w = topVisibleWindow(); + QWindowSystemInterface::handleFocusWindowChanged(w, Qt::ActiveWindowFocusReason); QtAndroidMenu::setActiveTopLevelWindow(w); - - if (w != 0) { + if (w && w->handle()) { QAndroidPlatformWindow *platformWindow = static_cast<QAndroidPlatformWindow *>(w->handle()); - if (platformWindow != 0) + if (platformWindow) platformWindow->updateSystemUiVisibility(); } } -int QAndroidPlatformScreen::rasterSurfaces() -{ - return m_rasterSurfaces; -} - -void QAndroidPlatformScreen::doRedraw(QImage* screenGrabImage) -{ - PROFILE_SCOPE; - if (!QtAndroid::activity()) - return; - - if (m_dirtyRect.isEmpty()) - return; - - // Stop if there are no visible raster windows. If we only have RasterGLSurface - // windows that have renderToTexture children (i.e. they need the OpenGL path) then - // we do not need an overlay surface. - bool hasVisibleRasterWindows = false; - for (QAndroidPlatformWindow *window : qAsConst(m_windowStack)) { - if (window->window()->isVisible() && window->isRaster() && !qt_window_private(window->window())->compositing) { - hasVisibleRasterWindows = true; - break; - } - } - if (!hasVisibleRasterWindows) { - lockSurface(); - if (m_id != -1) { - QtAndroid::destroySurface(m_id); - releaseSurface(); - m_id = -1; - } - unlockSurface(); - return; - } - QMutexLocker lock(&m_surfaceMutex); - if (m_id == -1 && m_rasterSurfaces) { - m_id = QtAndroid::createSurface(this, geometry(), true, m_depth); - AndroidDeadlockProtector protector; - if (!protector.acquire()) - return; - m_surfaceWaitCondition.wait(&m_surfaceMutex); - } - - if (!m_nativeSurface) - return; - - ANativeWindow_Buffer nativeWindowBuffer; - ARect nativeWindowRect; - nativeWindowRect.top = m_dirtyRect.top(); - nativeWindowRect.left = m_dirtyRect.left(); - nativeWindowRect.bottom = m_dirtyRect.bottom() + 1; // for some reason that I don't understand the QRect bottom needs to +1 to be the same with ARect bottom - nativeWindowRect.right = m_dirtyRect.right() + 1; // same for the right - - int ret; - if ((ret = ANativeWindow_lock(m_nativeSurface, &nativeWindowBuffer, &nativeWindowRect)) < 0) { - qWarning() << "ANativeWindow_lock() failed! error=" << ret; - return; - } - - int bpp = 4; - if (nativeWindowBuffer.format == WINDOW_FORMAT_RGB_565) { - bpp = 2; - m_pixelFormat = QImage::Format_RGB16; - } - - QImage screenImage(reinterpret_cast<uchar *>(nativeWindowBuffer.bits) - , nativeWindowBuffer.width, nativeWindowBuffer.height - , nativeWindowBuffer.stride * bpp , m_pixelFormat); - - QPainter compositePainter(&screenImage); - compositePainter.setCompositionMode(QPainter::CompositionMode_Source); - - QRegion visibleRegion(m_dirtyRect); - for (QAndroidPlatformWindow *window : qAsConst(m_windowStack)) { - if (!window->window()->isVisible() - || qt_window_private(window->window())->compositing - || !window->isRaster()) - continue; - - for (const QRect &rect : std::vector<QRect>(visibleRegion.begin(), visibleRegion.end())) { - QRect targetRect = window->geometry(); - targetRect &= rect; - - if (targetRect.isNull()) - continue; - - visibleRegion -= targetRect; - QRect windowRect = targetRect.translated(-window->geometry().topLeft()); - QAndroidPlatformBackingStore *backingStore = static_cast<QAndroidPlatformWindow *>(window)->backingStore(); - if (backingStore) - compositePainter.drawImage(targetRect.topLeft(), backingStore->toImage(), windowRect); - } - } - - for (const QRect &rect : visibleRegion) - compositePainter.fillRect(rect, QColor(Qt::transparent)); - - ret = ANativeWindow_unlockAndPost(m_nativeSurface); - if (ret >= 0) - m_dirtyRect = QRect(); - - if (screenGrabImage) { - if (screenGrabImage->size() != screenImage.size()) { - uchar* bytes = static_cast<uchar*>(malloc(screenImage.height() * screenImage.bytesPerLine())); - *screenGrabImage = QImage(bytes, screenImage.width(), screenImage.height(), - screenImage.bytesPerLine(), m_pixelFormat, - [](void* ptr){ if (ptr) free (ptr);}); - } - memcpy(screenGrabImage->bits(), - screenImage.bits(), - screenImage.bytesPerLine() * screenImage.height()); - } - m_repaintOccurred = true; -} - -QPixmap QAndroidPlatformScreen::doScreenShot(QRect grabRect) -{ - if (!m_repaintOccurred) - return QPixmap::fromImage(m_lastScreenshot.copy(grabRect)); - QRect tmp = m_dirtyRect; - m_dirtyRect = geometry(); - doRedraw(&m_lastScreenshot); - m_dirtyRect = tmp; - m_repaintOccurred = false; - return QPixmap::fromImage(m_lastScreenshot.copy(grabRect)); -} - static const int androidLogicalDpi = 72; QDpi QAndroidPlatformScreen::logicalDpi() const { - qreal lDpi = QtAndroid::scaledDensity() * androidLogicalDpi; + qreal lDpi = QtAndroid::pixelDensity() * androidLogicalDpi; return QDpi(lDpi, lDpi); } @@ -484,67 +326,4 @@ Qt::ScreenOrientation QAndroidPlatformScreen::nativeOrientation() const { return QAndroidPlatformIntegration::m_nativeOrientation; } - -void QAndroidPlatformScreen::surfaceChanged(JNIEnv *env, jobject surface, int w, int h) -{ - lockSurface(); - if (surface && w > 0 && h > 0) { - releaseSurface(); - m_nativeSurface = ANativeWindow_fromSurface(env, surface); - QMetaObject::invokeMethod(this, "setDirty", Qt::QueuedConnection, Q_ARG(QRect, QRect(0, 0, w, h))); - } else { - releaseSurface(); - } - unlockSurface(); - m_surfaceWaitCondition.wakeOne(); -} - -void QAndroidPlatformScreen::releaseSurface() -{ - if (m_nativeSurface) { - ANativeWindow_release(m_nativeSurface); - m_nativeSurface = 0; - } -} - -/*! - This function is called when Qt needs to be able to grab the content of a window. - - Returns the content of the window specified with the WId handle within the boundaries of - QRect(x, y, width, height). -*/ -QPixmap QAndroidPlatformScreen::grabWindow(WId window, int x, int y, int width, int height) const -{ - QRectF screenshotRect(x, y, width, height); - QWindow* wnd = 0; - if (window) - { - const auto windowList = qApp->allWindows(); - for (QWindow *w : windowList) - if (w->winId() == window) { - wnd = w; - break; - } - } - if (wnd) { - const qreal factor = logicalDpi().first / androidLogicalDpi; //HighDPI factor; - QRectF wndRect = wnd->geometry(); - if (wnd->parent()) - wndRect.moveTopLeft(wnd->parent()->mapToGlobal(wndRect.topLeft().toPoint())); - if (!qFuzzyCompare(factor, 1)) - wndRect = QRectF(wndRect.left() * factor, wndRect.top() * factor, - wndRect.width() * factor, wndRect.height() * factor); - - if (!screenshotRect.isEmpty()) { - screenshotRect.moveTopLeft(wndRect.topLeft() + screenshotRect.topLeft()); - screenshotRect = screenshotRect.intersected(wndRect); - } else { - screenshotRect = wndRect; - } - } else { - screenshotRect = screenshotRect.isValid() ? screenshotRect : geometry(); - } - return const_cast<QAndroidPlatformScreen *>(this)->doScreenShot(screenshotRect.toRect()); -} - QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformscreen.h b/src/plugins/platforms/android/qandroidplatformscreen.h index ef8371e72d..d850d0db09 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.h +++ b/src/plugins/platforms/android/qandroidplatformscreen.h @@ -5,26 +5,26 @@ #ifndef QANDROIDPLATFORMSCREEN_H #define QANDROIDPLATFORMSCREEN_H -#include "androidsurfaceclient.h" - #include <QList> #include <QPainter> #include <QTimer> #include <QWaitCondition> #include <QtCore/QJniObject> #include <qpa/qplatformscreen.h> - -#include <android/native_window.h> +#include <QtGui/qscreen_platform.h> QT_BEGIN_NAMESPACE class QAndroidPlatformWindow; -class QAndroidPlatformScreen: public QObject, public QPlatformScreen, public AndroidSurfaceClient + +class QAndroidPlatformScreen : public QObject, + public QPlatformScreen, + public QNativeInterface::QAndroidScreen { Q_OBJECT public: - QAndroidPlatformScreen(); + QAndroidPlatformScreen(const QJniObject &displayObject); ~QAndroidPlatformScreen(); QRect geometry() const override { return QRect(QPoint(), m_size); } @@ -38,22 +38,17 @@ public: int currentMode() const override { return m_currentMode; } int preferredMode() const override { return m_currentMode; } qreal refreshRate() const override { return m_refreshRate; } - - inline QWindow *topWindow() const; + inline QWindow *topVisibleWindow() const; QWindow *topLevelAt(const QPoint & p) const override; - // compositor api void addWindow(QAndroidPlatformWindow *window); void removeWindow(QAndroidPlatformWindow *window); void raise(QAndroidPlatformWindow *window); void lower(QAndroidPlatformWindow *window); - - void scheduleUpdate(); - void topWindowChanged(QWindow *w); - int rasterSurfaces(); + void topVisibleWindowChanged(); + int displayId() const override; public slots: - void setDirty(const QRect &rect); void setPhysicalSize(const QSize &size); void setAvailableGeometry(const QRect &rect); void setSize(const QSize &size); @@ -63,13 +58,8 @@ public slots: void setOrientation(Qt::ScreenOrientation orientation); protected: - bool event(QEvent *event) override; - typedef QList<QAndroidPlatformWindow *> WindowStackType; WindowStackType m_windowStack; - QRect m_dirtyRect; - bool m_updatePending = false; - QRect m_availableGeometry; int m_depth; QImage::Format m_format; @@ -78,31 +68,16 @@ protected: QString m_name; QList<Mode> m_modes; int m_currentMode = 0; + int m_displayId = -1; private: QDpi logicalDpi() const override; QDpi logicalBaseDpi() const override; Qt::ScreenOrientation orientation() const override; Qt::ScreenOrientation nativeOrientation() const override; - QPixmap grabWindow(WId window, int x, int y, int width, int height) const override; - void surfaceChanged(JNIEnv *env, jobject surface, int w, int h) override; - void releaseSurface(); void applicationStateChanged(Qt::ApplicationState); - QPixmap doScreenShot(QRect grabRect = QRect()); - -private slots: - void doRedraw(QImage *screenGrabImage = nullptr); - private: - int m_id = -1; - QAtomicInt m_rasterSurfaces = 0; - ANativeWindow* m_nativeSurface = nullptr; - QWaitCondition m_surfaceWaitCondition; QSize m_size; - - QImage m_lastScreenshot; - QImage::Format m_pixelFormat = QImage::Format_RGBA8888_Premultiplied; - bool m_repaintOccurred = false; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformservices.cpp b/src/plugins/platforms/android/qandroidplatformservices.cpp index 8f8f702011..39287aa905 100644 --- a/src/plugins/platforms/android/qandroidplatformservices.cpp +++ b/src/plugins/platforms/android/qandroidplatformservices.cpp @@ -24,17 +24,25 @@ QAndroidPlatformServices::QAndroidPlatformServices() QtAndroidPrivate::registerNewIntentListener(this); - QMetaObject::invokeMethod( - this, - [this] { - QJniObject context = QJniObject(QtAndroidPrivate::context()); - QJniObject intent = - context.callObjectMethod("getIntent", "()Landroid/content/Intent;"); - handleNewIntent(nullptr, intent.object()); - }, - Qt::QueuedConnection); + // Qt applications without Activity contexts cannot retrieve intents from the Activity. + if (QNativeInterface::QAndroidApplication::isActivityContext()) { + QMetaObject::invokeMethod( + this, + [this] { + QJniObject context = QJniObject(QtAndroidPrivate::context()); + QJniObject intent = + context.callObjectMethod("getIntent", "()Landroid/content/Intent;"); + handleNewIntent(nullptr, intent.object()); + }, + Qt::QueuedConnection); + } } +Q_DECLARE_JNI_CLASS(UriType, "android/net/Uri") +Q_DECLARE_JNI_CLASS(FileType, "java/io/File") +Q_DECLARE_JNI_CLASS(File, "java/io/File") +Q_DECLARE_JNI_CLASS(FileProvider, "androidx/core/content/FileProvider"); + bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) { QString mime; @@ -47,20 +55,44 @@ bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) // if the file is local, we need to pass the MIME type, otherwise Android // does not start an Intent to view this file const auto fileScheme = "file"_L1; - if ((url.scheme().isEmpty() || url.scheme() == fileScheme) && QFile::exists(url.path())) { - // a real URL including the scheme is needed, else the Intent can not be started + + // a real URL including the scheme is needed, else the Intent can not be started + if (url.scheme().isEmpty()) url.setScheme(fileScheme); - QMimeDatabase mimeDb; - mime = mimeDb.mimeTypeForUrl(url).name(); - } + + if (url.scheme() == fileScheme) + mime = QMimeDatabase().mimeTypeForUrl(url).name(); + + const QJniObject mimeString = QJniObject::fromString(mime); using namespace QNativeInterface; - QJniObject urlString = QJniObject::fromString(url.toString()); - QJniObject mimeString = QJniObject::fromString(mime); - return QJniObject::callStaticMethod<jboolean>( - QtAndroid::applicationClass(), "openURL", - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z", - QAndroidApplication::context(), urlString.object(), mimeString.object()); + + auto openUrl = [mimeString](const QJniObject &url) { + return QJniObject::callStaticMethod<jboolean>(QtAndroid::applicationClass(), "openURL", + QAndroidApplication::context(), url.object<jstring>(), mimeString.object<jstring>()); + }; + + if (url.scheme() != fileScheme || QNativeInterface::QAndroidApplication::sdkVersion() < 24) + return openUrl(QJniObject::fromString(url.toString())); + + // Use FileProvider for file scheme with sdk >= 24 + const QJniObject context = QAndroidApplication::context(); + const auto appId = context.callMethod<jstring>("getPackageName").toString(); + const auto providerName = QJniObject::fromString(appId + ".qtprovider"_L1); + + const auto urlPath = QJniObject::fromString(url.path()); + const auto urlFile = QJniObject(QtJniTypes::Traits<QtJniTypes::File>::className(), + urlPath.object<jstring>()); + + const auto fileProviderUri = QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::FileProvider>::className(), "getUriForFile", + QAndroidApplication::context(), providerName.object<jstring>(), + urlFile.object<QtJniTypes::FileType>()); + + if (fileProviderUri.isValid()) + return openUrl(fileProviderUri.callMethod<jstring>("toString")); + + return false; } bool QAndroidPlatformServices::openDocument(const QUrl &url) diff --git a/src/plugins/platforms/android/qandroidplatformtheme.cpp b/src/plugins/platforms/android/qandroidplatformtheme.cpp index c5b9ba9dee..99eeabac1d 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.cpp +++ b/src/plugins/platforms/android/qandroidplatformtheme.cpp @@ -4,6 +4,7 @@ #include "androidjnimain.h" #include "androidjnimenu.h" #include "qandroidplatformtheme.h" +#include "qandroidplatformiconengine.h" #include "qandroidplatformmenubar.h" #include "qandroidplatformmenu.h" #include "qandroidplatformmenuitem.h" @@ -22,6 +23,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaMenus, "qt.qpa.menus") + using namespace Qt::StringLiterals; namespace { @@ -158,14 +161,13 @@ QJsonObject AndroidStyle::loadStyleData() if (!stylePath.isEmpty() && !stylePath.endsWith(slashChar)) stylePath += slashChar; - Q_ASSERT(!stylePath.isEmpty()); - - QString androidTheme = QLatin1StringView(qgetenv("QT_ANDROID_THEME")); - if (!androidTheme.isEmpty() && !androidTheme.endsWith(slashChar)) - androidTheme += slashChar; + const Qt::ColorScheme colorScheme = QAndroidPlatformTheme::instance() + ? QAndroidPlatformTheme::instance()->colorScheme() + : QAndroidPlatformIntegration::colorScheme(); + if (colorScheme == Qt::ColorScheme::Dark) + stylePath += "darkUiMode/"_L1; - if (!androidTheme.isEmpty() && QFileInfo::exists(stylePath + androidTheme + "style.json"_L1)) - stylePath += androidTheme; + Q_ASSERT(!stylePath.isEmpty()); QFile f(stylePath + "style.json"_L1); if (!f.open(QIODevice::ReadOnly)) @@ -185,13 +187,22 @@ QJsonObject AndroidStyle::loadStyleData() return document.object(); } -static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette) +static void loadAndroidStyle(QPalette *defaultPalette, std::shared_ptr<AndroidStyle> &style) { double pixelDensity = QHighDpiScaling::isActive() ? QtAndroid::pixelDensity() : 1.0; - std::shared_ptr<AndroidStyle> style = std::make_shared<AndroidStyle>(); + if (style) { + style->m_standardPalette = QPalette(); + style->m_palettes.clear(); + style->m_fonts.clear(); + style->m_QWidgetsFonts.clear(); + } else { + style = std::make_shared<AndroidStyle>(); + } + style->m_styleData = AndroidStyle::loadStyleData(); + if (style->m_styleData.isEmpty()) - return std::shared_ptr<AndroidStyle>(); + return; { QFont font("Droid Sans Mono"_L1, 14.0 * 100 / 72); @@ -293,11 +304,43 @@ static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette) // Extract palette information } } - return style; +} + +QAndroidPlatformTheme *QAndroidPlatformTheme::m_instance = nullptr; + +QAndroidPlatformTheme *QAndroidPlatformTheme::instance( + QAndroidPlatformNativeInterface *androidPlatformNativeInterface) +{ + if (androidPlatformNativeInterface && !m_instance) { + m_instance = new QAndroidPlatformTheme(androidPlatformNativeInterface); + } + return m_instance; } QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *androidPlatformNativeInterface) { + updateStyle(); + + androidPlatformNativeInterface->m_androidStyle = m_androidStyleData; + + // default in case the style has not set a font + m_systemFont = QFont("Roboto"_L1, 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi +} + +QAndroidPlatformTheme::~QAndroidPlatformTheme() +{ + m_instance = nullptr; +} + +void QAndroidPlatformTheme::updateColorScheme() +{ + updateStyle(); + QWindowSystemInterface::handleThemeChange(); +} + +void QAndroidPlatformTheme::updateStyle() +{ + QColor windowText = Qt::black; QColor background(229, 229, 229); QColor light = background.lighter(150); QColor mid(background.darker(130)); @@ -314,7 +357,27 @@ QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *an QColor highlight(148, 210, 231); QColor disabledShadow = shadow.lighter(150); - m_defaultPalette = QPalette(Qt::black,background,light,dark,mid,text,base); + if (colorScheme() == Qt::ColorScheme::Dark) { + // Colors were prepared based on Theme.DeviceDefault.DayNight + windowText = QColor(250, 250, 250); + background = QColor(48, 48, 48); + light = background.darker(150); + mid = background.lighter(130); + midLight = mid.darker(110); + base = background; + disabledBase = background; + dark = background.darker(150); + darkDisabled = dark.darker(110); + text = QColor(250, 250, 250); + highlightedText = QColor(250, 250, 250); + disabledText = QColor(96, 96, 96); + button = QColor(48, 48, 48); + shadow = QColor(32, 32, 32); + highlight = QColor(102, 178, 204); + disabledShadow = shadow.darker(150); + } + + m_defaultPalette = QPalette(windowText,background,light,dark,mid,text,base); m_defaultPalette.setBrush(QPalette::Midlight, midLight); m_defaultPalette.setBrush(QPalette::Button, button); m_defaultPalette.setBrush(QPalette::Shadow, shadow); @@ -330,34 +393,52 @@ QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *an m_defaultPalette.setBrush(QPalette::Active, QPalette::Highlight, highlight); m_defaultPalette.setBrush(QPalette::Inactive, QPalette::Highlight, highlight); m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Highlight, highlight.lighter(150)); - m_androidStyleData = loadAndroidStyle(&m_defaultPalette); - QGuiApplication::setPalette(m_defaultPalette); - androidPlatformNativeInterface->m_androidStyle = m_androidStyleData; - // default in case the style has not set a font - m_systemFont = QFont("Roboto"_L1, 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi + loadAndroidStyle(&m_defaultPalette, m_androidStyleData); } QPlatformMenuBar *QAndroidPlatformTheme::createPlatformMenuBar() const { - return new QAndroidPlatformMenuBar; + auto *menuBar = new QAndroidPlatformMenuBar; + qCDebug(lcQpaMenus) << "Created" << menuBar; + return menuBar; } QPlatformMenu *QAndroidPlatformTheme::createPlatformMenu() const { - return new QAndroidPlatformMenu; + auto *menu = new QAndroidPlatformMenu; + qCDebug(lcQpaMenus) << "Created" << menu; + return menu; } QPlatformMenuItem *QAndroidPlatformTheme::createPlatformMenuItem() const { - return new QAndroidPlatformMenuItem; + auto *menuItem = new QAndroidPlatformMenuItem; + qCDebug(lcQpaMenus) << "Created" << menuItem; + return menuItem; } void QAndroidPlatformTheme::showPlatformMenuBar() { + qCDebug(lcQpaMenus) << "Showing platform menu bar"; QtAndroidMenu::openOptionsMenu(); } +Qt::ColorScheme QAndroidPlatformTheme::colorScheme() const +{ + if (m_colorSchemeOverride != Qt::ColorScheme::Unknown) + return m_colorSchemeOverride; + return QAndroidPlatformIntegration::colorScheme(); +} + +void QAndroidPlatformTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + m_colorSchemeOverride = scheme; + QMetaObject::invokeMethod(qGuiApp, [this]{ + updateColorScheme(); + }); +} + static inline int paletteType(QPlatformTheme::Palette type) { switch (type) { @@ -423,6 +504,11 @@ const QFont *QAndroidPlatformTheme::font(Font type) const return 0; } +QIconEngine *QAndroidPlatformTheme::createIconEngine(const QString &iconName) const +{ + return new QAndroidPlatformIconEngine(iconName); +} + QVariant QAndroidPlatformTheme::themeHint(ThemeHint hint) const { switch (hint) { diff --git a/src/plugins/platforms/android/qandroidplatformtheme.h b/src/plugins/platforms/android/qandroidplatformtheme.h index ec39ed4794..1b4ab5664d 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.h +++ b/src/plugins/platforms/android/qandroidplatformtheme.h @@ -9,13 +9,15 @@ #include <QtGui/qpalette.h> #include <QtCore/qhash.h> #include <QtCore/qbytearray.h> - +#include <QtCore/qloggingcategory.h> #include <QJsonObject> #include <memory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaMenus) + struct AndroidStyle { static QJsonObject loadStyleData(); @@ -30,23 +32,34 @@ class QAndroidPlatformNativeInterface; class QAndroidPlatformTheme: public QPlatformTheme { public: - QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface); + ~QAndroidPlatformTheme(); + void updateColorScheme(); + void updateStyle(); QPlatformMenuBar *createPlatformMenuBar() const override; QPlatformMenu *createPlatformMenu() const override; QPlatformMenuItem *createPlatformMenuItem() const override; void showPlatformMenuBar() override; + Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; + const QPalette *palette(Palette type = SystemPalette) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; QString standardButtonText(int button) const override; bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; + static QAndroidPlatformTheme *instance( + QAndroidPlatformNativeInterface * androidPlatformNativeInterface = nullptr); private: + QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface); + static QAndroidPlatformTheme * m_instance; std::shared_ptr<AndroidStyle> m_androidStyleData; QPalette m_defaultPalette; QFont m_systemFont; + Qt::ColorScheme m_colorSchemeOverride = Qt::ColorScheme::Unknown; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp b/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp index cb6bb6d390..4bf4f44fa1 100644 --- a/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp @@ -18,7 +18,6 @@ QT_BEGIN_NAMESPACE QAndroidPlatformVulkanWindow::QAndroidPlatformVulkanWindow(QWindow *window) : QAndroidPlatformWindow(window), - m_nativeSurfaceId(-1), m_nativeWindow(nullptr), m_vkSurface(0), m_createVkSurface(nullptr), @@ -29,11 +28,7 @@ QAndroidPlatformVulkanWindow::QAndroidPlatformVulkanWindow(QWindow *window) QAndroidPlatformVulkanWindow::~QAndroidPlatformVulkanWindow() { m_surfaceWaitCondition.wakeOne(); - lockSurface(); - if (m_nativeSurfaceId != -1) - QtAndroid::destroySurface(m_nativeSurfaceId); - clearSurface(); - unlockSurface(); + destroyAndClearSurface(); } void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) @@ -44,8 +39,8 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) m_oldGeometry = geometry(); QAndroidPlatformWindow::setGeometry(rect); - if (m_nativeSurfaceId != -1) - QtAndroid::setSurfaceGeometry(m_nativeSurfaceId, rect); + if (m_surfaceCreated) + setNativeGeometry(rect); QRect availableGeometry = screen()->availableGeometry(); if (rect.width() > 0 @@ -54,22 +49,13 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) && availableGeometry.height() > 0) { QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), rect.size())); } - - if (rect.topLeft() != m_oldGeometry.topLeft()) - repaint(QRegion(rect)); } void QAndroidPlatformVulkanWindow::applicationStateChanged(Qt::ApplicationState state) { QAndroidPlatformWindow::applicationStateChanged(state); if (state <= Qt::ApplicationHidden) { - lockSurface(); - if (m_nativeSurfaceId != -1) { - QtAndroid::destroySurface(m_nativeSurfaceId); - m_nativeSurfaceId = -1; - } - clearSurface(); - unlockSurface(); + destroyAndClearSurface(); } } @@ -91,27 +77,12 @@ void QAndroidPlatformVulkanWindow::clearSurface() } } -void QAndroidPlatformVulkanWindow::sendExpose() -{ - QRect availableGeometry = screen()->availableGeometry(); - if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) - QWindowSystemInterface::handleExposeEvent(window(), QRegion(QRect(QPoint(), geometry().size()))); -} - -void QAndroidPlatformVulkanWindow::surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) +void QAndroidPlatformVulkanWindow::destroyAndClearSurface() { - Q_UNUSED(jniEnv); - Q_UNUSED(w); - Q_UNUSED(h); - lockSurface(); - m_androidSurfaceObject = surface; - if (surface) - m_surfaceWaitCondition.wakeOne(); + destroySurface(); + clearSurface(); unlockSurface(); - - if (surface) - sendExpose(); } VkSurfaceKHR *QAndroidPlatformVulkanWindow::vkSurface() @@ -124,16 +95,15 @@ VkSurfaceKHR *QAndroidPlatformVulkanWindow::vkSurface() clearSurface(); QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1) { + if (!m_surfaceCreated) { AndroidDeadlockProtector protector; if (!protector.acquire()) return &m_vkSurface; - const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); - m_nativeSurfaceId = QtAndroid::createSurface(this, geometry(), windowStaysOnTop, 32); + createSurface(); m_surfaceWaitCondition.wait(&m_surfaceMutex); } - if (m_nativeSurfaceId == -1 || !m_androidSurfaceObject.isValid()) + if (!m_surfaceCreated || !m_androidSurfaceObject.isValid()) return &m_vkSurface; QJniEnvironment env; diff --git a/src/plugins/platforms/android/qandroidplatformvulkanwindow.h b/src/plugins/platforms/android/qandroidplatformvulkanwindow.h index 14d5b7fc0e..fa959239d1 100644 --- a/src/plugins/platforms/android/qandroidplatformvulkanwindow.h +++ b/src/plugins/platforms/android/qandroidplatformvulkanwindow.h @@ -10,7 +10,6 @@ #define VK_USE_PLATFORM_ANDROID_KHR -#include "androidsurfaceclient.h" #include "qandroidplatformvulkaninstance.h" #include "qandroidplatformwindow.h" @@ -20,7 +19,7 @@ QT_BEGIN_NAMESPACE -class QAndroidPlatformVulkanWindow : public QAndroidPlatformWindow, public AndroidSurfaceClient +class QAndroidPlatformVulkanWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformVulkanWindow(QWindow *window); @@ -32,17 +31,11 @@ public: VkSurfaceKHR *vkSurface(); -protected: - void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) override; - private: - void sendExpose(); void clearSurface(); + void destroyAndClearSurface(); - int m_nativeSurfaceId; ANativeWindow *m_nativeWindow; - QJniObject m_androidSurfaceObject; - QWaitCondition m_surfaceWaitCondition; QSurfaceFormat m_format; QRect m_oldGeometry; VkSurfaceKHR m_vkSurface; diff --git a/src/plugins/platforms/android/qandroidplatformwindow.cpp b/src/plugins/platforms/android/qandroidplatformwindow.cpp index 76c034db50..2482160573 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformwindow.cpp @@ -14,40 +14,102 @@ QT_BEGIN_NAMESPACE -static QBasicAtomicInt winIdGenerator = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window") + +Q_DECLARE_JNI_CLASS(QtInputInterface, "org/qtproject/qt/android/QtInputInterface") +Q_DECLARE_JNI_CLASS(QtInputConnectionListener, + "org/qtproject/qt/android/QtInputConnection$QtInputConnectionListener") QAndroidPlatformWindow::QAndroidPlatformWindow(QWindow *window) - : QPlatformWindow(window) + : QPlatformWindow(window), m_nativeQtWindow(nullptr), + m_surfaceContainerType(SurfaceContainer::TextureView), m_nativeParentQtWindow(nullptr), + m_androidSurfaceObject(nullptr) { m_windowFlags = Qt::Widget; m_windowState = Qt::WindowNoState; - m_windowId = winIdGenerator.fetchAndAddRelaxed(1) + 1; + // the surfaceType is overwritten in QAndroidPlatformOpenGLWindow ctor so let's save + // the fact that it's a raster window for now + m_isRaster = window->surfaceType() == QSurface::RasterSurface; setWindowState(window->windowStates()); // the following is in relation to the virtual geometry const bool forceMaximize = m_windowState & (Qt::WindowMaximized | Qt::WindowFullScreen); - const QRect requestedNativeGeometry = - forceMaximize ? QRect() : QHighDpi::toNativePixels(window->geometry(), window); - const QRect availableDeviceIndependentGeometry = (window->parent()) - ? window->parent()->geometry() - : QHighDpi::fromNativePixels(platformScreen()->availableGeometry(), window); + const QRect nativeScreenGeometry = platformScreen()->availableGeometry(); + if (forceMaximize) { + setGeometry(nativeScreenGeometry); + } else { + const QRect requestedNativeGeometry = QHighDpi::toNativePixels(window->geometry(), window); + const QRect availableDeviceIndependentGeometry = (window->parent()) + ? window->parent()->geometry() + : QHighDpi::fromNativePixels(nativeScreenGeometry, window); + // initialGeometry returns in native pixels + const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( + window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), + availableDeviceIndependentGeometry.height()); + if (requestedNativeGeometry != finalNativeGeometry) + setGeometry(finalNativeGeometry); + } + + if (isEmbeddingContainer()) + return; - // initialGeometry returns in native pixels - const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( - window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), - availableDeviceIndependentGeometry.height()); + if (parent()) { + QAndroidPlatformWindow *androidParent = static_cast<QAndroidPlatformWindow*>(parent()); + if (!androidParent->isEmbeddingContainer()) + m_nativeParentQtWindow = androidParent->nativeWindow(); + } + + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + QtJniTypes::QtInputConnectionListener listener = + reg->callInterface<QtJniTypes::QtInputInterface, QtJniTypes::QtInputConnectionListener>( + "getInputConnectionListener"); + + m_nativeQtWindow = QJniObject::construct<QtJniTypes::QtWindow>( + QNativeInterface::QAndroidApplication::context(), m_nativeParentQtWindow, listener); + m_nativeViewId = m_nativeQtWindow.callMethod<jint>("getId"); + + if (window->isTopLevel()) + platformScreen()->addWindow(this); - if (requestedNativeGeometry != finalNativeGeometry) - setGeometry(finalNativeGeometry); + static bool ok = false; + static const int value = qEnvironmentVariableIntValue("QT_ANDROID_SURFACE_CONTAINER_TYPE", &ok); + if (ok) { + static const SurfaceContainer type = static_cast<SurfaceContainer>(value); + if (type == SurfaceContainer::SurfaceView || type == SurfaceContainer::TextureView) + m_surfaceContainerType = type; + } else if (platformScreen()->windows().size() <= 1) { + // TODO should handle case where this changes at runtime -> need to change existing window + // into TextureView (or perhaps not, if the parent window would be SurfaceView, as long as + // onTop was false it would stay below the children) + m_surfaceContainerType = SurfaceContainer::SurfaceView; + } + qCDebug(lcQpaWindow) << "Window" << m_nativeViewId << "using surface container type" + << static_cast<int>(m_surfaceContainerType); +} + +QAndroidPlatformWindow::~QAndroidPlatformWindow() +{ + if (window()->isTopLevel()) + platformScreen()->removeWindow(this); } + void QAndroidPlatformWindow::lower() { + if (m_nativeParentQtWindow.isValid()) { + m_nativeParentQtWindow.callMethod<void>("bringChildToBack", nativeViewId()); + return; + } platformScreen()->lower(this); } void QAndroidPlatformWindow::raise() { + if (m_nativeParentQtWindow.isValid()) { + m_nativeParentQtWindow.callMethod<void>("bringChildToFront", nativeViewId()); + QWindowSystemInterface::handleFocusWindowChanged(window(), Qt::ActiveWindowFocusReason); + return; + } updateSystemUiVisibility(); platformScreen()->raise(this); } @@ -71,23 +133,25 @@ void QAndroidPlatformWindow::setGeometry(const QRect &rect) void QAndroidPlatformWindow::setVisible(bool visible) { - if (visible) - updateSystemUiVisibility(); + if (isEmbeddingContainer()) + return; + m_nativeQtWindow.callMethod<void>("setVisible", visible); if (visible) { - if ((m_windowState & Qt::WindowFullScreen) - || ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) { - setGeometry(platformScreen()->geometry()); - } else if (m_windowState & Qt::WindowMaximized) { - setGeometry(platformScreen()->availableGeometry()); + if (window()->isTopLevel()) { + updateSystemUiVisibility(); + if ((m_windowState & Qt::WindowFullScreen) + || ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) { + setGeometry(platformScreen()->geometry()); + } else if (m_windowState & Qt::WindowMaximized) { + setGeometry(platformScreen()->availableGeometry()); + } + requestActivateWindow(); } + } else if (window()->isTopLevel() && window() == qGuiApp->focusWindow()) { + platformScreen()->topVisibleWindowChanged(); } - if (visible) - platformScreen()->addWindow(this); - else - platformScreen()->removeWindow(this); - QRect availableGeometry = screen()->availableGeometry(); if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) QPlatformWindow::setVisible(visible); @@ -120,7 +184,30 @@ Qt::WindowFlags QAndroidPlatformWindow::windowFlags() const void QAndroidPlatformWindow::setParent(const QPlatformWindow *window) { - Q_UNUSED(window); + using namespace QtJniTypes; + + if (window) { + auto androidWindow = static_cast<const QAndroidPlatformWindow*>(window); + if (androidWindow->isEmbeddingContainer()) + return; + // If we were a top level window, remove from screen + if (!m_nativeParentQtWindow.isValid()) + platformScreen()->removeWindow(this); + + const QtWindow parentWindow = androidWindow->nativeWindow(); + // If this was a child window of another window, the java method takes care of that + m_nativeQtWindow.callMethod<void, QtWindow>("setParent", parentWindow.object()); + m_nativeParentQtWindow = parentWindow; + } else if (QtAndroid::isQtApplication()) { + m_nativeQtWindow.callMethod<void, QtWindow>("setParent", nullptr); + m_nativeParentQtWindow = QJniObject(); + platformScreen()->addWindow(this); + } +} + +WId QAndroidPlatformWindow::winId() const +{ + return m_nativeQtWindow.isValid() ? reinterpret_cast<WId>(m_nativeQtWindow.object()) : 0L; } QAndroidPlatformScreen *QAndroidPlatformWindow::platformScreen() const @@ -135,7 +222,9 @@ void QAndroidPlatformWindow::propagateSizeHints() void QAndroidPlatformWindow::requestActivateWindow() { - platformScreen()->topWindowChanged(window()); + // raise() will handle differences between top level and child windows, and requesting focus + if (!blockedByModal()) + raise(); } void QAndroidPlatformWindow::updateSystemUiVisibility() @@ -169,4 +258,134 @@ void QAndroidPlatformWindow::applicationStateChanged(Qt::ApplicationState) QWindowSystemInterface::flushWindowSystemEvents(); } +void QAndroidPlatformWindow::createSurface() +{ + const QRect rect = geometry(); + jint x = 0, y = 0, w = -1, h = -1; + if (!rect.isNull()) { + x = rect.x(); + y = rect.y(); + w = std::max(rect.width(), 1); + h = std::max(rect.height(), 1); + } + + const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); + const bool isOpaque = !format().hasAlpha() && qFuzzyCompare(window()->opacity(), 1.0); + + m_nativeQtWindow.callMethod<void>("createSurface", windowStaysOnTop, x, y, w, h, 32, isOpaque, + m_surfaceContainerType); + m_surfaceCreated = true; +} + +void QAndroidPlatformWindow::destroySurface() +{ + if (m_surfaceCreated) { + m_nativeQtWindow.callMethod<void>("destroySurface"); + m_surfaceCreated = false; + } +} + +void QAndroidPlatformWindow::setNativeGeometry(const QRect &geometry) +{ + if (!m_surfaceCreated) + return; + + jint x = 0; + jint y = 0; + jint w = -1; + jint h = -1; + if (!geometry.isNull()) { + x = geometry.x(); + y = geometry.y(); + w = geometry.width(); + h = geometry.height(); + } + m_nativeQtWindow.callMethod<void>("setGeometry", x, y, w, h); +} + +void QAndroidPlatformWindow::onSurfaceChanged(QtJniTypes::Surface surface) +{ + lockSurface(); + m_androidSurfaceObject = surface; + if (m_androidSurfaceObject.isValid()) // wait until we have a valid surface to draw into + m_surfaceWaitCondition.wakeOne(); + unlockSurface(); + + if (m_androidSurfaceObject.isValid()) { + // repaint the window, when we have a valid surface + sendExpose(); + } +} + +void QAndroidPlatformWindow::sendExpose() const +{ + QRect availableGeometry = screen()->availableGeometry(); + if (!geometry().isNull() && !availableGeometry.isNull()) { + QWindowSystemInterface::handleExposeEvent(window(), + QRegion(QRect(QPoint(), geometry().size()))); + } +} + +bool QAndroidPlatformWindow::blockedByModal() const +{ + QWindow *modalWindow = QGuiApplication::modalWindow(); + return modalWindow && modalWindow != window(); +} + +bool QAndroidPlatformWindow::isEmbeddingContainer() const +{ + // Returns true if the window is a wrapper for a foreign window solely to allow embedding Qt + // into a native Android app, in which case we should not try to control it more than we "need" to + return !QtAndroid::isQtApplication() && window()->isTopLevel(); +} + +void QAndroidPlatformWindow::setSurface(JNIEnv *env, jobject object, jint windowId, + QtJniTypes::Surface surface) +{ + Q_UNUSED(env) + Q_UNUSED(object) + + if (!qGuiApp) + return; + + const QList<QWindow*> windows = qGuiApp->allWindows(); + for (QWindow * window : windows) { + if (!window->handle()) + continue; + QAndroidPlatformWindow *platformWindow = + static_cast<QAndroidPlatformWindow *>(window->handle()); + if (platformWindow->nativeViewId() == windowId) + platformWindow->onSurfaceChanged(surface); + } +} + +void QAndroidPlatformWindow::windowFocusChanged(JNIEnv *env, jobject object, + jboolean focus, jint windowId) +{ + Q_UNUSED(env) + Q_UNUSED(object) + QWindow* window = QtAndroid::windowFromId(windowId); + Q_ASSERT_X(window, "QAndroidPlatformWindow", "windowFocusChanged event window should exist"); + if (focus) { + QWindowSystemInterface::handleFocusWindowChanged(window); + } else if (!focus && window == qGuiApp->focusWindow()) { + // Clear focus if current window has lost focus + QWindowSystemInterface::handleFocusWindowChanged(nullptr); + } +} + +bool QAndroidPlatformWindow::registerNatives(QJniEnvironment &env) +{ + if (!env.registerNativeMethods(QtJniTypes::Traits<QtJniTypes::QtWindow>::className(), + { + Q_JNI_NATIVE_SCOPED_METHOD(setSurface, QAndroidPlatformWindow), + Q_JNI_NATIVE_SCOPED_METHOD(windowFocusChanged, QAndroidPlatformWindow) + })) { + qCCritical(lcQpaWindow) << "RegisterNatives failed for" + << QtJniTypes::Traits<QtJniTypes::QtWindow>::className(); + return false; + } + return true; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformwindow.h b/src/plugins/platforms/android/qandroidplatformwindow.h index 6fccc2e7fe..3f1e8ac992 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.h +++ b/src/plugins/platforms/android/qandroidplatformwindow.h @@ -7,17 +7,32 @@ #include <qobject.h> #include <qrect.h> #include <qpa/qplatformwindow.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qmutex.h> +#include <QtCore/qwaitcondition.h> +#include <jni.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow) +Q_DECLARE_JNI_CLASS(QtWindow, "org/qtproject/qt/android/QtWindow") +Q_DECLARE_JNI_CLASS(Surface, "android/view/Surface") + class QAndroidPlatformScreen; -class QAndroidPlatformBackingStore; class QAndroidPlatformWindow: public QPlatformWindow { public: - explicit QAndroidPlatformWindow(QWindow *window); + enum class SurfaceContainer { + SurfaceView, + TextureView + }; + explicit QAndroidPlatformWindow(QWindow *window); + ~QAndroidPlatformWindow(); void lower() override; void raise() override; @@ -27,7 +42,8 @@ public: void setWindowFlags(Qt::WindowFlags flags) override; Qt::WindowFlags windowFlags() const; void setParent(const QPlatformWindow *window) override; - WId winId() const override { return m_windowId; } + + WId winId() const override; bool setMouseGrabEnabled(bool grab) override { Q_UNUSED(grab); return false; } bool setKeyboardGrabEnabled(bool grab) override { Q_UNUSED(grab); return false; } @@ -39,32 +55,46 @@ public: void propagateSizeHints() override; void requestActivateWindow() override; void updateSystemUiVisibility(); - inline bool isRaster() const { - if (isForeignWindow()) - return false; - - return window()->surfaceType() == QSurface::RasterSurface - || window()->surfaceType() == QSurface::RasterGLSurface; - } + inline bool isRaster() const { return m_isRaster; } bool isExposed() const override; + QtJniTypes::QtWindow nativeWindow() const { return m_nativeQtWindow; } virtual void applicationStateChanged(Qt::ApplicationState); + int nativeViewId() const { return m_nativeViewId; } - void setBackingStore(QAndroidPlatformBackingStore *store) { m_backingStore = store; } - QAndroidPlatformBackingStore *backingStore() const { return m_backingStore; } - - virtual void repaint(const QRegion &) { } + static bool registerNatives(QJniEnvironment &env); + void onSurfaceChanged(QtJniTypes::Surface surface); protected: void setGeometry(const QRect &rect) override; + void lockSurface() { m_surfaceMutex.lock(); } + void unlockSurface() { m_surfaceMutex.unlock(); } + void createSurface(); + void destroySurface(); + void setNativeGeometry(const QRect &geometry); + void sendExpose() const; + bool blockedByModal() const; + bool isEmbeddingContainer() const; -protected: Qt::WindowFlags m_windowFlags; Qt::WindowStates m_windowState; - - WId m_windowId; - - QAndroidPlatformBackingStore *m_backingStore = nullptr; + bool m_isRaster; + + int m_nativeViewId = -1; + QtJniTypes::QtWindow m_nativeQtWindow; + SurfaceContainer m_surfaceContainerType = SurfaceContainer::SurfaceView; + QtJniTypes::QtWindow m_nativeParentQtWindow; + // The Android Surface, accessed from multiple threads, guarded by m_surfaceMutex + QtJniTypes::Surface m_androidSurfaceObject; + QWaitCondition m_surfaceWaitCondition; + bool m_surfaceCreated = false; + QMutex m_surfaceMutex; + +private: + static void setSurface(JNIEnv *env, jobject obj, jint windowId, QtJniTypes::Surface surface); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setSurface) + static void windowFocusChanged(JNIEnv *env, jobject object, jboolean focus, jint windowId); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(windowFocusChanged) }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidsystemlocale.cpp b/src/plugins/platforms/android/qandroidsystemlocale.cpp index 858934b1f8..c5f2e59097 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.cpp +++ b/src/plugins/platforms/android/qandroidsystemlocale.cpp @@ -12,34 +12,38 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(Locale, "java/util/Locale") +Q_DECLARE_JNI_CLASS(Resources, "android/content/res/Resources") +Q_DECLARE_JNI_CLASS(Configuration, "android/content/res/Configuration") +Q_DECLARE_JNI_CLASS(LocaleList, "android/os/LocaleList") + +using namespace QtJniTypes; + QAndroidSystemLocale::QAndroidSystemLocale() : m_locale(QLocale::C) { } void QAndroidSystemLocale::getLocaleFromJava() const { - QWriteLocker locker(&m_lock); - - QJniObject javaLocaleObject; - QJniObject javaActivity(QtAndroid::activity()); - if (!javaActivity.isValid()) - javaActivity = QtAndroid::service(); - if (javaActivity.isValid()) { - QJniObject resources = javaActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;"); - QJniObject configuration = resources.callObjectMethod("getConfiguration", "()Landroid/content/res/Configuration;"); - - javaLocaleObject = configuration.getObjectField("locale", "Ljava/util/Locale;"); - } else { - javaLocaleObject = QJniObject::callStaticObjectMethod("java/util/Locale", "getDefault", "()Ljava/util/Locale;"); - } + const Locale javaLocaleObject = []{ + const QJniObject javaContext = QtAndroidPrivate::context(); + if (javaContext.isValid()) { + const QJniObject resources = javaContext.callMethod<Resources>("getResources"); + const QJniObject configuration = resources.callMethod<Configuration>("getConfiguration"); + return configuration.getField<Locale>("locale"); + } else { + return Locale::callStaticMethod<Locale>("getDefault"); + } + }(); - QString languageCode = javaLocaleObject.callObjectMethod("getLanguage", "()Ljava/lang/String;").toString(); - QString countryCode = javaLocaleObject.callObjectMethod("getCountry", "()Ljava/lang/String;").toString(); + const QString languageCode = javaLocaleObject.callMethod<QString>("getLanguage"); + const QString countryCode = javaLocaleObject.callMethod<QString>("getCountry"); + QWriteLocker locker(&m_lock); m_locale = QLocale(languageCode + u'_' + countryCode); } -QVariant QAndroidSystemLocale::query(QueryType type, QVariant in) const +QVariant QAndroidSystemLocale::query(QueryType type, QVariant &&in) const { if (type == LocaleChanged) { getLocaleFromJava(); @@ -142,12 +146,9 @@ QVariant QAndroidSystemLocale::query(QueryType type, QVariant in) const Q_ASSERT_X(false, Q_FUNC_INFO, "This can't happen."); case UILanguages: { if (QtAndroidPrivate::androidSdkVersion() >= 24) { - QJniObject localeListObject = - QJniObject::callStaticObjectMethod("android/os/LocaleList", "getDefault", - "()Landroid/os/LocaleList;"); + LocaleList localeListObject = LocaleList::callStaticMethod<LocaleList>("getDefault"); if (localeListObject.isValid()) { - QString lang = localeListObject.callObjectMethod("toLanguageTags", - "()Ljava/lang/String;").toString(); + QString lang = localeListObject.callMethod<QString>("toLanguageTags"); // Some devices return with it enclosed in []'s so check if both exists before // removing to ensure it is formatted correctly if (lang.startsWith(QChar('[')) && lang.endsWith(QChar(']'))) diff --git a/src/plugins/platforms/android/qandroidsystemlocale.h b/src/plugins/platforms/android/qandroidsystemlocale.h index 48e1d94a56..cd37b48270 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.h +++ b/src/plugins/platforms/android/qandroidsystemlocale.h @@ -11,10 +11,11 @@ QT_BEGIN_NAMESPACE class QAndroidSystemLocale : public QSystemLocale { + Q_DISABLE_COPY_MOVE(QAndroidSystemLocale) public: QAndroidSystemLocale(); - QVariant query(QueryType type, QVariant in) const override; + QVariant query(QueryType type, QVariant &&in) const override; QLocale fallbackLocale() const override; private: diff --git a/src/plugins/platforms/bsdfb/qbsdfbscreen.cpp b/src/plugins/platforms/bsdfb/qbsdfbscreen.cpp index 564e670290..31f8dab091 100644 --- a/src/plugins/platforms/bsdfb/qbsdfbscreen.cpp +++ b/src/plugins/platforms/bsdfb/qbsdfbscreen.cpp @@ -128,7 +128,7 @@ bool QBsdFbScreen::initialize() QRect userGeometry; // Parse arguments - for (const QString &arg : qAsConst(m_arguments)) { + for (const QString &arg : std::as_const(m_arguments)) { QRegularExpressionMatch match; if (arg.contains(mmSizeRx, &match)) userMmSize = QSize(match.captured(1).toInt(), match.captured(2).toInt()); diff --git a/src/plugins/platforms/cocoa/CMakeLists.txt b/src/plugins/platforms/cocoa/CMakeLists.txt index 21f87dae83..491c61703f 100644 --- a/src/plugins/platforms/cocoa/CMakeLists.txt +++ b/src/plugins/platforms/cocoa/CMakeLists.txt @@ -1,6 +1,5 @@ -# Generated from cocoa.pro. - -# special case: +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QCocoaIntegrationPlugin Plugin: @@ -8,7 +7,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin OUTPUT_NAME qcocoa - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES cocoa # special case + DEFAULT_IF "cocoa" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms SOURCES main.mm @@ -16,12 +15,9 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin qcocoaapplicationdelegate.h qcocoaapplicationdelegate.mm qcocoabackingstore.h qcocoabackingstore.mm qcocoaclipboard.h qcocoaclipboard.mm - qcocoacolordialoghelper.h qcocoacolordialoghelper.mm qcocoacursor.h qcocoacursor.mm qcocoadrag.h qcocoadrag.mm qcocoaeventdispatcher.h qcocoaeventdispatcher.mm - qcocoafiledialoghelper.h qcocoafiledialoghelper.mm - qcocoafontdialoghelper.h qcocoafontdialoghelper.mm qcocoahelpers.h qcocoahelpers.mm qcocoainputcontext.h qcocoainputcontext.mm qcocoaintegration.h qcocoaintegration.mm @@ -48,6 +44,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin qcocoacolordialoghelper.h qcocoacolordialoghelper.mm qcocoafiledialoghelper.h qcocoafiledialoghelper.mm qcocoafontdialoghelper.h qcocoafontdialoghelper.mm + qcocoamessagedialog.h qcocoamessagedialog.mm DEFINES QT_NO_FOREACH LIBRARIES @@ -59,15 +56,14 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin ${FWIOSurface} ${FWMetal} ${FWQuartzCore} + ${FWUniformTypeIdentifiers} Qt::Core Qt::CorePrivate Qt::Gui Qt::GuiPrivate ) -# special case begin qt_disable_apple_app_extension_api_only(QCocoaIntegrationPlugin) -# special case end # Resources: set(qcocoaresources_resource_files @@ -83,10 +79,6 @@ qt_internal_add_resource(QCocoaIntegrationPlugin "qcocoaresources" ${qcocoaresources_resource_files} ) - -#### Keys ignored in scope 1:.:.:cocoa.pro:<TRUE>: -# OTHER_FILES = "cocoa.json" - ## Scopes: ##################################################################### @@ -110,8 +102,3 @@ qt_internal_extend_target(QCocoaIntegrationPlugin CONDITION QT_FEATURE_sessionma SOURCES qcocoasessionmanager.cpp qcocoasessionmanager.h ) - -#### Keys ignored in scope 7:.:.:cocoa.pro:TARGET Qt::Widgets: -# QT_FOR_CONFIG = "widgets" -#### Keys ignored in scope 12:.:.:cocoa.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/cocoa/main.mm b/src/plugins/platforms/cocoa/main.mm index 39b3aa2458..7fbf26cb75 100644 --- a/src/plugins/platforms/cocoa/main.mm +++ b/src/plugins/platforms/cocoa/main.mm @@ -8,6 +8,8 @@ #include "qcocoaintegration.h" #include "qcocoatheme.h" +#include <QtCore/private/qcore_mac_p.h> + QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index ccd5f7b665..40c1e90511 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -36,6 +36,23 @@ void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) } switch (event->type()) { + case QAccessible::Announcement: { + auto *announcementEvent = static_cast<QAccessibleAnnouncementEvent *>(event); + auto priorityLevel = (announcementEvent->priority() == QAccessible::AnnouncementPriority::Assertive) + ? NSAccessibilityPriorityHigh + : NSAccessibilityPriorityMedium; + NSDictionary *announcementInfo = @{ + NSAccessibilityPriorityKey: [NSNumber numberWithInt:priorityLevel], + NSAccessibilityAnnouncementKey: announcementEvent->message().toNSString() + }; + // post event for application element, as the comment for + // NSAccessibilityAnnouncementRequestedNotification in the + // NSAccessibilityConstants.h header says + NSAccessibilityPostNotificationWithUserInfo(NSApp, + NSAccessibilityAnnouncementRequestedNotification, + announcementInfo); + break; + } case QAccessible::Focus: { NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); break; @@ -54,6 +71,10 @@ void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) case QAccessible::NameChanged: NSAccessibilityPostNotification(element, NSAccessibilityTitleChangedNotification); break; + case QAccessible::TableModelChanged: + // ### Could NSAccessibilityRowCountChangedNotification be relevant here? + [element updateTableModel]; + break; default: break; } @@ -113,7 +134,6 @@ static void populateRoleMap() roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole; roleMap[QAccessible::Row] = NSAccessibilityRowRole; roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole; - roleMap[QAccessible::Cell] = NSAccessibilityTextFieldRole; roleMap[QAccessible::Button] = NSAccessibilityButtonRole; roleMap[QAccessible::EditableText] = NSAccessibilityTextFieldRole; roleMap[QAccessible::Link] = NSAccessibilityLinkRole; @@ -121,7 +141,7 @@ static void populateRoleMap() roleMap[QAccessible::Splitter] = NSAccessibilitySplitGroupRole; roleMap[QAccessible::List] = NSAccessibilityListRole; roleMap[QAccessible::ListItem] = NSAccessibilityStaticTextRole; - roleMap[QAccessible::Cell] = NSAccessibilityStaticTextRole; + roleMap[QAccessible::Cell] = NSAccessibilityCellRole; roleMap[QAccessible::Client] = NSAccessibilityGroupRole; roleMap[QAccessible::Paragraph] = NSAccessibilityGroupRole; roleMap[QAccessible::Section] = NSAccessibilityGroupRole; @@ -133,6 +153,7 @@ static void populateRoleMap() roleMap[QAccessible::Note] = NSAccessibilityGroupRole; roleMap[QAccessible::ComplementaryContent] = NSAccessibilityGroupRole; roleMap[QAccessible::Graphic] = NSAccessibilityImageRole; + roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole; } /* @@ -151,6 +172,8 @@ NSString *macRole(QAccessibleInterface *interface) if (roleMap.contains(qtRole)) { // MAC_ACCESSIBILTY_DEBUG() << "return" << roleMap[qtRole]; + if (roleMap[qtRole] == NSAccessibilityComboBoxRole && !interface->state().editable) + return NSAccessibilityMenuButtonRole; if (roleMap[qtRole] == NSAccessibilityTextFieldRole && interface->state().multiLine) return NSAccessibilityTextAreaRole; return roleMap[qtRole]; diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h index 806359888f..a96ab55735 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h @@ -13,7 +13,11 @@ QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QMacAccessibilityElement, NSObject <NSAccessibilityElement> - (instancetype)initWithId:(QAccessible::Id)anId; +- (instancetype)initWithId:(QAccessible::Id)anId role:(NSAccessibilityRole)role; + (instancetype)elementWithId:(QAccessible::Id)anId; ++ (instancetype)elementWithInterface:(QAccessibleInterface *)iface; +- (void)updateTableModel; +- (QAccessibleInterface *)qtInterface; ) #endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm index f3c3d0bfaa..8d4d6d683d 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm @@ -9,12 +9,17 @@ #include "qcocoawindow.h" #include "qcocoascreen.h" +#include <QtCore/qlogging.h> #include <QtGui/private/qaccessiblecache_p.h> #include <QtGui/private/qaccessiblebridgeutils_p.h> #include <QtGui/qaccessible.h> QT_USE_NAMESPACE +Q_LOGGING_CATEGORY(lcAccessibilityTable, "qt.accessibility.table") + +using namespace Qt::Literals::StringLiterals; + #if QT_CONFIG(accessibility) /** @@ -80,14 +85,92 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of @implementation QMacAccessibilityElement { QAccessible::Id axid; + int m_rowIndex; + int m_columnIndex; + + // used by NSAccessibilityTable + NSMutableArray<QMacAccessibilityElement *> *rows; // corresponds to accessibilityRows + NSMutableArray<QMacAccessibilityElement *> *columns; // corresponds to accessibilityColumns + + // If synthesizedRole is set, this means that this objects does not have a corresponding + // QAccessibleInterface, but it is synthesized by the cocoa plugin in order to meet the + // NSAccessibility requirements. + // The ownership is controlled by the parent object identified with the axid member variable. + // (Therefore, if this member is set, this objects axid member is the same as the parents axid + // member) + NSString *synthesizedRole; } - (instancetype)initWithId:(QAccessible::Id)anId { + return [self initWithId:anId role:nil]; +} + +- (instancetype)initWithId:(QAccessible::Id)anId role:(NSAccessibilityRole)role +{ Q_ASSERT((int)anId < 0); self = [super init]; if (self) { axid = anId; + m_rowIndex = -1; + m_columnIndex = -1; + rows = nil; + columns = nil; + synthesizedRole = role; + // table: if this is not created as an element managed by the table, then + // it's either the table itself, or an element created for an already existing + // cell interface (or an element that's not at all related to a table). + if (!synthesizedRole) { + if (QAccessibleInterface *iface = QAccessible::accessibleInterface(axid)) { + if (iface->tableInterface()) { + [self updateTableModel]; + } else if (const auto *cell = iface->tableCellInterface()) { + // If we create an element for a table cell, initialize it with row/column + // and insert it into the corresponding row's columns array. + m_rowIndex = cell->rowIndex(); + m_columnIndex = cell->columnIndex(); + QAccessibleInterface *table = cell->table(); + Q_ASSERT(table); + QAccessibleTableInterface *tableInterface = table->tableInterface(); + if (tableInterface) { + auto *tableElement = [QMacAccessibilityElement elementWithInterface:table]; + Q_ASSERT(tableElement); + if (!tableElement->rows + || int(tableElement->rows.count) <= m_rowIndex + || int(tableElement->rows.count) != tableInterface->rowCount()) { + qCWarning(lcAccessibilityTable) + << "Cell requested for row" << m_rowIndex << "is out of" + << "bounds for table with" << (tableElement->rows ? + tableElement->rows.count : tableInterface->rowCount()) + << "rows! Resizing table model."; + [tableElement updateTableModel]; + } + + Q_ASSERT(tableElement->rows); + Q_ASSERT(int(tableElement->rows.count) > m_rowIndex); + + auto *rowElement = tableElement->rows[m_rowIndex]; + if (!rowElement->columns || int(rowElement->columns.count) != tableInterface->columnCount()) { + if (rowElement->columns) { + qCWarning(lcAccessibilityTable) + << "Table representation column count is out of sync:" + << rowElement->columns.count << "!=" << tableInterface->columnCount(); + } + rowElement->columns = [rowElement populateTableRow:rowElement->columns + count:tableInterface->columnCount()]; + } + + qCDebug(lcAccessibilityTable) << "Creating cell representation for" + << m_rowIndex << m_columnIndex + << "in table with" + << tableElement->rows.count << "rows and" + << rowElement->columns.count << "columns"; + + rowElement->columns[m_columnIndex] = self; + } + } + } + } } return self; @@ -103,30 +186,45 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of QMacAccessibilityElement *element = cache->elementForId(anId); if (!element) { - QAccessibleInterface *iface = QAccessible::accessibleInterface(anId); - Q_ASSERT(iface); - if (!iface || !iface->isValid()) - return nil; + Q_ASSERT(QAccessible::accessibleInterface(anId)); element = [[self alloc] initWithId:anId]; cache->insertElement(anId, element); } return element; } ++ (instancetype)elementWithInterface:(QAccessibleInterface *)iface +{ + Q_ASSERT(iface); + if (!iface) + return nil; + + const QAccessible::Id anId = QAccessible::uniqueId(iface); + return [self elementWithId:anId]; +} + - (void)invalidate { axid = 0; + rows = nil; + columns = nil; + synthesizedRole = nil; + NSAccessibilityPostNotification(self, NSAccessibilityUIElementDestroyedNotification); [self release]; } - (void)dealloc { + if (rows) + [rows release]; // will also release all entries first + if (columns) + [columns release]; // will also release all entries first [super dealloc]; } - (BOOL)isEqual:(id)object { if ([object isKindOfClass:[QMacAccessibilityElement class]]) { QMacAccessibilityElement *other = object; - return other->axid == axid; + return other->axid == axid && other->synthesizedRole == synthesizedRole; } else { return NO; } @@ -136,16 +234,137 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return axid; } +- (BOOL)isManagedByParent { + return synthesizedRole != nil; +} + +- (NSMutableArray *)populateTableArray:(NSMutableArray *)array role:(NSAccessibilityRole)role count:(int)count +{ + if (QAccessibleInterface *iface = self.qtInterface) { + if (array && int(array.count) != count) { + [array release]; + array = nil; + } + if (!array) { + array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count]; + [array retain]; + } else { + [array removeAllObjects]; + } + Q_ASSERT(array); + for (int n = 0; n < count; ++n) { + // columns will have same axid as table (but not inserted in cache) + QMacAccessibilityElement *element = + [[QMacAccessibilityElement alloc] initWithId:axid role:role]; + if (element) { + if (role == NSAccessibilityRowRole) + element->m_rowIndex = n; + else if (role == NSAccessibilityColumnRole) + element->m_columnIndex = n; + [array addObject:element]; + [element release]; + } else { + qWarning("QCocoaAccessibility: invalid child"); + } + } + return array; + } + return nil; +} + +- (NSMutableArray *)populateTableRow:(NSMutableArray *)array count:(int)count +{ + Q_ASSERT(synthesizedRole == NSAccessibilityRowRole); + if (array && int(array.count) != count) { + [array release]; + array = nil; + } + + if (!array) { + array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count]; + [array retain]; + // When macOS asks for the children of a row, then we populate the row's column + // array with synthetic elements as place holders. This way, we don't have to + // create QAccessibleInterfaces for every cell before they are really needed. + // We don't add those synthetic elements into the cache, and we give them the + // same axid as the table. This way, we can get easily to the table, and from + // there to the QAccessibleInterface for the cell, when we have to eventually + // associate such an interface with the element (at which point it is no longer + // a placeholder). + for (int n = 0; n < count; ++n) { + // columns will have same axid as table (but not inserted in cache) + QMacAccessibilityElement *cell = + [[QMacAccessibilityElement alloc] initWithId:axid role:NSAccessibilityCellRole]; + if (cell) { + cell->m_rowIndex = m_rowIndex; + cell->m_columnIndex = n; + [array addObject:cell]; + } + } + } + Q_ASSERT(array); + return array; +} + +- (void)updateTableModel +{ + if (QAccessibleInterface *iface = self.qtInterface) { + if (QAccessibleTableInterface *table = iface->tableInterface()) { + Q_ASSERT(!self.isManagedByParent); + qCDebug(lcAccessibilityTable) << "Updating table representation with" + << table->rowCount() << table->columnCount(); + rows = [self populateTableArray:rows role:NSAccessibilityRowRole count:table->rowCount()]; + columns = [self populateTableArray:columns role:NSAccessibilityColumnRole count:table->columnCount()]; + } + } +} + +- (QAccessibleInterface *)qtInterface +{ + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (!iface || !iface->isValid()) + return nullptr; + + // If this is a placeholder element for a table cell, associate it with the + // cell interface (which will be created now, if needed). The current axid is + // for the table to which the cell belongs, so iface is pointing at the table. + if (synthesizedRole == NSAccessibilityCellRole) { + // get the cell interface - there must be a valid one + QAccessibleTableInterface *table = iface->tableInterface(); + Q_ASSERT(table); + QAccessibleInterface *cell = table->cellAt(m_rowIndex, m_columnIndex); + if (!cell) + return nullptr; + Q_ASSERT(cell->isValid()); + iface = cell; + + // no longer a placeholder + axid = QAccessible::uniqueId(cell); + synthesizedRole = nil; + + QAccessibleCache *cache = QAccessibleCache::instance(); + if (QMacAccessibilityElement *cellElement = cache->elementForId(axid)) { + // there already is another, non-placeholder element in the cache + Q_ASSERT(cellElement->synthesizedRole == nil); + // we have to release it if it's not us + if (cellElement != self) { + // for the same cell position + Q_ASSERT(cellElement->m_rowIndex == m_rowIndex && cellElement->m_columnIndex == m_columnIndex); + [cellElement release]; + } + } + + cache->insertElement(axid, self); + } + return iface; +} + // // accessibility protocol // - (BOOL)isAccessibilityFocused { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) { - return false; - } // Just check if the app thinks we're focused. id focusedElement = NSApp.accessibilityApplicationFocusedUIElement; return [focusedElement isEqual:self]; @@ -165,31 +384,119 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSString *) accessibilityRole { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return NSAccessibilityUnknownRole; - return QCocoaAccessible::macRole(iface); + // shortcut for cells, rows, and columns in a table + if (synthesizedRole) + return synthesizedRole; + if (QAccessibleInterface *iface = self.qtInterface) + return QCocoaAccessible::macRole(iface); + return NSAccessibilityUnknownRole; } - (NSString *) accessibilitySubRole { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return NSAccessibilityUnknownRole; - return QCocoaAccessible::macSubrole(iface); + if (QAccessibleInterface *iface = self.qtInterface) + return QCocoaAccessible::macSubrole(iface); + return NSAccessibilityUnknownRole; } - (NSString *) accessibilityRoleDescription { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return NSAccessibilityUnknownRole; - return NSAccessibilityRoleDescription(self.accessibilityRole, self.accessibilitySubRole); + if (QAccessibleInterface *iface = self.qtInterface) + return NSAccessibilityRoleDescription(self.accessibilityRole, self.accessibilitySubRole); + return NSAccessibilityUnknownRole; } - (NSArray *) accessibilityChildren { + // shortcut for cells + if (synthesizedRole == NSAccessibilityCellRole) + return nil; + + QAccessibleInterface *iface = self.qtInterface; + if (!iface) + return nil; + if (QAccessibleTableInterface *table = iface->tableInterface()) { + // either a table or table rows/columns + if (!synthesizedRole) { + // This is the table element, parent of all rows and columns + /* + * Typical 2x2 table hierarchy as can be observed in a table found under + * Apple -> System Settings -> General -> Login Items (macOS 13) + * + * (AXTable) + * | Columns: NSArray* (2 items) + * | Rows: NSArray* (2 items) + * | Visible Columns: NSArray* (2 items) + * | Visible Rows: NSArray* (2 items) + * | Children: NSArray* (5 items) + +----<--| Header: (AXGroup) + | * +-- (AXRow) + | * | +-- (AXText) + | * | +-- (AXTextField) + | * +-- (AXRow) + | * | +-- (AXText) + | * | +-- (AXTextField) + | * +-- (AXColumn) + | * | Header: "Item" (sort button) + | * | Index: 0 + | * | Rows: NSArray* (2 items) + | * | Visible Rows: NSArray* (2 items) + | * +-- (AXColumn) + | * | Header: "Kind" (sort button) + | * | Index: 1 + | * | Rows: NSArray* (2 items) + | * | Visible Rows: NSArray* (2 items) + +----> +-- (AXGroup) + * +-- (AXButton/AXSortButton) Item [NSAccessibilityTableHeaderCellProxy] + * +-- (AXButton/AXSortButton) Kind [NSAccessibilityTableHeaderCellProxy] + */ + NSArray *rs = [self accessibilityRows]; + NSArray *cs = [self accessibilityColumns]; + const int rCount = int([rs count]); + const int cCount = int([cs count]); + int childCount = rCount + cCount; + NSMutableArray<QMacAccessibilityElement *> *tableChildren = + [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:childCount]; + for (int i = 0; i < rCount; ++i) { + [tableChildren addObject:[rs objectAtIndex:i]]; + } + for (int i = 0; i < cCount; ++i) { + [tableChildren addObject:[cs objectAtIndex:i]]; + } + return NSAccessibilityUnignoredChildren(tableChildren); + } else if (synthesizedRole == NSAccessibilityColumnRole) { + return nil; + } else if (synthesizedRole == NSAccessibilityRowRole) { + // axid matches the parent table axid so that we can easily find the parent table + // children of row are cell/any items + Q_ASSERT(m_rowIndex >= 0); + const int numColumns = table->columnCount(); + columns = [self populateTableRow:columns count:numColumns]; + return NSAccessibilityUnignoredChildren(columns); + } + } + return QCocoaAccessible::unignoredChildren(iface); +} + +- (NSArray *) accessibilitySelectedChildren { QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return nil; - return QCocoaAccessible::unignoredChildren(iface); + + QAccessibleSelectionInterface *selection = iface->selectionInterface(); + if (!selection) + return nil; + + const QList<QAccessibleInterface *> selectedList = selection->selectedItems(); + const qsizetype numSelected = selectedList.size(); + NSMutableArray<QMacAccessibilityElement *> *selectedChildren = + [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numSelected]; + for (QAccessibleInterface *selectedChild : selectedList) { + if (selectedChild && selectedChild->isValid()) { + QAccessible::Id id = QAccessible::uniqueId(selectedChild); + QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId:id]; + if (element) + [selectedChildren addObject:element]; + } + } + return NSAccessibilityUnignoredChildren(selectedChildren); } - (id) accessibilityWindow { @@ -203,36 +510,66 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSString *) accessibilityTitle { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return nil; - if (iface->role() == QAccessible::StaticText) - return nil; - return iface->text(QAccessible::Name).toNSString(); + if (QAccessibleInterface *iface = self.qtInterface) { + if (iface->role() == QAccessible::StaticText) + return nil; + if (self.isManagedByParent) + return nil; + return iface->text(QAccessible::Name).toNSString(); + } + return nil; } - (BOOL) isAccessibilityEnabled { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return false; - return !iface->state().disabled; + if (QAccessibleInterface *iface = self.qtInterface) + return !iface->state().disabled; + return false; } - (id)accessibilityParent { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + if (synthesizedRole == NSAccessibilityCellRole) { + // a synthetic cell without interface - shortcut to the row + QMacAccessibilityElement *tableElement = + [QMacAccessibilityElement elementWithId:axid]; + Q_ASSERT(tableElement && tableElement->rows); + Q_ASSERT(int(tableElement->rows.count) > m_rowIndex); + QMacAccessibilityElement *rowElement = tableElement->rows[m_rowIndex]; + return rowElement; + } + + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return nil; + if (self.isManagedByParent) { + // axid is the same for the parent element + return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId:axid]); + } + // macOS expects that the hierarchy is: // App -> Window -> Children // We don't actually have the window reflected properly in QAccessibility. // Check if the parent is the application and then instead return the native window. if (QAccessibleInterface *parent = iface->parent()) { - if (parent->role() != QAccessible::Application) { - QAccessible::Id parentId = QAccessible::uniqueId(parent); - return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId: parentId]); + if (parent->tableInterface()) { + QMacAccessibilityElement *tableElement = + [QMacAccessibilityElement elementWithInterface:parent]; + + // parent of cell should be row + int rowIndex = -1; + if (m_rowIndex >= 0 && m_columnIndex >= 0) + rowIndex = m_rowIndex; + else if (QAccessibleTableCellInterface *cell = iface->tableCellInterface()) + rowIndex = cell->rowIndex(); + Q_ASSERT(tableElement->rows); + if (rowIndex > int([tableElement->rows count]) || rowIndex == -1) + return nil; + QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex]; + return NSAccessibilityUnignoredAncestor(rowElement); } + if (parent->role() != QAccessible::Application) + return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithInterface: parent]); } if (QWindow *window = iface->window()) { @@ -246,78 +583,91 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSRect)accessibilityFrame { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return NSZeroRect; - return QCocoaScreen::mapToNative(iface->rect()); -} -- (NSString*)accessibilityLabel { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) { - qWarning() << "Called accessibilityLabel on invalid object: " << axid; - return nil; + QRect rect; + if (self.isManagedByParent) { + if (QAccessibleTableInterface *table = iface->tableInterface()) { + // Construct the geometry of the Row/Column by looking at the individual table cells + // ### Assumes that cells logical coordinates have spatial ordering (e.g finds the + // rows width by taking the union between the leftmost item and the rightmost item in + // a row). + // Otherwise, we have to iterate over *all* cells in a row/columns to + // find out the Row/Column geometry + const bool isRow = synthesizedRole == NSAccessibilityRowRole; + QPoint cellPos; + int &row = isRow ? cellPos.ry() : cellPos.rx(); + int &col = isRow ? cellPos.rx() : cellPos.ry(); + + NSUInteger trackIndex = self.accessibilityIndex; + if (trackIndex != NSNotFound) { + row = int(trackIndex); + if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) { + rect = firstCell->rect(); + col = isRow ? table->columnCount() : table->rowCount(); + if (col > 1) { + --col; + if (QAccessibleInterface *lastCell = + table->cellAt(cellPos.y(), cellPos.x())) + rect = rect.united(lastCell->rect()); + } + } + } + } + } else { + rect = iface->rect(); } - return iface->text(QAccessible::Description).toNSString(); -} -- (void)setAccessibilityLabel:(NSString*)label{ - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return; - iface->setText(QAccessible::Description, QString::fromNSString(label)); + return QCocoaScreen::mapToNative(rect); } -- (id) accessibilityMinValue:(QAccessibleInterface*)iface { - if (QAccessibleValueInterface *val = iface->valueInterface()) - return @(val->minimumValue().toDouble()); +- (NSString*)accessibilityLabel { + if (QAccessibleInterface *iface = self.qtInterface) + return iface->text(QAccessible::Description).toNSString(); + qWarning() << "Called accessibilityLabel on invalid object: " << axid; return nil; } -- (id) accessibilityMaxValue:(QAccessibleInterface*)iface { - if (QAccessibleValueInterface *val = iface->valueInterface()) - return @(val->maximumValue().toDouble()); - return nil; +- (void)setAccessibilityLabel:(NSString*)label{ + if (QAccessibleInterface *iface = self.qtInterface) + iface->setText(QAccessible::Description, QString::fromNSString(label)); } - (id) accessibilityValue { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return nil; - - // VoiceOver asks for the value attribute for all elements. Return nil - // if we don't want the element to have a value attribute. - if (!QCocoaAccessible::hasValueAttribute(iface)) - return nil; - - return QCocoaAccessible::getValueAttribute(iface); + if (QAccessibleInterface *iface = self.qtInterface) { + // VoiceOver asks for the value attribute for all elements. Return nil + // if we don't want the element to have a value attribute. + if (QCocoaAccessible::hasValueAttribute(iface)) + return QCocoaAccessible::getValueAttribute(iface); + } + return nil; } - (NSInteger) accessibilityNumberOfCharacters { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return 0; - if (QAccessibleTextInterface *text = iface->textInterface()) - return text->characterCount(); + if (QAccessibleInterface *iface = self.qtInterface) { + if (QAccessibleTextInterface *text = iface->textInterface()) + return text->characterCount(); + } return 0; } - (NSString *) accessibilitySelectedText { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return nil; - if (QAccessibleTextInterface *text = iface->textInterface()) { - int start = 0; - int end = 0; - text->selection(0, &start, &end); - return text->text(start, end).toNSString(); + if (QAccessibleInterface *iface = self.qtInterface) { + if (QAccessibleTextInterface *text = iface->textInterface()) { + int start = 0; + int end = 0; + text->selection(0, &start, &end); + return text->text(start, end).toNSString(); + } } return nil; } - (NSRange) accessibilitySelectedTextRange { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return NSRange(); if (QAccessibleTextInterface *text = iface->textInterface()) { int start = 0; @@ -334,8 +684,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSInteger)accessibilityLineForIndex:(NSInteger)index { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return 0; if (QAccessibleTextInterface *text = iface->textInterface()) { QString textToPos = text->text(0, index); @@ -345,8 +695,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSRange)accessibilityVisibleCharacterRange { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return NSRange(); // FIXME This is not correct and may impact performance for big texts if (QAccessibleTextInterface *text = iface->textInterface()) @@ -355,8 +705,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSInteger) accessibilityInsertionPointLineNumber { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return 0; if (QAccessibleTextInterface *text = iface->textInterface()) { int position = text->cursorPosition(); @@ -367,8 +717,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of - (NSArray *)accessibilityParameterizedAttributeNames { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) { + QAccessibleInterface *iface = self.qtInterface; + if (!iface) { qWarning() << "Called attribute on invalid object: " << axid; return nil; } @@ -391,8 +741,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) { + QAccessibleInterface *iface = self.qtInterface; + if (!iface) { qWarning() << "Called attribute on invalid object: " << axid; return nil; } @@ -432,7 +782,7 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of QRectF rect; if (range.length > 0) { NSUInteger position = range.location + range.length - 1; - if (position > range.location && iface->textInterface()->text(position, position + 1) == QStringLiteral("\n")) + if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1) --position; QRect lastRect = iface->textInterface()->characterRect(position); rect = firstRect.united(lastRect); @@ -460,8 +810,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (BOOL)accessibilityIsAttributeSettable:(NSString *)attribute { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return NO; if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { @@ -479,8 +829,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (void)accessibilitySetValue:(id)value forAttribute:(NSString *)attribute { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return; if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) { if (QAccessibleActionInterface *action = iface->actionInterface()) @@ -508,8 +858,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of - (NSArray *)accessibilityActionNames { NSMutableArray *nsActions = [[NSMutableArray new] autorelease]; - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return nsActions; const QStringList &supportedActionNames = QAccessibleBridgeUtils::effectiveActionNames(iface); @@ -523,8 +873,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSString *)accessibilityActionDescription:(NSString *)action { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + QAccessibleInterface *iface = self.qtInterface; + if (!iface) return nil; // FIXME is that the right return type?? QString qtAction = QCocoaAccessible::translateAction(action, iface); QString description; @@ -541,8 +891,7 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (void)accessibilityPerformAction:(NSString *)action { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid()) { + if (QAccessibleInterface *iface = self.qtInterface) { const QString qtAction = QCocoaAccessible::translateAction(action, iface); QAccessibleBridgeUtils::performEffectiveAction(iface, qtAction); } @@ -551,15 +900,20 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of // misc - (BOOL)accessibilityIsIgnored { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return true; - return QCocoaAccessible::shouldBeIgnored(iface); + // Short-cut for placeholders and synthesized elements. Working around a bug + // that corrups lists returned by NSAccessibilityUnignoredChildren, otherwise + // we could ignore rows and columns that are outside the table. + if (self.isManagedByParent) + return false; + + if (QAccessibleInterface *iface = self.qtInterface) + return QCocoaAccessible::shouldBeIgnored(iface); + return true; } - (id)accessibilityHitTest:(NSPoint)point { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) { + QAccessibleInterface *iface = self.qtInterface; + if (!iface) { // qDebug("Hit test: INVALID"); return NSAccessibilityUnignoredAncestor(self); } @@ -578,26 +932,23 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of childInterface = childOfChildInterface; } while (childOfChildInterface && childOfChildInterface->isValid()); - QAccessible::Id childId = QAccessible::uniqueId(childInterface); // hit a child, forward to child accessible interface. - QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithId:childId]; + QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface]; if (accessibleElement) return NSAccessibilityUnignoredAncestor(accessibleElement); return NSAccessibilityUnignoredAncestor(self); } - (id)accessibilityFocusedUIElement { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - - if (!iface || !iface->isValid()) { + QAccessibleInterface *iface = self.qtInterface; + if (!iface) { qWarning("FocusedUIElement for INVALID"); return nil; } QAccessibleInterface *childInterface = iface->focusChild(); if (childInterface && childInterface->isValid()) { - QAccessible::Id childAxid = QAccessible::uniqueId(childInterface); - QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithId:childAxid]; + QMacAccessibilityElement *accessibleElement = [QMacAccessibilityElement elementWithInterface:childInterface]; return NSAccessibilityUnignoredAncestor(accessibleElement); } @@ -605,8 +956,7 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSString *) accessibilityHelp { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid()) { + if (QAccessibleInterface *iface = self.qtInterface) { const QString helpText = iface->text(QAccessible::Help); if (!helpText.isEmpty()) return helpText.toNSString(); @@ -614,6 +964,47 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return nil; } +/* + * Support for table + */ +- (NSInteger) accessibilityIndex { + NSInteger index = 0; + if (synthesizedRole == NSAccessibilityCellRole) + return m_columnIndex; + if (QAccessibleInterface *iface = self.qtInterface) { + if (self.isManagedByParent) { + // axid matches the parent table axid so that we can easily find the parent table + // children of row are cell/any items + if (QAccessibleTableInterface *table = iface->tableInterface()) { + if (m_rowIndex >= 0) + index = NSInteger(m_rowIndex); + else if (m_columnIndex >= 0) + index = NSInteger(m_columnIndex); + } + } + } + return index; +} + +- (NSArray *) accessibilityRows { + if (!synthesizedRole && rows) { + QAccessibleInterface *iface = self.qtInterface; + if (iface && iface->tableInterface()) + return NSAccessibilityUnignoredChildren(rows); + } + return nil; +} + +- (NSArray *) accessibilityColumns { + if (!synthesizedRole && columns) { + QAccessibleInterface *iface = self.qtInterface; + if (iface && iface->tableInterface()) + return NSAccessibilityUnignoredChildren(columns); + } + return nil; +} + @end #endif // QT_CONFIG(accessibility) + diff --git a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm index 07ac6023c0..d642115926 100644 --- a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm +++ b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm @@ -148,7 +148,8 @@ QT_USE_NAMESPACE - (void)applicationWillFinishLaunching:(NSNotification *)notification { - Q_UNUSED(notification); + if ([reflectionDelegate respondsToSelector:_cmd]) + [reflectionDelegate applicationWillFinishLaunching:notification]; /* From the Cocoa documentation: "A good place to install event handlers @@ -185,15 +186,34 @@ QT_USE_NAMESPACE - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - Q_UNUSED(aNotification); + if ([reflectionDelegate respondsToSelector:_cmd]) + [reflectionDelegate applicationDidFinishLaunching:aNotification]; + inLaunch = false; if (qEnvironmentVariableIsEmpty("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM")) { - // Move the application window to front to avoid launching behind the terminal. - // Ignoring other apps is necessary (we must ignore the terminal), but makes - // Qt apps play slightly less nice with other apps when lanching from Finder - // (See the activateIgnoringOtherApps docs.) - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + auto frontmostApplication = NSWorkspace.sharedWorkspace.frontmostApplication; + auto currentApplication = NSRunningApplication.currentApplication; + if (frontmostApplication != currentApplication) { + // Move the application to front to avoid launching behind the terminal. + // Ignoring other apps is necessary (we must ignore the terminal), but makes + // Qt apps play slightly less nice with other apps when launching from Finder + // (see the activateIgnoringOtherApps docs). FIXME: Try to distinguish between + // being non-active here because another application stole activation in the + // time it took us to launch from Finder, and being non-active because we were + // launched from Terminal or something that doesn't activate us at all. + qCDebug(lcQpaApplication) << "Launched with" << frontmostApplication + << "as frontmost application. Activating" << currentApplication << "instead."; + [NSApplication.sharedApplication activateIgnoringOtherApps:YES]; + } + + // Qt windows are typically shown in main(), at which point the application + // is not active yet. When the application is activated, either externally + // or via the override above, it will only bring the main and key windows + // forward, which differs from the behavior if these windows had been shown + // once the application was already active. To work around this, we explicitly + // activate the current application again, bringing all windows to the front. + [currentApplication activateWithOptions:NSApplicationActivateAllWindows]; } QCocoaMenuBar::insertWindowMenu(); @@ -232,6 +252,11 @@ QT_USE_NAMESPACE - (void)applicationDidBecomeActive:(NSNotification *)notification { + if (QCocoaWindow::s_applicationActivationObserver) { + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:QCocoaWindow::s_applicationActivationObserver]; + QCocoaWindow::s_applicationActivationObserver = nil; + } + if ([reflectionDelegate respondsToSelector:_cmd]) [reflectionDelegate applicationDidBecomeActive:notification]; @@ -309,18 +334,80 @@ QT_USE_NAMESPACE [self doesNotRecognizeSelector:invocationSelector]; } +- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void(^)(NSArray<id<NSUserActivityRestoring>> *restorableObjects))restorationHandler +{ + // Check if eg. user has installed an app delegate capable of handling this + if ([reflectionDelegate respondsToSelector:_cmd] + && [reflectionDelegate application:application continueUserActivity:userActivity + restorationHandler:restorationHandler] == YES) { + return YES; + } + + if (!QGuiApplication::instance()) + return NO; + + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance(); + Q_ASSERT(cocoaIntegration); + return cocoaIntegration->services()->handleUrl(QUrl::fromNSURL(userActivity.webpageURL)); + } + + return NO; +} + - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { Q_UNUSED(replyEvent); + NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - QWindowSystemInterface::handleFileOpenEvent(QUrl(QString::fromNSString(urlString))); + const QString qurlString = QString::fromNSString(urlString); + + if (event.eventClass == kInternetEventClass && event.eventID == kAEGetURL) { + // 'GURL' (Get URL) event this application should handle + if (!QGuiApplication::instance()) + return; + QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance(); + Q_ASSERT(cocoaIntegration); + cocoaIntegration->services()->handleUrl(QUrl(qurlString)); + return; + } + + // The string we get from the requesting application might not necessarily meet + // QUrl's requirement for a IDN-compliant host. So if we can't parse into a QUrl, + // then we pass the string on to the application as the name of a file (and + // QFileOpenEvent::file is not guaranteed to be the path to a local, open'able + // file anyway). + if (const QUrl url(qurlString); url.isValid()) + QWindowSystemInterface::handleFileOpenEvent(url); + else + QWindowSystemInterface::handleFileOpenEvent(qurlString); } + +- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)application +{ + if (@available(macOS 12, *)) { + if ([reflectionDelegate respondsToSelector:_cmd]) + return [reflectionDelegate applicationSupportsSecureRestorableState:application]; + } + + // We don't support or implement state restorations via the AppKit + // state restoration APIs, but if we did, we would/should support + // secure state restoration. This is the default for apps linked + // against the macOS 14 SDK, but as we target versions below that + // as well we need to return YES here explicitly to silence a runtime + // warning. + return YES; +} + @end @implementation QCocoaApplicationDelegate (Menus) - (BOOL)validateMenuItem:(NSMenuItem*)item { + qCDebug(lcQpaMenus) << "Validating" << item << "for" << self; + auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item); if (!nativeItem) return item.enabled; // FIXME Test with with Qt as plugin or embedded QWindow. @@ -342,6 +429,8 @@ QT_USE_NAMESPACE - (void)qt_itemFired:(QCocoaNSMenuItem *)item { + qCDebug(lcQpaMenus) << "Activating" << item; + if (item.hasSubmenu) return; @@ -353,7 +442,6 @@ QT_USE_NAMESPACE if (!platformItem || platformItem->menu()) return; - QScopedScopeLevelCounter scopeLevelCounter(QGuiApplicationPrivate::instance()->threadData.loadRelaxed()); QGuiApplicationPrivate::modifier_buttons = QAppleKeyMapper::fromCocoaModifiers([NSEvent modifierFlags]); static QMetaMethod activatedSignal = QMetaMethod::fromSignal(&QCocoaMenuItem::activated); diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.h b/src/plugins/platforms/cocoa/qcocoabackingstore.h index 6db88f923c..71b6015a54 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.h +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.h @@ -54,6 +54,7 @@ private: bool eventFilter(QObject *watched, QEvent *event) override; QSize m_requestedSize; + QRegion m_staticContents; class GraphicsBuffer : public QIOSurfaceGraphicsBuffer { @@ -78,7 +79,8 @@ private: bool recreateBackBufferIfNeeded(); void finalizeBackBuffer(); - void preserveFromFrontBuffer(const QRegion ®ion, const QPoint &offset = QPoint()); + void blitBuffer(GraphicsBuffer *sourceBuffer, const QRegion &sourceRegion, + GraphicsBuffer *destinationBuffer, const QPoint &destinationOffset = QPoint()); void backingPropertiesChanged(); QMacNotificationObserver m_backingPropertiesObserver; diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 5d8bd2b7bb..b211b5d02d 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -9,6 +9,7 @@ #include "qcocoahelpers.h" #include <QtCore/qmath.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/qpainter.h> #include <QuartzCore/CATransaction.h> @@ -22,8 +23,9 @@ QCocoaBackingStore::QCocoaBackingStore(QWindow *window) QCFType<CGColorSpaceRef> QCocoaBackingStore::colorSpace() const { - NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view(); - return QCFType<CGColorSpaceRef>::constructFromGet(view.window.colorSpace.CGColorSpace); + const auto *platformWindow = static_cast<QCocoaWindow *>(window()->handle()); + const QNSView *view = qnsview_cast(platformWindow->view()); + return QCFType<CGColorSpaceRef>::constructFromGet(view.colorSpace.CGColorSpace); } // ---------------------------------------------------------------------------- @@ -71,12 +73,11 @@ bool QCALayerBackingStore::eventFilter(QObject *watched, QEvent *event) void QCALayerBackingStore::resize(const QSize &size, const QRegion &staticContents) { - qCDebug(lcQpaBackingStore) << "Resize requested to" << size; - - if (!staticContents.isNull()) - qCWarning(lcQpaBackingStore) << "QCALayerBackingStore does not support static contents"; + qCDebug(lcQpaBackingStore) << "Resize requested to" << size + << "with static contents" << staticContents; m_requestedSize = size; + m_staticContents = staticContents; } void QCALayerBackingStore::beginPaint(const QRegion ®ion) @@ -188,11 +189,50 @@ bool QCALayerBackingStore::recreateBackBufferIfNeeded() } #endif - qCInfo(lcQpaBackingStore) << "Creating surface of" << requestedBufferSize - << "based on requested" << m_requestedSize << "and dpr =" << devicePixelRatio; + qCInfo(lcQpaBackingStore)<< "Creating surface of" << requestedBufferSize + << "for" << window() << "based on requested" << m_requestedSize + << "dpr =" << devicePixelRatio << "and color space" << colorSpace(); static auto pixelFormat = QImage::toPixelFormat(QImage::Format_ARGB32_Premultiplied); - m_buffers.back().reset(new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace())); + auto *newBackBuffer = new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace()); + + if (!m_staticContents.isEmpty() && m_buffers.back()) { + // We implicitly support static backingstore content as a result of + // finalizing the back buffer on flush, where we copy any non-painted + // areas from the front buffer. But there is no guarantee that a resize + // will always come after a flush, where we have a pristine front buffer + // to copy from. It may come after a few begin/endPaints, where the back + // buffer then contains (part of) the latest state. We also have the case + // of single-buffered backingstore, where the front and back buffer is + // the same, which means we must do the copy from the old back buffer + // to the newly resized buffer now, before we replace it below. + + // If the back buffer has been partially filled already, we need to + // copy parts of the static content from that. The rest we copy from + // the front buffer. + const QRegion backBufferRegion = m_staticContents - m_buffers.back()->dirtyRegion; + const QRegion frontBufferRegion = m_staticContents - backBufferRegion; + + qCInfo(lcQpaBackingStore) << "Preserving static content" << backBufferRegion + << "from back buffer, and" << frontBufferRegion << "from front buffer"; + + newBackBuffer->lock(QPlatformGraphicsBuffer::SWWriteAccess); + blitBuffer(m_buffers.back().get(), backBufferRegion, newBackBuffer); + Q_ASSERT(frontBufferRegion.isEmpty() || m_buffers.front()); + blitBuffer(m_buffers.front().get(), frontBufferRegion, newBackBuffer); + newBackBuffer->unlock(); + + // The new back buffer now is valid for the static contents region. + // We don't need to maintain the static contents region for resizes + // of any other buffers in the swap chain, as these will finalize + // their content on flush from the buffer we just filled, and we + // don't need to mark them dirty for the area we just filled, as + // new buffers are fully dirty when created. + newBackBuffer->dirtyRegion -= m_staticContents; + m_staticContents = {}; + } + + m_buffers.back().reset(newBackBuffer); return true; } @@ -255,7 +295,7 @@ bool QCALayerBackingStore::scroll(const QRegion ®ion, int dx, int dy) if (!frontBufferRegion.isEmpty()) { qCDebug(lcQpaBackingStore) << "Scrolling" << frontBufferRegion << "by copying from front buffer"; - preserveFromFrontBuffer(frontBufferRegion, scrollDelta); + blitBuffer(m_buffers.front().get(), frontBufferRegion, m_buffers.back().get(), scrollDelta); } m_buffers.back()->unlock(); @@ -439,10 +479,11 @@ void QCALayerBackingStore::backingPropertiesChanged() qCDebug(lcQpaBackingStore) << "Backing properties for" << window() << "did change"; - qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers"; + const auto newColorSpace = colorSpace(); + qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers to" << newColorSpace; for (auto &buffer : m_buffers) { if (buffer) - buffer->setColorSpace(colorSpace()); + buffer->setColorSpace(newColorSpace); } } @@ -474,54 +515,76 @@ void QCALayerBackingStore::finalizeBackBuffer() if (!m_buffers.back()->isDirty()) return; - m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); - preserveFromFrontBuffer(m_buffers.back()->dirtyRegion); - m_buffers.back()->unlock(); + qCDebug(lcQpaBackingStore) << "Finalizing back buffer with dirty region" << m_buffers.back()->dirtyRegion; + + if (m_buffers.back() != m_buffers.front()) { + m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); + blitBuffer(m_buffers.front().get(), m_buffers.back()->dirtyRegion, m_buffers.back().get()); + m_buffers.back()->unlock(); + } else { + qCDebug(lcQpaBackingStore) << "Front and back buffer is the same. Can not finalize back buffer."; + } // The back buffer is now completely in sync, ready to be presented m_buffers.back()->dirtyRegion = QRegion(); } -void QCALayerBackingStore::preserveFromFrontBuffer(const QRegion ®ion, const QPoint &offset) +/* + \internal + + Blits \a sourceRegion from \a sourceBuffer to \a destinationBuffer, + at offset \a destinationOffset. + + The source buffer is automatically locked for read only access + during the blit. + + The destination buffer has to be locked for write access by the + caller. +*/ + +void QCALayerBackingStore::blitBuffer(GraphicsBuffer *sourceBuffer, const QRegion &sourceRegion, + GraphicsBuffer *destinationBuffer, const QPoint &destinationOffset) { + Q_ASSERT(sourceBuffer && destinationBuffer); + Q_ASSERT(sourceBuffer != destinationBuffer); - if (m_buffers.front() == m_buffers.back()) - return; // Nothing to preserve from + if (sourceRegion.isEmpty()) + return; - qCDebug(lcQpaBackingStore) << "Preserving" << region << "of front buffer to" - << region.translated(offset) << "of back buffer"; + qCDebug(lcQpaBackingStore) << "Blitting" << sourceRegion << "of" << sourceBuffer + << "to" << sourceRegion.translated(destinationOffset) << "of" << destinationBuffer; - Q_ASSERT(m_buffers.back()->isLocked() == QPlatformGraphicsBuffer::SWWriteAccess); + Q_ASSERT(destinationBuffer->isLocked() == QPlatformGraphicsBuffer::SWWriteAccess); - m_buffers.front()->lock(QPlatformGraphicsBuffer::SWReadAccess); - const QImage *frontBuffer = m_buffers.front()->asImage(); + sourceBuffer->lock(QPlatformGraphicsBuffer::SWReadAccess); + const QImage *sourceImage = sourceBuffer->asImage(); - const QRect frontSurfaceBounds(QPoint(0, 0), m_buffers.front()->size()); - const qreal sourceDevicePixelRatio = frontBuffer->devicePixelRatio(); + const QRect sourceBufferBounds(QPoint(0, 0), sourceBuffer->size()); + const qreal sourceDevicePixelRatio = sourceImage->devicePixelRatio(); - QPainter painter(m_buffers.back()->asImage()); + QPainter painter(destinationBuffer->asImage()); painter.setCompositionMode(QPainter::CompositionMode_Source); // Let painter operate in device pixels, to make it easier to compare coordinates - const qreal targetDevicePixelRatio = painter.device()->devicePixelRatio(); - painter.scale(1.0 / targetDevicePixelRatio, 1.0 / targetDevicePixelRatio); + const qreal destinationDevicePixelRatio = painter.device()->devicePixelRatio(); + painter.scale(1.0 / destinationDevicePixelRatio, 1.0 / destinationDevicePixelRatio); - for (const QRect &rect : region) { + for (const QRect &rect : sourceRegion) { QRect sourceRect(rect.topLeft() * sourceDevicePixelRatio, rect.size() * sourceDevicePixelRatio); - QRect targetRect((rect.topLeft() + offset) * targetDevicePixelRatio, - rect.size() * targetDevicePixelRatio); + QRect destinationRect((rect.topLeft() + destinationOffset) * destinationDevicePixelRatio, + rect.size() * destinationDevicePixelRatio); #ifdef QT_DEBUG - if (Q_UNLIKELY(!frontSurfaceBounds.contains(sourceRect.bottomRight()))) { - qCWarning(lcQpaBackingStore) << "Front buffer too small to preserve" - << QRegion(sourceRect).subtracted(frontSurfaceBounds); + if (Q_UNLIKELY(!sourceBufferBounds.contains(sourceRect.bottomRight()))) { + qCWarning(lcQpaBackingStore) << "Source buffer of size" << sourceBuffer->size() + << "is too small to blit" << sourceRect; } #endif - painter.drawImage(targetRect, *frontBuffer, sourceRect); + painter.drawImage(destinationRect, *sourceImage, sourceRect); } - m_buffers.front()->unlock(); + sourceBuffer->unlock(); } // ---------------------------------------------------------------------------- @@ -559,6 +622,6 @@ QImage *QCALayerBackingStore::GraphicsBuffer::asImage() return &m_image; } -#include "moc_qcocoabackingstore.cpp" - QT_END_NAMESPACE + +#include "moc_qcocoabackingstore.cpp" diff --git a/src/plugins/platforms/cocoa/qcocoaclipboard.mm b/src/plugins/platforms/cocoa/qcocoaclipboard.mm index 8b91e9d5d1..241faadbec 100644 --- a/src/plugins/platforms/cocoa/qcocoaclipboard.mm +++ b/src/plugins/platforms/cocoa/qcocoaclipboard.mm @@ -3,13 +3,15 @@ #include "qcocoaclipboard.h" +#include <QtGui/qutimimeconverter.h> + #ifndef QT_NO_CLIPBOARD QT_BEGIN_NAMESPACE QCocoaClipboard::QCocoaClipboard() - :m_clipboard(new QMacPasteboard(kPasteboardClipboard, QMacInternalPasteboardMime::MIME_CLIP)) - ,m_find(new QMacPasteboard(kPasteboardFind, QMacInternalPasteboardMime::MIME_CLIP)) + :m_clipboard(new QMacPasteboard(kPasteboardClipboard, QUtiMimeConverter::HandlerScopeFlag::Clipboard)) + ,m_find(new QMacPasteboard(kPasteboardFind, QUtiMimeConverter::HandlerScopeFlag::Clipboard)) { connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QCocoaClipboard::handleApplicationStateChanged); } @@ -68,8 +70,8 @@ void QCocoaClipboard::handleApplicationStateChanged(Qt::ApplicationState state) emitChanged(QClipboard::FindBuffer); } -#include "moc_qcocoaclipboard.cpp" - QT_END_NAMESPACE +#include "moc_qcocoaclipboard.cpp" + #endif // QT_NO_CLIPBOARD diff --git a/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm b/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm index 777c3d65b6..b58f58caeb 100644 --- a/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm @@ -182,7 +182,17 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSColorPanelDelegate); mDialogIsExecuting = false; mResultSet = false; mClosingDueToKnownButton = false; - [mColorPanel makeKeyAndOrderFront:mColorPanel]; + // Make this an asynchronous call, so the panel is made key only + // in the next event loop run. This is to make sure that by + // the time the modal loop is run in runModalForWindow below, + // which internally also sets the panel to key window, + // the panel is not yet key, and the NSApp still has the right + // reference to the _previousKeyWindow. Otherwise both NSApp.key + // and NSApp._previousKeyWindow would wrongly point to the panel, + // loosing any reference to the window that was key before. + dispatch_async(dispatch_get_main_queue(), ^{ + [mColorPanel makeKeyAndOrderFront:mColorPanel]; + }); } - (BOOL)runApplicationModalPanel diff --git a/src/plugins/platforms/cocoa/qcocoacursor.mm b/src/plugins/platforms/cocoa/qcocoacursor.mm index 3c1b67830f..aaa35a91ef 100644 --- a/src/plugins/platforms/cocoa/qcocoacursor.mm +++ b/src/plugins/platforms/cocoa/qcocoacursor.mm @@ -240,7 +240,7 @@ NSCursor *QCocoaCursor::createCursorData(QCursor *cursor) switch (cursor->shape()) { case Qt::BitmapCursor: { if (cursor->pixmap().isNull()) - return createCursorFromBitmap(cursor->bitmap(Qt::ReturnByValue), cursor->mask(Qt::ReturnByValue), hotspot); + return createCursorFromBitmap(cursor->bitmap(), cursor->mask(), hotspot); else return createCursorFromPixmap(cursor->pixmap(), hotspot); break; } diff --git a/src/plugins/platforms/cocoa/qcocoadrag.h b/src/plugins/platforms/cocoa/qcocoadrag.h index 24485ac6ac..dedf8a7fd9 100644 --- a/src/plugins/platforms/cocoa/qcocoadrag.h +++ b/src/plugins/platforms/cocoa/qcocoadrag.h @@ -44,7 +44,7 @@ private: NSEvent *m_lastEvent; NSView *m_lastView; Qt::DropAction m_executed_drop_action; - QEventLoop internalDragLoop; + QEventLoop *m_internalDragLoop = nullptr; bool maybeDragMultipleItems(); diff --git a/src/plugins/platforms/cocoa/qcocoadrag.mm b/src/plugins/platforms/cocoa/qcocoadrag.mm index 50f094716f..a8404889e9 100644 --- a/src/plugins/platforms/cocoa/qcocoadrag.mm +++ b/src/plugins/platforms/cocoa/qcocoadrag.mm @@ -2,12 +2,15 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <AppKit/AppKit.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> #include "qcocoadrag.h" #include "qmacclipboard.h" #include "qcocoahelpers.h" #include <QtGui/private/qcoregraphics_p.h> +#include <QtGui/qutimimeconverter.h> #include <QtCore/qsysinfo.h> +#include <QtCore/private/qcore_mac_p.h> #include <vector> @@ -96,7 +99,7 @@ Qt::DropAction QCocoaDrag::drag(QDrag *o) m_drag = o; m_executed_drop_action = Qt::IgnoreAction; - QMacPasteboard dragBoard(CFStringRef(NSPasteboardNameDrag), QMacInternalPasteboardMime::MIME_DND); + QMacPasteboard dragBoard(CFStringRef(NSPasteboardNameDrag), QUtiMimeConverter::HandlerScopeFlag::DnD); m_drag->mimeData()->setData("application/x-qt-mime-type-name"_L1, QByteArray("dummy")); dragBoard.setMimeData(m_drag->mimeData(), QMacPasteboard::LazyRequest); @@ -159,8 +162,7 @@ bool QCocoaDrag::maybeDragMultipleItems() for (NSPasteboardItem *item in dragBoard.pasteboardItems) { bool isUrl = false; for (NSPasteboardType type in item.types) { - using NSStringRef = NSString *; - if ([type isEqualToString:NSStringRef(kUTTypeFileURL)]) { + if ([type isEqualToString:UTTypeFileURL.identifier]) { isUrl = true; break; } @@ -184,6 +186,12 @@ bool QCocoaDrag::maybeDragMultipleItems() // contains a combined picture for all urls we drag. auto imageOrNil = dragImage; for (const auto &qtUrl : qtUrls) { + if (!qtUrl.isValid()) + continue; + + if (qtUrl.isRelative()) // NSPasteboardWriting rejects such items. + continue; + NSURL *nsUrl = qtUrl.toNSURL(); auto *newItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:nsUrl] autorelease]; const NSRect itemFrame = NSMakeRect(itemLocation.x, itemLocation.y, @@ -205,7 +213,9 @@ bool QCocoaDrag::maybeDragMultipleItems() } [sourceView beginDraggingSessionWithItems:dragItems event:m_lastEvent source:sourceView]; - internalDragLoop.exec(); + QEventLoop eventLoop; + QScopedValueRollback updateGuard(m_internalDragLoop, &eventLoop); + eventLoop.exec(); return true; } @@ -216,8 +226,10 @@ void QCocoaDrag::setAcceptedAction(Qt::DropAction act) void QCocoaDrag::exitDragLoop() { - if (internalDragLoop.isRunning()) - internalDragLoop.exit(); + if (m_internalDragLoop) { + Q_ASSERT(m_internalDragLoop->isRunning()); + m_internalDragLoop->exit(); + } } @@ -232,14 +244,14 @@ QPixmap QCocoaDrag::dragPixmap(QDrag *drag, QPoint &hotSpot) const QFontMetrics fm(f); if (data->hasImage()) { - const QImage img = data->imageData().value<QImage>(); + QImage img = data->imageData().value<QImage>(); if (!img.isNull()) { - pm = QPixmap::fromImage(img).scaledToWidth(dragImageMaxChars *fm.averageCharWidth()); + pm = QPixmap::fromImage(std::move(img)).scaledToWidth(dragImageMaxChars *fm.averageCharWidth()); } } if (pm.isNull() && (data->hasText() || data->hasUrls()) ) { - QString s = data->hasText() ? data->text() : data->urls().first().toString(); + QString s = data->hasText() ? data->text() : data->urls().constFirst().toString(); if (s.length() > dragImageMaxChars) s = s.left(dragImageMaxChars -3) + QChar(0x2026); if (!s.isEmpty()) { @@ -297,11 +309,11 @@ QStringList QCocoaDropData::formats_sys() const qDebug("DnD: Cannot get PasteBoard!"); return formats; } - formats = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).formats(); + formats = QMacPasteboard(board, QUtiMimeConverter::HandlerScopeFlag::DnD).formats(); return formats; } -QVariant QCocoaDropData::retrieveData_sys(const QString &mimeType, QMetaType type) const +QVariant QCocoaDropData::retrieveData_sys(const QString &mimeType, QMetaType) const { QVariant data; PasteboardRef board; @@ -309,7 +321,7 @@ QVariant QCocoaDropData::retrieveData_sys(const QString &mimeType, QMetaType typ qDebug("DnD: Cannot get PasteBoard!"); return data; } - data = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).retrieveData(mimeType, type); + data = QMacPasteboard(board, QUtiMimeConverter::HandlerScopeFlag::DnD).retrieveData(mimeType); CFRelease(board); return data; } @@ -322,7 +334,7 @@ bool QCocoaDropData::hasFormat_sys(const QString &mimeType) const qDebug("DnD: Cannot get PasteBoard!"); return has; } - has = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).hasFormat(mimeType); + has = QMacPasteboard(board, QUtiMimeConverter::HandlerScopeFlag::DnD).hasFormat(mimeType); CFRelease(board); return has; } diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 7a291022fe..96eb70dabc 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -55,20 +55,26 @@ #include <QtCore/private/qabstracteventdispatcher_p.h> #include <QtCore/private/qcfsocketnotifier_p.h> #include <QtCore/private/qtimerinfo_unix_p.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qpointer.h> #include <CoreFoundation/CoreFoundation.h> +Q_FORWARD_DECLARE_OBJC_CLASS(NSWindow); + QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcher); + typedef struct _NSModalSession *NSModalSession; typedef struct _QCocoaModalSessionInfo { QPointer<QWindow> window; NSModalSession session; - void *nswindow; + NSWindow *nswindow; } QCocoaModalSessionInfo; class QCocoaEventDispatcherPrivate; -class QCocoaEventDispatcher : public QAbstractEventDispatcher +class QCocoaEventDispatcher : public QAbstractEventDispatcherV2 { Q_OBJECT Q_DECLARE_PRIVATE(QCocoaEventDispatcher) @@ -83,12 +89,12 @@ public: void registerSocketNotifier(QSocketNotifier *notifier); void unregisterSocketNotifier(QSocketNotifier *notifier); - void registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object); - bool unregisterTimer(int timerId); - bool unregisterTimers(QObject *object); - QList<TimerInfo> registeredTimers(QObject *object) const; - - int remainingTime(int timerId); + void registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, + QObject *object) final; + bool unregisterTimer(Qt::TimerId timerId) final; + bool unregisterTimers(QObject *object) final; + QList<TimerInfoV2> timersForObject(QObject *object) const final; + Duration remainingTime(Qt::TimerId timerId) const final; void wakeUp(); void interrupt(); @@ -126,6 +132,7 @@ public: QStack<QCocoaModalSessionInfo> cocoaModalSessionStack; bool currentExecIsNSAppRun; bool nsAppRunCalledByQt; + bool initializingNSApplication = false; bool cleanupModalSessionsNeeded; uint processEventsCalled; NSModalSession currentModalSessionCached; @@ -144,7 +151,6 @@ public: QList<void *> queuedUserInputEvents; // NSEvent * CFRunLoopSourceRef postedEventsSource; CFRunLoopObserverRef waitingObserver; - CFRunLoopObserverRef firstTimeObserver; QAtomicInt serialNumber; int lastSerial; bool interrupt; @@ -153,7 +159,6 @@ public: static void postedEventsSourceCallback(void *info); static void waitingObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info); - static void firstLoopEntry(CFRunLoopObserverRef ref, CFRunLoopActivity activity, void *info); bool sendQueuedUserInputEvents(); void processPostedEvents(); }; diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index 1197706397..739fbda4f5 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -49,6 +49,7 @@ #include <QtCore/qscopeguard.h> #include <QtCore/qsocketnotifier.h> #include <QtCore/private/qthread_p.h> +#include <QtCore/private/qcore_mac_p.h> #include <qpa/qplatformwindow.h> #include <qpa/qplatformnativeinterface.h> @@ -57,6 +58,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher"); + static inline CFRunLoopRef mainRunLoop() { return CFRunLoopGetMain(); @@ -86,6 +89,13 @@ void QCocoaEventDispatcherPrivate::runLoopTimerCallback(CFRunLoopTimerRef, void void QCocoaEventDispatcherPrivate::activateTimersSourceCallback(void *info) { QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info); + if (d->initializingNSApplication) { + qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization"; + // We don't want to process any sources during explicit NSApplication + // initialization, so defer the source until the actual event processing. + CFRunLoopSourceSignal(d->activateTimersSourceRef); + return; + } d->processTimers(); d->maybeCancelWaitForMoreEvents(); } @@ -105,6 +115,7 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() return; } + using DoubleSeconds = std::chrono::duration<double, std::ratio<1>>; if (!runLoopTimerRef) { // start the CFRunLoopTimer CFAbsoluteTime ttf = CFAbsoluteTimeGetCurrent(); @@ -112,10 +123,10 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() CFTimeInterval oneyear = CFTimeInterval(3600. * 24. * 365.); // Q: when should the CFRunLoopTimer fire for the first time? - struct timespec tv; - if (timerInfoList.timerWait(tv)) { + if (auto opt = timerInfoList.timerWait()) { // A: when we have timers to fire, of course - interval = qMax(tv.tv_sec + tv.tv_nsec / 1000000000., 0.0000001); + DoubleSeconds secs{*opt}; + interval = qMax(secs.count(), 0.0000001); } else { // this shouldn't really happen, but in case it does, set the timer to fire a some point in the distant future interval = oneyear; @@ -135,10 +146,10 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() CFTimeInterval interval; // Q: when should the timer first next? - struct timespec tv; - if (timerInfoList.timerWait(tv)) { + if (auto opt = timerInfoList.timerWait()) { // A: when we have timers to fire, of course - interval = qMax(tv.tv_sec + tv.tv_nsec / 1000000000., 0.0000001); + DoubleSeconds secs{*opt}; + interval = qMax(secs.count(), 0.0000001); } else { // no timers can fire, but we cannot stop the CFRunLoopTimer, set the timer to fire at some // point in the distant future (the timer interval is one year) @@ -160,10 +171,11 @@ void QCocoaEventDispatcherPrivate::maybeStopCFRunLoopTimer() runLoopTimerRef = nullptr; } -void QCocoaEventDispatcher::registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *obj) +void QCocoaEventDispatcher::registerTimer(Qt::TimerId timerId, Duration interval, + Qt::TimerType timerType, QObject *obj) { #ifndef QT_NO_DEBUG - if (timerId < 1 || interval < 0 || !obj) { + if (qToUnderlying(timerId) < 1 || interval.count() < 0 || !obj) { qWarning("QCocoaEventDispatcher::registerTimer: invalid arguments"); return; } else if (obj->thread() != thread() || thread() != QThread::currentThread()) { @@ -177,10 +189,10 @@ void QCocoaEventDispatcher::registerTimer(int timerId, qint64 interval, Qt::Time d->maybeStartCFRunLoopTimer(); } -bool QCocoaEventDispatcher::unregisterTimer(int timerId) +bool QCocoaEventDispatcher::unregisterTimer(Qt::TimerId timerId) { #ifndef QT_NO_DEBUG - if (timerId < 1) { + if (qToUnderlying(timerId) < 1) { qWarning("QCocoaEventDispatcher::unregisterTimer: invalid argument"); return false; } else if (thread() != QThread::currentThread()) { @@ -219,13 +231,13 @@ bool QCocoaEventDispatcher::unregisterTimers(QObject *obj) return returnValue; } -QList<QCocoaEventDispatcher::TimerInfo> -QCocoaEventDispatcher::registeredTimers(QObject *object) const +QList<QCocoaEventDispatcher::TimerInfoV2> +QCocoaEventDispatcher::timersForObject(QObject *object) const { #ifndef QT_NO_DEBUG if (!object) { qWarning("QCocoaEventDispatcher:registeredTimers: invalid argument"); - return QList<TimerInfo>(); + return {}; } #endif @@ -364,6 +376,7 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) // [NSApp run], which is the normal code path for cocoa applications. if (NSModalSession session = d->currentModalSession()) { QBoolBlocker execGuard(d->currentExecIsNSAppRun, false); + qCDebug(lcEventDispatcher) << "Running modal session" << session; while ([NSApp runModalSession:session] == NSModalResponseContinue && !d->interrupt) { qt_mac_waitForMoreEvents(NSModalPanelRunLoopMode); if (session != d->currentModalSessionCached) { @@ -407,6 +420,7 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) // to use cocoa's native way of running modal sessions: if (flags & QEventLoop::WaitForMoreEvents) qt_mac_waitForMoreEvents(NSModalPanelRunLoopMode); + qCDebug(lcEventDispatcher) << "Running modal session" << session; NSInteger status = [NSApp runModalSession:session]; if (status != NSModalResponseContinue && session == d->currentModalSessionCached) { // INVARIANT: Someone called [NSApp stopModal:] from outside the event @@ -527,17 +541,17 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) return retVal; } -int QCocoaEventDispatcher::remainingTime(int timerId) +auto QCocoaEventDispatcher::remainingTime(Qt::TimerId timerId) const -> Duration { #ifndef QT_NO_DEBUG - if (timerId < 1) { + if (qToUnderlying(timerId) < 1) { qWarning("QCocoaEventDispatcher::remainingTime: invalid argument"); - return -1; + return Duration::min(); } #endif - Q_D(QCocoaEventDispatcher); - return d->timerInfoList.timerRemainingTime(timerId); + Q_D(const QCocoaEventDispatcher); + return d->timerInfoList.remainingDuration(timerId); } void QCocoaEventDispatcher::wakeUp() @@ -554,22 +568,42 @@ void QCocoaEventDispatcher::wakeUp() void QCocoaEventDispatcherPrivate::ensureNSAppInitialized() { - // Some elements in Cocoa require NSApplication to be running before - // they get fully initialized, in particular the menu bar. This - // function is intended for cases where a dialog is told to execute before - // QGuiApplication::exec is called, or the application spins the events loop - // manually rather than calling QGuiApplication:exec. - // The function makes sure that NSApplication starts running, but stops - // it again as soon as the send posted events callback is called. That way - // we let Cocoa finish the initialization it seems to need. We'll only - // apply this trick at most once for any application, and we avoid doing it - // for the common case where main just starts QGuiApplication::exec. + // Some elements in Cocoa require NSApplication to be initialized before + // use, for example the menu bar. Under normal circumstances this happens + // as part of [NSApp run], as a result of a call to QGuiApplication:exec(), + // but in the cases where a dialog is asked to execute before that happens, + // or the application spins the event loop manually via processEvents(), + // we need to explicitly ensure NSApplication initialization. + + // We can unfortunately not do this via NSApplicationLoad(), as the function + // bails out early if there's already an NSApplication instance, which is + // the case if any code has called [NSApplication sharedApplication], + // or its short form 'NSApp'. + + // Instead we do an actual [NSApp run], but stop the application as soon + // as possible, ensuring that AppKit will do the required initialization, + // including calling [NSApplication finishLaunching]. + + // We only apply this trick at most once for any application, and we avoid + // doing it for the common case where main just starts QGuiApplication::exec. if (nsAppRunCalledByQt || [NSApp isRunning]) return; + + qCDebug(lcEventDispatcher) << "Ensuring NSApplication is initialized"; nsAppRunCalledByQt = true; - QBoolBlocker block1(interrupt, true); - QBoolBlocker block2(currentExecIsNSAppRun, true); + + // Stopping the application will still process runloop sources before + // actually stopping, so we need to explicitly guard our sources from + // doing anything, deferring their actions until later. + QBoolBlocker initializationGuard(initializingNSApplication, true); + + CFRunLoopPerformBlock(mainRunLoop(), kCFRunLoopCommonModes, ^{ + qCDebug(lcEventDispatcher) << "NSApplication has been initialized; Stopping NSApp"; + [NSApp stop:NSApp]; + cancelWaitForMoreEvents(); // Post event that wakes up the runloop + }); [NSApp run]; + qCDebug(lcEventDispatcher) << "Finished ensuring NSApplication is initialized"; } void QCocoaEventDispatcherPrivate::temporarilyStopAllModalSessions() @@ -587,6 +621,8 @@ void QCocoaEventDispatcherPrivate::temporarilyStopAllModalSessions() for (int i=0; i<stackSize; ++i) { QCocoaModalSessionInfo &info = cocoaModalSessionStack[i]; if (info.session) { + qCDebug(lcEventDispatcher) << "Temporarily ending modal session" << info.session + << "for" << info.nswindow; [NSApp endModalSession:info.session]; info.session = nullptr; [(NSWindow*) info.nswindow release]; @@ -626,6 +662,8 @@ NSModalSession QCocoaEventDispatcherPrivate::currentModalSession() [(NSWindow*) info.nswindow retain]; QRect rect = cocoaWindow->geometry(); info.session = [NSApp beginModalSessionForWindow:nswindow]; + qCDebug(lcEventDispatcher) << "Begun modal session" << info.session + << "for" << nswindow; // The call to beginModalSessionForWindow above processes events and may // have deleted or destroyed the window. Check if it's still valid. @@ -674,6 +712,8 @@ void QCocoaEventDispatcherPrivate::cleanupModalSessions() currentModalSessionCached = nullptr; if (info.session) { Q_ASSERT(info.nswindow); + qCDebug(lcEventDispatcher) << "Ending modal session" << info.session + << "for" << info.nswindow; [NSApp endModalSession:info.session]; [(NSWindow *)info.nswindow release]; } @@ -686,6 +726,14 @@ void QCocoaEventDispatcherPrivate::cleanupModalSessions() void QCocoaEventDispatcherPrivate::beginModalSession(QWindow *window) { + qCDebug(lcEventDispatcher) << "Adding modal session for" << window; + + if (std::any_of(cocoaModalSessionStack.constBegin(), cocoaModalSessionStack.constEnd(), + [&](const auto &sessionInfo) { return sessionInfo.window == window; })) { + qCWarning(lcEventDispatcher) << "Modal session for" << window << "already exists!"; + return; + } + // We need to start spinning the modal session. Usually this is done with // QDialog::exec() for Qt Widgets based applications, but for others that // just call show(), we need to interrupt(). @@ -706,6 +754,8 @@ void QCocoaEventDispatcherPrivate::beginModalSession(QWindow *window) void QCocoaEventDispatcherPrivate::endModalSession(QWindow *window) { + qCDebug(lcEventDispatcher) << "Removing modal session for" << window; + Q_Q(QCocoaEventDispatcher); // Mark all sessions attached to window as pending to be stopped. We do this @@ -752,7 +802,7 @@ void qt_mac_maybeCancelWaitForMoreEventsForwarder(QAbstractEventDispatcher *even } QCocoaEventDispatcher::QCocoaEventDispatcher(QObject *parent) - : QAbstractEventDispatcher(*new QCocoaEventDispatcherPrivate, parent) + : QAbstractEventDispatcherV2(*new QCocoaEventDispatcherPrivate, parent) { Q_D(QCocoaEventDispatcher); @@ -789,21 +839,6 @@ QCocoaEventDispatcher::QCocoaEventDispatcher(QObject *parent) QCocoaEventDispatcherPrivate::waitingObserverCallback, &observerContext); CFRunLoopAddObserver(mainRunLoop(), d->waitingObserver, kCFRunLoopCommonModes); - - /* The first cycle in the loop adds the source and the events of the source - are not processed. - We use an observer to process the posted events for the first - execution of the loop. */ - CFRunLoopObserverContext firstTimeObserverContext; - bzero(&firstTimeObserverContext, sizeof(CFRunLoopObserverContext)); - firstTimeObserverContext.info = d; - d->firstTimeObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, - kCFRunLoopEntry, - /* repeats = */ false, - 0, - QCocoaEventDispatcherPrivate::firstLoopEntry, - &firstTimeObserverContext); - CFRunLoopAddObserver(mainRunLoop(), d->firstTimeObserver, kCFRunLoopCommonModes); } void QCocoaEventDispatcherPrivate::waitingObserverCallback(CFRunLoopObserverRef, @@ -868,18 +903,17 @@ void QCocoaEventDispatcherPrivate::processPostedEvents() } } -void QCocoaEventDispatcherPrivate::firstLoopEntry(CFRunLoopObserverRef ref, - CFRunLoopActivity activity, - void *info) -{ - Q_UNUSED(ref); - Q_UNUSED(activity); - static_cast<QCocoaEventDispatcherPrivate *>(info)->processPostedEvents(); -} - void QCocoaEventDispatcherPrivate::postedEventsSourceCallback(void *info) { QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info); + if (d->initializingNSApplication) { + qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization"; + // We don't want to process any sources during explicit NSApplication + // initialization, so defer the source until the actual event processing. + CFRunLoopSourceSignal(d->postedEventsSource); + return; + } + if (d->processEventsCalled && (d->processEventsFlags & QEventLoop::EventLoopExec) == 0) { // processEvents() was called "manually," ignore this source for now d->maybeCancelWaitForMoreEvents(); @@ -943,7 +977,7 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher() { Q_D(QCocoaEventDispatcher); - qDeleteAll(d->timerInfoList); + d->timerInfoList.clearTimers(); d->maybeStopCFRunLoopTimer(); CFRunLoopRemoveSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes); CFRelease(d->activateTimersSourceRef); @@ -952,6 +986,8 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher() for (int i = 0; i < d->cocoaModalSessionStack.count(); ++i) { QCocoaModalSessionInfo &info = d->cocoaModalSessionStack[i]; if (info.session) { + qCDebug(lcEventDispatcher) << "Ending modal session" << info.session + << "for" << info.nswindow << "during shutdown"; [NSApp endModalSession:info.session]; [(NSWindow *)info.nswindow release]; } @@ -970,9 +1006,6 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher() CFRunLoopObserverInvalidate(d->waitingObserver); CFRelease(d->waitingObserver); - - CFRunLoopObserverInvalidate(d->firstTimeObserver); - CFRelease(d->firstTimeObserver); } QtCocoaInterruptDispatcher* QtCocoaInterruptDispatcher::instance = nullptr; diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h index 85e317b8ef..3ffccb10fd 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h @@ -38,6 +38,7 @@ public: public: // for QNSOpenSavePanelDelegate void panelClosed(NSInteger result); + void panelDirectoryDidChange(NSString *path); private: void createNSOpenSavePanelDelegate(); diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm index bb32774d1d..044a282686 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -18,6 +18,8 @@ #include <QtCore/qoperatingsystemversion.h> #include <QtCore/qdir.h> #include <QtCore/qregularexpression.h> +#include <QtCore/qpointer.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/qguiapplication.h> #include <QtGui/private/qguiapplication_p.h> @@ -25,6 +27,8 @@ #include <qpa/qplatformtheme.h> #include <qpa/qplatformnativeinterface.h> +#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> + QT_USE_NAMESPACE using namespace Qt::StringLiterals; @@ -52,13 +56,12 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; NSView *m_accessoryView; NSPopUpButton *m_popupButton; NSTextField *m_textField; - QCocoaFileDialogHelper *m_helper; - NSString *m_currentDirectory; + QPointer<QCocoaFileDialogHelper> m_helper; SharedPointerFileDialogOptions m_options; - QString *m_currentSelection; - QStringList *m_nameFilterDropDownList; - QStringList *m_selectedNameFilter; + QString m_currentSelection; + QStringList m_nameFilterDropDownList; + QStringList m_selectedNameFilter; } - (instancetype)initWithAcceptMode:(const QString &)selectFile @@ -78,26 +81,56 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; m_helper = helper; - m_nameFilterDropDownList = new QStringList(m_options->nameFilters()); + m_nameFilterDropDownList = m_options->nameFilters(); QString selectedVisualNameFilter = m_options->initiallySelectedNameFilter(); - m_selectedNameFilter = new QStringList([self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]); - - QFileInfo sel(selectFile); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]; + + m_panel.extensionHidden = [&]{ + for (const auto &nameFilter : m_nameFilterDropDownList) { + const auto extensions = QPlatformFileDialogHelper::cleanFilterList(nameFilter); + for (const auto &extension : extensions) { + // Explicitly show extensions if we detect a filter + // of "all files", as clicking a single file with + // extensions hidden will then populate the name + // field with only the file name, without any + // extension. + if (extension == "*"_L1 || extension == "*.*"_L1) + return false; + + // Explicitly show extensions if we detect a filter + // that has a multi-part extension. This prevents + // confusing situations where the user clicks e.g. + // 'foo.tar.gz' and 'foo.tar' is populated in the + // file name box, but when then clicking save macOS + // will warn that the file needs to end in .gz, + // due to thinking the user tried to save the file + // as a 'tar' file instead. Unfortunately this + // property can only be set before the panel is + // shown, so we can't toggle it on and off based + // on the active filter. + if (extension.count('.') > 1) + return false; + } + } + return true; + }(); + + const QFileInfo sel(selectFile); if (sel.isDir() && !sel.isBundle()){ - m_currentDirectory = [sel.absoluteFilePath().toNSString() retain]; - m_currentSelection = new QString; + m_panel.directoryURL = [NSURL fileURLWithPath:sel.absoluteFilePath().toNSString()]; + m_currentSelection.clear(); } else { - m_currentDirectory = [sel.absolutePath().toNSString() retain]; - m_currentSelection = new QString(sel.absoluteFilePath()); + m_panel.directoryURL = [NSURL fileURLWithPath:sel.absolutePath().toNSString()]; + m_currentSelection = sel.absoluteFilePath(); } [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)]; [self createTextField]; [self createAccessory]; - m_panel.accessoryView = m_nameFilterDropDownList->size() > 1 ? m_accessoryView : nil; + m_panel.accessoryView = m_nameFilterDropDownList.size() > 1 ? m_accessoryView : nil; // -setAccessoryView: can result in -panel:directoryDidChange: - // resetting our m_currentDirectory, set the delegate + // resetting our current directory. Set the delegate // here to make sure it gets the correct value. m_panel.delegate = self; @@ -111,10 +144,6 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (void)dealloc { - delete m_nameFilterDropDownList; - delete m_selectedNameFilter; - delete m_currentSelection; - [m_panel orderOut:m_panel]; m_panel.accessoryView = nil; [m_popupButton release]; @@ -122,19 +151,17 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; [m_accessoryView release]; m_panel.delegate = nil; [m_panel release]; - [m_currentDirectory release]; [super dealloc]; } - (bool)showPanel:(Qt::WindowModality) windowModality withParent:(QWindow *)parent { - QFileInfo info(*m_currentSelection); + const QFileInfo info(m_currentSelection); NSString *filepath = info.filePath().toNSString(); NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; bool selectable = (m_options->acceptMode() == QFileDialogOptions::AcceptSave) || [self panel:m_panel shouldEnableURL:url]; - m_panel.directoryURL = [NSURL fileURLWithPath:m_currentDirectory]; m_panel.nameFieldStringValue = selectable ? info.fileName().toNSString() : @""; [self updateProperties]; @@ -159,6 +186,8 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; // QEventLoop has been interrupted, and the second-most event loop has not // yet been reactivated (regardless if [NSApp run] is still on the stack)), // showing a native modal dialog will fail. + if (!m_helper) + return; QMacAutoReleasePool pool; @@ -180,7 +209,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (void)closePanel { - *m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); + m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); if (m_panel.sheet) [NSApp endSheet:m_panel]; @@ -190,19 +219,6 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; [m_panel close]; } -- (BOOL)isHiddenFileAtURL:(NSURL *)url -{ - BOOL hidden = NO; - if (url) { - CFBooleanRef isHiddenProperty; - if (CFURLCopyResourcePropertyForKey((__bridge CFURLRef)url, kCFURLIsHiddenKey, &isHiddenProperty, nullptr)) { - hidden = CFBooleanGetValue(isHiddenProperty); - CFRelease(isHiddenProperty); - } - } - return hidden; -} - - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { Q_UNUSED(sender); @@ -211,64 +227,140 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; if (!filename.length) return NO; - // Always accept directories regardless of their names (unless it is a bundle): - NSFileManager *fm = NSFileManager.defaultManager; - NSDictionary *fileAttrs = [fm attributesOfItemAtPath:filename error:nil]; - if (!fileAttrs) - return NO; // Error accessing the file means 'no'. - NSString *fileType = fileAttrs.fileType; - bool isDir = [fileType isEqualToString:NSFileTypeDirectory]; - if (isDir) { - if (!m_panel.treatsFilePackagesAsDirectories) { - if ([NSWorkspace.sharedWorkspace isFilePackageAtPath:filename] == NO) - return YES; - } + const QFileInfo fileInfo(QString::fromNSString(filename)); + + // Always accept directories regardless of their names. + // This also includes symlinks and aliases to directories. + if (fileInfo.isDir()) { + // Unless it's a bundle, and we should treat bundles as files. + // FIXME: We'd like to use QFileInfo::isBundle() here, but the + // detection in QFileInfo goes deeper than NSWorkspace does + // (likely a bug), and as a result causes TCC permission + // dialogs to pop up when used. + bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories; + if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename])) + return YES; } - // Treat symbolic links and aliases to directories like directories - QFileInfo fileInfo(QString::fromNSString(filename)); - if (fileInfo.isSymLink() && QFileInfo(fileInfo.symLinkTarget()).isDir()) - return YES; - - QString qtFileName = fileInfo.fileName(); - // No filter means accept everything - bool nameMatches = m_selectedNameFilter->isEmpty(); - // Check if the current file name filter accepts the file: - for (int i = 0; !nameMatches && i < m_selectedNameFilter->size(); ++i) { - if (QDir::match(m_selectedNameFilter->at(i), qtFileName)) - nameMatches = true; - } - if (!nameMatches) + if (![self fileInfoMatchesCurrentNameFilter:fileInfo]) return NO; QDir::Filters filter = m_options->filter(); - if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && isDir) - || (!(filter & QDir::Files) && [fileType isEqualToString:NSFileTypeRegular]) - || ((filter & QDir::NoSymLinks) && [fileType isEqualToString:NSFileTypeSymbolicLink])) + if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && fileInfo.isDir()) + || (!(filter & QDir::Files) && (fileInfo.isFile() && !fileInfo.isSymLink())) + || ((filter & QDir::NoSymLinks) && fileInfo.isSymLink())) return NO; bool filterPermissions = ((filter & QDir::PermissionMask) && (filter & QDir::PermissionMask) != QDir::PermissionMask); if (filterPermissions) { - if ((!(filter & QDir::Readable) && [fm isReadableFileAtPath:filename]) - || (!(filter & QDir::Writable) && [fm isWritableFileAtPath:filename]) - || (!(filter & QDir::Executable) && [fm isExecutableFileAtPath:filename])) + if ((!(filter & QDir::Readable) && fileInfo.isReadable()) + || (!(filter & QDir::Writable) && fileInfo.isWritable()) + || (!(filter & QDir::Executable) && fileInfo.isExecutable())) return NO; } - if (!(filter & QDir::Hidden) - && (qtFileName.startsWith(u'.') || [self isHiddenFileAtURL:url])) + + // We control the visibility of hidden files via the showsHiddenFiles + // property on the panel, based on QDir::Hidden being set. But the user + // can also toggle this via the Command+Shift+. keyboard shortcut, + // in which case they have explicitly requested to show hidden files, + // and we should enable them even if QDir::Hidden was not set. In + // effect, we don't need to filter on QDir::Hidden here. + + return YES; +} + +- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError * _Nullable *)outError +{ + Q_ASSERT(sender == m_panel); + + if (!m_panel.allowedFileTypes && !m_selectedNameFilter.isEmpty()) { + // The save panel hasn't done filtering on our behalf, + // either because we couldn't represent the filter via + // allowedFileTypes, or we opted out due to a multi part + // extension, so do the filtering/validation ourselves. + QFileInfo fileInfo(QString::fromNSString(url.path).normalized(QString::NormalizationForm_C)); + + if ([self fileInfoMatchesCurrentNameFilter:fileInfo]) + return YES; + + if (fileInfo.suffix().isEmpty()) { + // The filter requires a file name with an extension. + // We're going to add a default file name in selectedFiles, + // to match the native behavior. Check now that we can + // overwrite the file, if is already exists. + fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo]; + + if (!fileInfo.exists() || m_options->testOption(QFileDialogOptions::DontConfirmOverwrite)) + return YES; + + QMacAutoReleasePool pool; + auto *alert = [[NSAlert new] autorelease]; + alert.alertStyle = NSAlertStyleCritical; + + alert.messageText = [NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"\\U201c%@\\U201d already exists. Do you want to replace it?"), + fileInfo.fileName().toNSString()]; + alert.informativeText = [NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"A file or folder with the same name already exists in the folder %@. " + "Replacing it will overwrite its current contents."), + fileInfo.absoluteDir().dirName().toNSString()]; + + auto *replaceButton = [alert addButtonWithTitle:qt_mac_AppKitString(@"SavePanel", @"Replace")]; + replaceButton.hasDestructiveAction = YES; + replaceButton.tag = 1337; + [alert addButtonWithTitle:qt_mac_AppKitString(@"Common", @"Cancel")]; + + [alert beginSheetModalForWindow:m_panel + completionHandler:^(NSModalResponse returnCode) { + [NSApp stopModalWithCode:returnCode]; + }]; + return [NSApp runModalForWindow:alert.window] == replaceButton.tag; + } else { + QFileInfo firstFilter(m_selectedNameFilter.first()); + auto *domain = qGuiApp->organizationDomain().toNSString(); + *outError = [NSError errorWithDomain:domain code:0 userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"You cannot save this document with extension \\U201c.%1$@\\U201d at the end " + "of the name. The required extension is \\U201c.%2$@\\U201d."), + fileInfo.completeSuffix().toNSString(), firstFilter.completeSuffix().toNSString()] + }]; return NO; + } + } return YES; } +- (QFileInfo)applyDefaultSuffixFromCurrentNameFilter:(const QFileInfo &)fileInfo +{ + QFileInfo filterInfo(m_selectedNameFilter.first()); + return QFileInfo(fileInfo.absolutePath(), + fileInfo.baseName() + '.' + filterInfo.completeSuffix()); +} + +- (bool)fileInfoMatchesCurrentNameFilter:(const QFileInfo &)fileInfo +{ + // No filter means accept everything + if (m_selectedNameFilter.isEmpty()) + return true; + + // Check if the current file name filter accepts the file + for (const auto &filter : m_selectedNameFilter) { + if (QDir::match(filter, fileInfo.fileName())) + return true; + } + + return false; +} + - (void)setNameFilters:(const QStringList &)filters hideDetails:(BOOL)hideDetails { [m_popupButton removeAllItems]; - *m_nameFilterDropDownList = filters; + m_nameFilterDropDownList = filters; if (filters.size() > 0){ for (int i = 0; i < filters.size(); ++i) { - QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i); + const QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i); [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@""]; } [m_popupButton selectItemAtIndex:0]; @@ -284,8 +376,10 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; { // This m_delegate function is called when the _name_ filter changes. Q_UNUSED(sender); - QString selection = m_nameFilterDropDownList->value([m_popupButton indexOfSelectedItem]); - *m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; + if (!m_helper) + return; + const QString selection = m_nameFilterDropDownList.value([m_popupButton indexOfSelectedItem]); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; [m_panel validateVisibleColumns]; [self updateProperties]; @@ -304,18 +398,25 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; } return result; } else { - QList<QUrl> result; QString filename = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); - const QString defaultSuffix = m_options->defaultSuffix(); - const QFileInfo fileInfo(filename); + QFileInfo fileInfo(filename); + + if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) { + // We end up in this situation if we accept a file name without extension + // in panel:validateURL:error. If so, we match the behavior of the native + // save dialog and add the first of the accepted extension from the filter. + fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo]; + } // If neither the user or the NSSavePanel have provided a suffix, use // the default suffix (if it exists). - if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) - filename.append('.').append(defaultSuffix); + const QString defaultSuffix = m_options->defaultSuffix(); + if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) { + fileInfo.setFile(fileInfo.absolutePath(), + fileInfo.baseName() + '.' + defaultSuffix); + } - result << QUrl::fromLocalFile(filename); - return result; + return { QUrl::fromLocalFile(fileInfo.filePath()) }; } } @@ -347,19 +448,25 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; m_panel.allowedFileTypes = [self computeAllowedFileTypes]; - // Explicitly show extensions if we detect a filter - // that has a multi-part extension. This prevents - // confusing situations where the user clicks e.g. - // 'foo.tar.gz' and 'foo.tar' is populated in the - // file name box, but when then clicking save macOS - // will warn that the file needs to end in .gz, - // due to thinking the user tried to save the file - // as a 'tar' file instead. Unfortunately this - // property can only be set before the panel is - // shown, so it will not have any effect when - // switching filters in an already opened dialog. - if (m_panel.allowedFileTypes.count > 2) - m_panel.extensionHidden = NO; + // Setting allowedFileTypes to nil is not enough to reset any + // automatically added extension based on a previous filter. + // This is problematic because extensions can in some cases + // be hidden from the user, resulting in confusion when the + // resulting file name doesn't match the current empty filter. + // We work around this by temporarily resetting the allowed + // content type to one without an extension, which forces + // the save panel to update and remove the extension. + const bool nameFieldHasExtension = m_panel.nameFieldStringValue.pathExtension.length > 0; + if (!m_panel.allowedFileTypes && !nameFieldHasExtension && !openpanel_cast(m_panel)) { + if (!UTTypeDirectory.preferredFilenameExtension) { + m_panel.allowedContentTypes = @[ UTTypeDirectory ]; + m_panel.allowedFileTypes = nil; + } else { + qWarning() << "UTTypeDirectory unexpectedly reported an extension"; + } + } + + m_panel.showsHiddenFiles = m_options->filter().testFlag(QDir::Hidden); if (m_panel.visible) [m_panel validateVisibleColumns]; @@ -368,10 +475,22 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (void)panelSelectionDidChange:(id)sender { Q_UNUSED(sender); + + if (!m_helper) + return; + + // Save panels only allow you to select directories, which + // means currentChanged will only be emitted when selecting + // a directory, and if so, with the latest chosen file name, + // which is confusing and inconsistent. We choose to bail + // out entirely for save panels, to give consistent behavior. + if (!openpanel_cast(m_panel)) + return; + if (m_panel.visible) { - QString selection = QString::fromNSString(m_panel.URL.path); - if (selection != *m_currentSelection) { - *m_currentSelection = selection; + const QString selection = QString::fromNSString(m_panel.URL.path); + if (selection != m_currentSelection) { + m_currentSelection = selection; emit m_helper->currentChanged(QUrl::fromLocalFile(selection)); } } @@ -381,14 +500,10 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; { Q_UNUSED(sender); - if (!(path && path.length) || [path isEqualToString:m_currentDirectory]) + if (!m_helper) return; - [m_currentDirectory release]; - m_currentDirectory = [path retain]; - - // ### fixme: priv->setLastVisitedDirectory(newDir); - emit m_helper->directoryEntered(QUrl::fromLocalFile(QString::fromNSString(m_currentDirectory))); + m_helper->panelDirectoryDidChange(path); } /* @@ -396,11 +511,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; for the current name filter, and updates the save panel. If a filter do not conform to the format *.xyz or * or *.*, - all files types are allowed. - - Extensions with more than one part (e.g. "tar.gz") are - reduced to their final part, as NSSavePanel does not deal - well with multi-part extensions. + or contains an extensions with more than one part (e.g. "tar.gz") + we treat that as allowing all file types, and do our own + validation in panel:validateURL:error. */ - (NSArray<NSString*>*)computeAllowedFileTypes { @@ -408,7 +521,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; return nil; // panel:shouldEnableURL: does the file filtering for NSOpenPanel QStringList fileTypes; - for (const QString &filter : *m_selectedNameFilter) { + for (const QString &filter : std::as_const(m_selectedNameFilter)) { if (!filter.startsWith("*."_L1)) continue; @@ -419,6 +532,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; continue; auto extensions = filter.split('.', Qt::SkipEmptyParts); + if (extensions.count() > 2) + return nil; + fileTypes += extensions.last(); } @@ -455,10 +571,10 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; m_popupButton.target = self; m_popupButton.action = @selector(filterChanged:); - if (m_nameFilterDropDownList->size() > 0) { + if (!m_nameFilterDropDownList.isEmpty()) { int filterToUse = -1; - for (int i = 0; i < m_nameFilterDropDownList->size(); ++i) { - QString currentFilter = m_nameFilterDropDownList->at(i); + for (int i = 0; i < m_nameFilterDropDownList.size(); ++i) { + const QString currentFilter = m_nameFilterDropDownList.at(i); if (selectedFilter == currentFilter || (filterToUse == -1 && currentFilter.startsWith(selectedFilter))) filterToUse = i; @@ -472,9 +588,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (QStringList) findStrippedFilterWithVisualFilterName:(QString)name { - for (int i = 0; i < m_nameFilterDropDownList->size(); ++i) { - if (m_nameFilterDropDownList->at(i).startsWith(name)) - return QPlatformFileDialogHelper::cleanFilterList(m_nameFilterDropDownList->at(i)); + for (const QString ¤tFilter : std::as_const(m_nameFilterDropDownList)) { + if (currentFilter.startsWith(name)) + return QPlatformFileDialogHelper::cleanFilterList(currentFilter); } return QStringList(); } @@ -515,21 +631,32 @@ void QCocoaFileDialogHelper::panelClosed(NSInteger result) void QCocoaFileDialogHelper::setDirectory(const QUrl &directory) { + m_directory = directory; + if (m_delegate) m_delegate->m_panel.directoryURL = [NSURL fileURLWithPath:directory.toLocalFile().toNSString()]; - else - m_directory = directory; } QUrl QCocoaFileDialogHelper::directory() const { - if (m_delegate) { - QString path = QString::fromNSString(m_delegate->m_panel.directoryURL.path).normalized(QString::NormalizationForm_C); - return QUrl::fromLocalFile(path); - } return m_directory; } +void QCocoaFileDialogHelper::panelDirectoryDidChange(NSString *path) +{ + if (!path || [path isEqual:NSNull.null] || !path.length) + return; + + const auto oldDirectory = m_directory; + m_directory = QUrl::fromLocalFile( + QString::fromNSString(path).normalized(QString::NormalizationForm_C)); + + if (m_directory != oldDirectory) { + // FIXME: Plumb old directory back to QFileDialog's lastVisitedDir? + emit directoryEntered(m_directory); + } +} + void QCocoaFileDialogHelper::selectFile(const QUrl &filename) { QString filePath = filename.toLocalFile(); diff --git a/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm index ac5d784b79..5cdf6bf02c 100644 --- a/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm @@ -166,7 +166,17 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSFontPanelDelegate); { mDialogIsExecuting = false; mResultSet = false; - [mFontPanel makeKeyAndOrderFront:mFontPanel]; + // Make this an asynchronous call, so the panel is made key only + // in the next event loop run. This is to make sure that by + // the time the modal loop is run in runModalForWindow below, + // which internally also sets the panel to key window, + // the panel is not yet key, and the NSApp still has the right + // reference to the _previousKeyWindow. Otherwise both NSApp.key + // and NSApp._previousKeyWindow would wrongly point to the panel, + // loosing any reference to the window that was key before. + dispatch_async(dispatch_get_main_queue(), ^{ + [mFontPanel makeKeyAndOrderFront:mFontPanel]; + }); } - (BOOL)runApplicationModalPanel diff --git a/src/plugins/platforms/cocoa/qcocoaglcontext.mm b/src/plugins/platforms/cocoa/qcocoaglcontext.mm index 807f502275..a65311175f 100644 --- a/src/plugins/platforms/cocoa/qcocoaglcontext.mm +++ b/src/plugins/platforms/cocoa/qcocoaglcontext.mm @@ -8,6 +8,8 @@ #include "qcocoahelpers.h" #include "qcocoascreen.h" +#include <QtCore/private/qcore_mac_p.h> + #include <qdebug.h> #include <dlfcn.h> @@ -415,7 +417,7 @@ bool QCocoaGLContext::setDrawable(QPlatformSurface *surface) // NSOpenGLContext is not re-entrant. Even when using separate contexts per thread, // view, and window, calls into the API will still deadlock. For more information // see https://openradar.appspot.com/37064579 -static QMutex s_reentrancyMutex; +Q_CONSTINIT static QMutex s_reentrancyMutex; void QCocoaGLContext::update() { diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.h b/src/plugins/platforms/cocoa/qcocoahelpers.h index 869b245911..c6862a9e65 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.h +++ b/src/plugins/platforms/cocoa/qcocoahelpers.h @@ -41,6 +41,8 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen) Q_DECLARE_LOGGING_CATEGORY(lcQpaApplication) Q_DECLARE_LOGGING_CATEGORY(lcQpaClipboard) Q_DECLARE_LOGGING_CATEGORY(lcInputDevices) +Q_DECLARE_LOGGING_CATEGORY(lcQpaDialogs) +Q_DECLARE_LOGGING_CATEGORY(lcQpaMenus) class QPixmap; class QString; @@ -54,16 +56,6 @@ NSDragOperation qt_mac_mapDropActions(Qt::DropActions actions); Qt::DropAction qt_mac_mapNSDragOperation(NSDragOperation nsActions); Qt::DropActions qt_mac_mapNSDragOperations(NSDragOperation nsActions); -template <typename T> -typename std::enable_if<std::is_pointer<T>::value, T>::type -qt_objc_cast(id object) -{ - if ([object isKindOfClass:[typename std::remove_pointer<T>::type class]]) - return static_cast<T>(object); - - return nil; -} - QT_MANGLE_NAMESPACE(QNSView) *qnsview_cast(NSView *view); // Misc @@ -86,6 +78,9 @@ Qt::MouseButtons currentlyPressedMouseButtons(); // accelerators. QString qt_mac_removeAmpersandEscapes(QString s); +// Similar to __NXKitString for localized AppKit strings +NSString *qt_mac_AppKitString(NSString *table, NSString *key); + enum { QtCocoaEventSubTypeWakeup = SHRT_MAX, QtCocoaEventSubTypePostMessage = SHRT_MAX-1 @@ -313,7 +308,7 @@ QSendSuperHelper<Args...> qt_objcDynamicSuperHelper(id receiver, SEL selector, A // ------------------------------------------------------------------------- -struct InputMethodQueryResult : public QHash<Qt::InputMethodQuery, QVariant> +struct InputMethodQueryResult : public QHash<int, QVariant> { operator bool() { return !isEmpty(); } }; diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.mm b/src/plugins/platforms/cocoa/qcocoahelpers.mm index 0a6a2a7d04..1eba88d5e3 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.mm +++ b/src/plugins/platforms/cocoa/qcocoahelpers.mm @@ -28,6 +28,8 @@ Q_LOGGING_CATEGORY(lcQpaScreen, "qt.qpa.screen", QtCriticalMsg); Q_LOGGING_CATEGORY(lcQpaApplication, "qt.qpa.application"); Q_LOGGING_CATEGORY(lcQpaClipboard, "qt.qpa.clipboard") Q_LOGGING_CATEGORY(lcInputDevices, "qt.qpa.input.devices") +Q_LOGGING_CATEGORY(lcQpaDialogs, "qt.qpa.dialogs") +Q_LOGGING_CATEGORY(lcQpaMenus, "qt.qpa.menus") // // Conversion Functions @@ -334,6 +336,15 @@ QString qt_mac_removeAmpersandEscapes(QString s) return QPlatformTheme::removeMnemonics(s).trimmed(); } +NSString *qt_mac_AppKitString(NSString *table, NSString *key) +{ + static const NSBundle *appKit = [NSBundle bundleForClass:NSApplication.class]; + if (!appKit) + return key; + + return [appKit localizedStringForKey:key value:nil table:table]; +} + QT_END_NAMESPACE /*! \internal diff --git a/src/plugins/platforms/cocoa/qcocoainputcontext.mm b/src/plugins/platforms/cocoa/qcocoainputcontext.mm index b242cd69c6..70461376e2 100644 --- a/src/plugins/platforms/cocoa/qcocoainputcontext.mm +++ b/src/plugins/platforms/cocoa/qcocoainputcontext.mm @@ -150,11 +150,18 @@ void QCocoaInputContext::updateLocale() QString language = QString::fromNSString(languages.firstObject); QLocale locale(language); - if (m_locale != locale) { + + bool localeUpdated = m_locale != locale; + static bool firstUpdate = true; + + m_locale = locale; + + if (localeUpdated && !firstUpdate) { qCDebug(lcQpaInputMethods) << "Reporting new locale" << locale; - m_locale = locale; emitLocaleChanged(); } + + firstUpdate = false; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.h b/src/plugins/platforms/cocoa/qcocoaintegration.h index c6d73e3c06..664700cf51 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.h +++ b/src/plugins/platforms/cocoa/qcocoaintegration.h @@ -20,7 +20,9 @@ #include <QtCore/QScopedPointer> #include <qpa/qplatformintegration.h> #include <QtGui/private/qcoretextfontdatabase_p.h> -#include <QtGui/private/qopenglcontext_p.h> +#ifndef QT_NO_OPENGL +# include <QtGui/private/qopenglcontext_p.h> +#endif #include <QtGui/private/qapplekeymapper_p.h> Q_FORWARD_DECLARE_OBJC_CLASS(NSToolbar); @@ -82,14 +84,10 @@ public: QCocoaServices *services() const override; QVariant styleHint(StyleHint hint) const override; - Qt::KeyboardModifiers queryKeyboardModifiers() const override; - QList<int> possibleKeys(const QKeyEvent *event) const override; - - void setToolbar(QWindow *window, NSToolbar *toolbar); - NSToolbar *toolbar(QWindow *window) const; - void clearToolbars(); + QPlatformKeyMapper *keyMapper() const override; void setApplicationIcon(const QIcon &icon) const override; + void setApplicationBadge(qint64 number) override; void beep() const override; void quit() const override; @@ -119,7 +117,6 @@ private: #if QT_CONFIG(vulkan) mutable QCocoaVulkanInstance *mCocoaVulkanInstance = nullptr; #endif - QHash<QWindow *, NSToolbar *> mToolbars; QCocoaWindowManager m_windowManager; }; diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.mm b/src/plugins/platforms/cocoa/qcocoaintegration.mm index 9911263c51..2ce39ff897 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.mm +++ b/src/plugins/platforms/cocoa/qcocoaintegration.mm @@ -30,12 +30,19 @@ #include <QtCore/qcoreapplication.h> #include <QtGui/qpointingdevice.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/private/qcoregraphics_p.h> -#include <QtGui/private/qopenglcontext_p.h> +#include <QtGui/private/qmacmimeregistry_p.h> +#ifndef QT_NO_OPENGL +# include <QtGui/private/qopenglcontext_p.h> +#endif #include <QtGui/private/qrhibackingstore_p.h> #include <QtGui/private/qfontengine_coretext_p.h> #include <IOKit/graphics/IOGraphicsLib.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> + +#include <inttypes.h> static void initResources() { @@ -118,9 +125,9 @@ QCocoaIntegration::QCocoaIntegration(const QStringList ¶mList) #endif mFontDb.reset(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>); - QString icStr = QPlatformInputContextFactory::requested(); - icStr.isNull() ? mInputContext.reset(new QCocoaInputContext) - : mInputContext.reset(QPlatformInputContextFactory::create(icStr)); + auto icStrs = QPlatformInputContextFactory::requested(); + icStrs.isEmpty() ? mInputContext.reset(new QCocoaInputContext) + : mInputContext.reset(QPlatformInputContextFactory::create(icStrs)); initResources(); QMacAutoReleasePool pool; @@ -135,16 +142,6 @@ QCocoaIntegration::QCocoaIntegration(const QStringList ¶mList) // wants to be foreground applications so change the process type. (But // see the function implementation for exceptions.) qt_mac_transformProccessToForegroundApplication(); - - // Move the application window to front to make it take focus, also when launching - // from the terminal. On 10.12+ this call has been moved to applicationDidFinishLauching - // to work around issues with loss of focus at startup. - if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSSierra) { - // Ignoring other apps is necessary (we must ignore the terminal), but makes - // Qt apps play slightly less nice with other apps when lanching from Finder - // (See the activateIgnoringOtherApps docs.) - [cocoaApplication activateIgnoringOtherApps : YES]; - } } // Qt 4 also does not set the application delegate, so that behavior @@ -163,7 +160,7 @@ QCocoaIntegration::QCocoaIntegration(const QStringList ¶mList) QCocoaScreen::initializeScreens(); - QMacInternalPasteboardMime::initializeMimeTypes(); + QMacMimeRegistry::initializeMimeTypes(); QCocoaMimeTypes::initializeMimeTypes(); QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); QWindowSystemInterface::registerInputDevice(new QInputDevice(QString("keyboard"), 0, @@ -188,17 +185,18 @@ QCocoaIntegration::~QCocoaIntegration() [[NSApplication sharedApplication] setDelegate:nil]; } + // Stop global mouse event and app activation monitoring + QCocoaWindow::removePopupMonitor(); + #ifndef QT_NO_CLIPBOARD // Delete the clipboard integration and destroy mime type converters. // Deleting the clipboard integration flushes promised pastes using // the mime converters - the ordering here is important. delete mCocoaClipboard; - QMacInternalPasteboardMime::destroyMimeTypes(); + QMacMimeRegistry::destroyMimeTypes(); #endif QCocoaScreen::cleanupScreens(); - - clearToolbars(); } QCocoaIntegration *QCocoaIntegration::instance() @@ -238,6 +236,7 @@ bool QCocoaIntegration::hasCapability(QPlatformIntegration::Capability cap) cons case RasterGLSurface: case ApplicationState: case ApplicationIcon: + case BackingStoreStaticContents: return true; default: return QPlatformIntegration::hasCapability(cap); @@ -305,6 +304,18 @@ QPlatformBackingStore *QCocoaIntegration::createPlatformBackingStore(QWindow *wi return new QCALayerBackingStore(window); case QSurface::MetalSurface: case QSurface::OpenGLSurface: + case QSurface::VulkanSurface: + // If the window is a widget window, we know that the QWidgetRepaintManager + // will explicitly use rhiFlush() for the window owning the backingstore, + // and any child window with the same surface format. This means we can + // safely return a QCALayerBackingStore here, to ensure that any plain + // flush() for child windows that don't have a matching surface format + // will still work, by setting the layer's contents property. + if (window->inherits("QWidgetWindow")) + return new QCALayerBackingStore(window); + + // Otherwise we return a QRhiBackingStore, that implements flush() in + // terms of rhiFlush(). return new QRhiBackingStore(window); default: return nullptr; @@ -395,38 +406,9 @@ QVariant QCocoaIntegration::styleHint(StyleHint hint) const return QPlatformIntegration::styleHint(hint); } -Qt::KeyboardModifiers QCocoaIntegration::queryKeyboardModifiers() const +QPlatformKeyMapper *QCocoaIntegration::keyMapper() const { - return QAppleKeyMapper::queryKeyboardModifiers(); -} - -QList<int> QCocoaIntegration::possibleKeys(const QKeyEvent *event) const -{ - return mKeyboardMapper->possibleKeys(event); -} - -void QCocoaIntegration::setToolbar(QWindow *window, NSToolbar *toolbar) -{ - if (NSToolbar *prevToolbar = mToolbars.value(window)) - [prevToolbar release]; - - [toolbar retain]; - mToolbars.insert(window, toolbar); -} - -NSToolbar *QCocoaIntegration::toolbar(QWindow *window) const -{ - return mToolbars.value(window); -} - -void QCocoaIntegration::clearToolbars() -{ - QHash<QWindow *, NSToolbar *>::const_iterator it = mToolbars.constBegin(); - while (it != mToolbars.constEnd()) { - [it.value() release]; - ++it; - } - mToolbars.clear(); + return mKeyboardMapper.data(); } void QCocoaIntegration::setApplicationIcon(const QIcon &icon) const @@ -436,6 +418,11 @@ void QCocoaIntegration::setApplicationIcon(const QIcon &icon) const NSApp.applicationIconImage = [NSImage imageFromQIcon:icon withSize:fallbackSize]; } +void QCocoaIntegration::setApplicationBadge(qint64 number) +{ + NSApp.dockTile.badgeLabel = number ? [NSString stringWithFormat:@"%" PRId64, number] : nil; +} + void QCocoaIntegration::beep() const { NSBeep(); @@ -454,8 +441,8 @@ void QCocoaIntegration::focusWindowChanged(QWindow *focusWindow) return; static bool hasDefaultApplicationIcon = [](){ - NSImage *genericApplicationIcon = [[NSWorkspace sharedWorkspace] - iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)]; + NSImage *genericApplicationIcon = [NSWorkspace.sharedWorkspace + iconForContentType:UTTypeApplicationBundle]; NSImage *applicationIcon = [NSImage imageNamed:NSImageNameApplicationIcon]; NSRect rect = NSMakeRect(0, 0, 32, 32); diff --git a/src/plugins/platforms/cocoa/qcocoamenu.mm b/src/plugins/platforms/cocoa/qcocoamenu.mm index 8a3d259b3e..fa88a19d45 100644 --- a/src/plugins/platforms/cocoa/qcocoamenu.mm +++ b/src/plugins/platforms/cocoa/qcocoamenu.mm @@ -18,6 +18,9 @@ #include "qcocoascreen.h" #include "qcocoaapplicationdelegate.h" +#include <QtCore/private/qcore_mac_p.h> +#include <QtCore/qpointer.h> + QT_BEGIN_NAMESPACE QCocoaMenu::QCocoaMenu() : @@ -35,11 +38,13 @@ QCocoaMenu::QCocoaMenu() : QCocoaMenu::~QCocoaMenu() { - for (auto *item : qAsConst(m_menuItems)) { + for (auto *item : std::as_const(m_menuItems)) { if (item->menuParent() == this) item->setMenuParent(nullptr); } + if (isOpen()) + dismiss(); [m_nativeMenu release]; } @@ -58,7 +63,7 @@ void QCocoaMenu::setMinimumWidth(int width) void QCocoaMenu::setFont(const QFont &font) { if (font.resolveMask()) { - NSFont *customMenuFont = [NSFont fontWithName:font.families().first().toNSString() + NSFont *customMenuFont = [NSFont fontWithName:font.families().constFirst().toNSString() size:font.pointSize()]; m_nativeMenu.font = customMenuFont; } @@ -86,7 +91,7 @@ void QCocoaMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem * int index = m_menuItems.indexOf(beforeItem); // if a before item is supplied, it should be in the menu if (index < 0) { - qWarning("Before menu item not found"); + qCWarning(lcQpaMenus) << beforeItem << "not in" << m_menuItems; return; } m_menuItems.insert(index, cocoaItem); @@ -124,13 +129,13 @@ void QCocoaMenu::insertNative(QCocoaMenuItem *item, QCocoaMenuItem *beforeItem) } if (nativeItem.menu) { - qWarning() << "Menu item" << item->text() << "already in menu" << QString::fromNSString(nativeItem.menu.title); + qCWarning(lcQpaMenus) << "Menu item" << item->text() << "already in menu" << QString::fromNSString(nativeItem.menu.title); return; } if (beforeItem) { if (beforeItem->isMerged()) { - qWarning("No non-merged before menu item found"); + qCWarning(lcQpaMenus, "No non-merged before menu item found"); return; } const NSInteger nativeIndex = [m_nativeMenu indexOfItem:beforeItem->nsItem()]; @@ -166,7 +171,7 @@ void QCocoaMenu::removeMenuItem(QPlatformMenuItem *menuItem) QMacAutoReleasePool pool; QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem); if (!m_menuItems.contains(cocoaItem)) { - qWarning("Menu does not contain the item to be removed"); + qCWarning(lcQpaMenus) << m_menuItems << "does not contain" << cocoaItem; return; } @@ -179,7 +184,7 @@ void QCocoaMenu::removeMenuItem(QPlatformMenuItem *menuItem) m_menuItems.removeOne(cocoaItem); if (!cocoaItem->isMerged()) { if (m_nativeMenu != cocoaItem->nsItem().menu) { - qWarning("Item to remove does not belong to this menu"); + qCWarning(lcQpaMenus) << cocoaItem << "does not belong to" << m_nativeMenu; return; } [m_nativeMenu removeItem:cocoaItem->nsItem()]; @@ -219,7 +224,7 @@ void QCocoaMenu::syncMenuItem_helper(QPlatformMenuItem *menuItem, bool menubarUp QMacAutoReleasePool pool; QCocoaMenuItem *cocoaItem = static_cast<QCocoaMenuItem *>(menuItem); if (!m_menuItems.contains(cocoaItem)) { - qWarning("Item does not belong to this menu"); + qCWarning(lcQpaMenus) << cocoaItem << "does not belong to" << this; return; } @@ -284,7 +289,7 @@ void QCocoaMenu::syncSeparatorsCollapsible(bool enable) if (lastVisibleItem && lastVisibleItem.separatorItem) lastVisibleItem.hidden = YES; } else { - for (auto *item : qAsConst(m_menuItems)) { + for (auto *item : std::as_const(m_menuItems)) { if (!item->isSeparator()) continue; @@ -318,8 +323,12 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, { QMacAutoReleasePool pool; + QPointer<QCocoaMenu> guard = this; + QPoint pos = QPoint(targetRect.left(), targetRect.top() + targetRect.height()); - QCocoaWindow *cocoaWindow = parentWindow ? static_cast<QCocoaWindow *>(parentWindow->handle()) : nullptr; + // If the app quits while the menu is open (e.g. through a timer that starts before the menu was opened), + // then the window will have been destroyed before this function finishes executing. Account for that with QPointer. + QPointer<QCocoaWindow> cocoaWindow = parentWindow ? static_cast<QCocoaWindow *>(parentWindow->handle()) : nullptr; NSView *view = cocoaWindow ? cocoaWindow->view() : nil; NSMenuItem *nsItem = item ? ((QCocoaMenuItem *)item)->nsItem() : nil; @@ -402,6 +411,11 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, } } + if (!guard) { + menuParentGuard.dismiss(); + return; + } + // The calls above block, and also swallow any mouse release event, // so we need to clear any mouse button that triggered the menu popup. if (cocoaWindow && !cocoaWindow->isForeignWindow()) @@ -423,7 +437,7 @@ QPlatformMenuItem *QCocoaMenu::menuItemAt(int position) const QPlatformMenuItem *QCocoaMenu::menuItemForTag(quintptr tag) const { - for (auto *item : qAsConst(m_menuItems)) { + for (auto *item : std::as_const(m_menuItems)) { if (item->tag() == tag) return item; } @@ -439,7 +453,7 @@ QList<QCocoaMenuItem *> QCocoaMenu::items() const QList<QCocoaMenuItem *> QCocoaMenu::merged() const { QList<QCocoaMenuItem *> result; - for (auto *item : qAsConst(m_menuItems)) { + for (auto *item : std::as_const(m_menuItems)) { if (item->menu()) { // recurse into submenus result.append(item->menu()->merged()); continue; @@ -460,7 +474,7 @@ void QCocoaMenu::propagateEnabledState(bool enabled) if (!m_enabled && enabled) // Some ancestor was enabled, but this menu is not return; - for (auto *item : qAsConst(m_menuItems)) { + for (auto *item : std::as_const(m_menuItems)) { if (QCocoaMenu *menu = item->menu()) menu->propagateEnabledState(enabled); else @@ -481,6 +495,10 @@ void QCocoaMenu::setAttachedItem(NSMenuItem *item) if (m_attachedItem) m_attachedItem.submenu = m_nativeMenu; + // NSMenuItems with a submenu and submenuAction: as the item's action + // will not take part in NSMenuValidation, so explicitly enable/disable + // the item here. See also QCocoaMenuItem::resolveTargetAction() + m_attachedItem.enabled = m_attachedItem.hasSubmenu; } NSMenuItem *QCocoaMenu::attachedItem() const diff --git a/src/plugins/platforms/cocoa/qcocoamenubar.h b/src/plugins/platforms/cocoa/qcocoamenubar.h index b30f8569ab..785de9c0f6 100644 --- a/src/plugins/platforms/cocoa/qcocoamenubar.h +++ b/src/plugins/platforms/cocoa/qcocoamenubar.h @@ -9,6 +9,8 @@ #include <qpa/qplatformmenu.h> #include "qcocoamenu.h" +#include <QtCore/qpointer.h> + QT_BEGIN_NAMESPACE class QCocoaWindow; @@ -45,14 +47,12 @@ private: bool needsImmediateUpdate(); bool shouldDisable(QCocoaWindow *active) const; - void insertDefaultEditItems(QCocoaMenu *menu); NSMenuItem *nativeItemForMenu(QCocoaMenu *menu) const; QList<QPointer<QCocoaMenu> > m_menus; NSMenu *m_nativeMenu; QPointer<QCocoaWindow> m_window; - QList<QPointer<QCocoaMenuItem>> m_defaultEditMenuItems; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoamenubar.mm b/src/plugins/platforms/cocoa/qcocoamenubar.mm index 23aae466df..2493d90724 100644 --- a/src/plugins/platforms/cocoa/qcocoamenubar.mm +++ b/src/plugins/platforms/cocoa/qcocoamenubar.mm @@ -9,10 +9,14 @@ #include "qcocoamenuloader.h" #include "qcocoaapplication.h" // for custom application category #include "qcocoaapplicationdelegate.h" +#include "qcocoahelpers.h" #include <QtGui/QGuiApplication> #include <QtCore/QDebug> +#include <QtCore/private/qcore_mac_p.h> +#include <QtGui/private/qguiapplication_p.h> + QT_BEGIN_NAMESPACE static QList<QCocoaMenuBar*> static_menubars; @@ -21,18 +25,19 @@ QCocoaMenuBar::QCocoaMenuBar() { static_menubars.append(this); + // clicks into the menu bar should close all popup windows + static QMacNotificationObserver menuBarClickObserver(nil, NSMenuDidBeginTrackingNotification, ^{ + QGuiApplicationPrivate::instance()->closeAllPopups(); + }); + m_nativeMenu = [[NSMenu alloc] init]; -#ifdef QT_COCOA_ENABLE_MENU_DEBUG - qDebug() << "Construct QCocoaMenuBar" << this << m_nativeMenu; -#endif + qCDebug(lcQpaMenus) << "Constructed" << this << "with" << m_nativeMenu; } QCocoaMenuBar::~QCocoaMenuBar() { -#ifdef QT_COCOA_ENABLE_MENU_DEBUG - qDebug() << "~QCocoaMenuBar" << this; -#endif - for (auto menu : qAsConst(m_menus)) { + qCDebug(lcQpaMenus) << "Destructing" << this << "with" << m_nativeMenu; + for (auto menu : std::as_const(m_menus)) { if (!menu) continue; NSMenuItem *item = nativeItemForMenu(menu); @@ -85,17 +90,16 @@ void QCocoaMenuBar::insertMenu(QPlatformMenu *platformMenu, QPlatformMenu *befor { QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu); QCocoaMenu *beforeMenu = static_cast<QCocoaMenu *>(before); -#ifdef QT_COCOA_ENABLE_MENU_DEBUG - qDebug() << "QCocoaMenuBar" << this << "insertMenu" << menu << "before" << before; -#endif + + qCDebug(lcQpaMenus) << "Inserting" << menu << "before" << before << "into" << this; if (m_menus.contains(QPointer<QCocoaMenu>(menu))) { - qWarning("This menu already belongs to the menubar, remove it first"); + qCWarning(lcQpaMenus, "This menu already belongs to the menubar, remove it first"); return; } if (beforeMenu && !m_menus.contains(QPointer<QCocoaMenu>(beforeMenu))) { - qWarning("The before menu does not belong to the menubar"); + qCWarning(lcQpaMenus, "The before menu does not belong to the menubar"); return; } @@ -129,7 +133,7 @@ void QCocoaMenuBar::removeMenu(QPlatformMenu *platformMenu) { QCocoaMenu *menu = static_cast<QCocoaMenu *>(platformMenu); if (!m_menus.contains(menu)) { - qWarning("Trying to remove a menu that does not belong to the menubar"); + qCWarning(lcQpaMenus) << "Trying to remove" << menu << "that does not belong to" << this; return; } @@ -158,18 +162,6 @@ void QCocoaMenuBar::syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate) for (QCocoaMenuItem *item : cocoaMenu->items()) cocoaMenu->syncMenuItem_helper(item, menubarUpdate); - const QString captionNoAmpersand = QString::fromNSString(cocoaMenu->nsMenu().title) - .remove(u'&'); - if (captionNoAmpersand == QCoreApplication::translate("QCocoaMenu", "Edit")) { - // prevent recursion from QCocoaMenu::insertMenuItem - when the menu is visible - // it calls syncMenu again. QCocoaMenu::setVisible just sets the bool, which then - // gets evaluated in the code after this block. - const bool wasVisible = cocoaMenu->isVisible(); - cocoaMenu->setVisible(false); - insertDefaultEditItems(cocoaMenu); - cocoaMenu->setVisible(wasVisible); - } - BOOL shouldHide = YES; if (cocoaMenu->isVisible()) { // If the NSMenu has no visible items, or only separators, we should hide it @@ -182,10 +174,44 @@ void QCocoaMenuBar::syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate) } } - if (NSMenuItem *attachedItem = cocoaMenu->attachedItem()) { - // Non-nil attached item means the item's submenu is set - attachedItem.title = cocoaMenu->nsMenu().title; - attachedItem.hidden = shouldHide; + if (NSMenuItem *menuItem = cocoaMenu->attachedItem()) { + // Non-nil menu item means the item's sub menu is set + + NSString *menuTitle = cocoaMenu->nsMenu().title; + + // The NSMenu's title is what's visible to the user, and AppKit uses this + // for some of its heuristics of when to add special items to the menus, + // such as 'Enter Full Screen' in the View menu, the search bare in the + // Help menu, and the "Send App feedback to Apple" in the Help menu. + // This relies on the title matching AppKit's localized value from the + // MenuCommands table, which in turn depends on the preferredLocalizations + // of the AppKit bundle. We don't do any automatic translation of menu + // titles visible to the user, so this relies on the application developer + // having chosen translated titles that match AppKit's, and that the Qt + // preferred UI languages match AppKit's preferredLocalizations. + + // In the case of the Edit menu, AppKit uses the NSMenuItem's title + // for its heuristics of when to add the dictation and emoji entries, + // and this title is not visible to the user. But like above, the + // heuristics are based on the localized title of the menu, so we need + // to ensure the title matches AppKit's localization. + + // Unfortunately, the title we have at this point may have gone through + // Qt's i18n machinery already, via e.g. tr("Edit") in the application, + // in which case we don't know the context of the translation, and can't + // do a reverse lookup to go back to the untranslated title to pass to + // AppKit. As a workaround we translate the title via a our context, + // and document that the user needs to ensure their application matches + // this translation. + if ([menuTitle isEqual:@"Edit"] || [menuTitle isEqual:tr("Edit").toNSString()]) { + menuItem.title = qt_mac_AppKitString(@"InputManager", @"Edit"); + } else { + // The Edit menu is the only case we know of so far, but to be on + // the safe side we always sync the menu title. + menuItem.title = menuTitle; + } + + menuItem.hidden = shouldHide; } } @@ -199,9 +225,7 @@ NSMenuItem *QCocoaMenuBar::nativeItemForMenu(QCocoaMenu *menu) const void QCocoaMenuBar::handleReparent(QWindow *newParentWindow) { -#ifdef QT_COCOA_ENABLE_MENU_DEBUG - qDebug() << "QCocoaMenuBar" << this << "handleReparent" << newParentWindow; -#endif + qCDebug(lcQpaMenus) << "Reparenting" << this << "to" << newParentWindow; if (!m_window.isNull()) m_window->setMenubar(nullptr); @@ -233,7 +257,7 @@ QCocoaWindow *QCocoaMenuBar::findWindowForMenubar() QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar() { - for (auto *menubar : qAsConst(static_menubars)) { + for (auto *menubar : std::as_const(static_menubars)) { if (menubar->m_window.isNull()) return menubar; } @@ -269,12 +293,11 @@ void QCocoaMenuBar::updateMenuBarImmediately() if (!mb) return; -#ifdef QT_COCOA_ENABLE_MENU_DEBUG - qDebug() << "QCocoaMenuBar" << "updateMenuBarImmediately" << cw; -#endif + qCDebug(lcQpaMenus) << "Updating" << mb << "immediately for" << cw; + bool disableForModal = mb->shouldDisable(cw); - for (auto menu : qAsConst(mb->m_menus)) { + for (auto menu : std::as_const(mb->m_menus)) { if (!menu) continue; NSMenuItem *item = mb->nativeItemForMenu(menu); @@ -303,7 +326,21 @@ void QCocoaMenuBar::updateMenuBarImmediately() } [mergedItems release]; - [NSApp setMainMenu:mb->nsMenu()]; + + NSMenu *newMainMenu = mb->nsMenu(); + if (NSApp.mainMenu == newMainMenu) { + // NSApplication triggers _customizeMainMenu when the menu + // changes, which takes care of adding text input items to + // the edit menu e.g., but this doesn't happen if the menu + // is the same. In our case we might be re-using an existing + // menu, but the menu might have new sub menus that need to + // be customized. To ensure NSApplication does the right + // thing we reset the main menu first. + qCDebug(lcQpaMenus) << "Clearing main menu temporarily"; + NSApp.mainMenu = nil; + } + NSApp.mainMenu = newMainMenu; + insertWindowMenu(); [loader qtTranslateApplicationMenu]; } @@ -324,21 +361,39 @@ void QCocoaMenuBar::insertWindowMenu() winMenuItem.hidden = YES; winMenuItem.submenu = [[[NSMenu alloc] initWithTitle:@"QtWindowMenu"] autorelease]; + + // AppKit has a bug in [NSApplication setWindowsMenu:] where it will resolve + // the last item of the window menu's itemArray, but not account for the array + // being empty, resulting in a lookup of itemAtIndex:-1. To work around this, + // we insert a hidden dummy item into the menu. See FB13369198. + auto *dummyItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + dummyItem.hidden = YES; + [winMenuItem.submenu addItem:[dummyItem autorelease]]; + [mainMenu insertItem:winMenuItem atIndex:mainMenu.itemArray.count]; app.windowsMenu = winMenuItem.submenu; - // Windows, created and 'ordered front' before, will not be in this menu: + // Windows that have already been ordered in at this point have already been + // evaluated by AppKit via _addToWindowsMenuIfNecessary and added to the menu, + // but since the menu didn't exist at that point the addition was a noop. + // Instead of trying to duplicate the logic AppKit uses for deciding if + // a window should be part of the Window menu we toggle one of the settings + // that definitely will affect this, which results in AppKit reevaluating the + // situation and adding the window to the menu if necessary. for (NSWindow *win in app.windows) { - if (win.title && ![win.title isEqualToString:@""]) - [app addWindowsItem:win title:win.title filename:NO]; + win.excludedFromWindowsMenu = !win.excludedFromWindowsMenu; + win.excludedFromWindowsMenu = !win.excludedFromWindowsMenu; } } QList<QCocoaMenuItem*> QCocoaMenuBar::merged() const { QList<QCocoaMenuItem*> r; - for (auto menu : qAsConst(m_menus)) + for (auto menu : std::as_const(m_menus)) { + if (!menu) + continue; r.append(menu->merged()); + } return r; } @@ -357,10 +412,10 @@ bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const // When there is an application modal window on screen, the entries of // the menubar should be disabled. The exception in Qt is that if the // modal window is the only window on screen, then we enable the menu bar. - for (auto *window : qAsConst(topWindows)) { + for (auto *window : std::as_const(topWindows)) { if (window->isVisible() && window->modality() == Qt::ApplicationModal) { // check for other visible windows - for (auto *other : qAsConst(topWindows)) { + for (auto *other : std::as_const(topWindows)) { if ((window != other) && (other->isVisible())) { // INVARIANT: we found another visible window // on screen other than our modalWidget. We therefore @@ -381,8 +436,8 @@ bool QCocoaMenuBar::shouldDisable(QCocoaWindow *active) const QPlatformMenu *QCocoaMenuBar::menuForTag(quintptr tag) const { - for (auto menu : qAsConst(m_menus)) - if (menu->tag() == tag) + for (auto menu : std::as_const(m_menus)) + if (menu && menu->tag() == tag) return menu; return nullptr; @@ -390,10 +445,13 @@ QPlatformMenu *QCocoaMenuBar::menuForTag(quintptr tag) const NSMenuItem *QCocoaMenuBar::itemForRole(QPlatformMenuItem::MenuRole role) { - for (auto menu : qAsConst(m_menus)) - for (auto *item : menu->items()) - if (item->effectiveRole() == role) - return item->nsItem(); + for (auto menu : std::as_const(m_menus)) { + if (menu) { + for (auto *item : menu->items()) + if (item->effectiveRole() == role) + return item->nsItem(); + } + } return nil; } @@ -403,48 +461,6 @@ QCocoaWindow *QCocoaMenuBar::cocoaWindow() const return m_window.data(); } -void QCocoaMenuBar::insertDefaultEditItems(QCocoaMenu *menu) -{ - if (menu->items().isEmpty()) - return; - - NSMenu *nsEditMenu = menu->nsMenu(); - if ([nsEditMenu itemAtIndex:nsEditMenu.numberOfItems - 1].action - == @selector(orderFrontCharacterPalette:)) { - for (auto defaultEditMenuItem : qAsConst(m_defaultEditMenuItems)) { - if (menu->items().contains(defaultEditMenuItem)) - menu->removeMenuItem(defaultEditMenuItem); - } - qDeleteAll(m_defaultEditMenuItems); - m_defaultEditMenuItems.clear(); - } else { - if (m_defaultEditMenuItems.isEmpty()) { - QCocoaMenuItem *separator = new QCocoaMenuItem; - separator->setIsSeparator(true); - - QCocoaMenuItem *dictationItem = new QCocoaMenuItem; - dictationItem->setText(QCoreApplication::translate("QCocoaMenuItem", "Start Dictation...")); - QObject::connect(dictationItem, &QPlatformMenuItem::activated, this, []{ - [NSApplication.sharedApplication performSelector:@selector(startDictation:)]; - }); - - QCocoaMenuItem *emojiItem = new QCocoaMenuItem; - emojiItem->setText(QCoreApplication::translate("QCocoaMenuItem", "Emoji && Symbols")); - emojiItem->setShortcut(QKeyCombination(Qt::MetaModifier|Qt::ControlModifier, Qt::Key_Space)); - QObject::connect(emojiItem, &QPlatformMenuItem::activated, this, []{ - [NSApplication.sharedApplication orderFrontCharacterPalette:nil]; - }); - - m_defaultEditMenuItems << separator << dictationItem << emojiItem; - } - for (auto defaultEditMenuItem : qAsConst(m_defaultEditMenuItems)) { - if (menu->items().contains(defaultEditMenuItem)) - menu->removeMenuItem(defaultEditMenuItem); - menu->insertMenuItem(defaultEditMenuItem, nullptr); - } - } -} - QT_END_NAMESPACE #include "moc_qcocoamenubar.cpp" diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.h b/src/plugins/platforms/cocoa/qcocoamenuitem.h index 6e8c17d30c..f677ffb7a7 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.h +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.h @@ -8,7 +8,7 @@ #include <qpa/qplatformmenu.h> #include <QtGui/QImage> -//#define QT_COCOA_ENABLE_MENU_DEBUG +#include <QtCore/qpointer.h> Q_FORWARD_DECLARE_OBJC_CLASS(NSMenuItem); Q_FORWARD_DECLARE_OBJC_CLASS(NSMenu); diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.mm b/src/plugins/platforms/cocoa/qcocoamenuitem.mm index 688f0504a6..3a0f71bc50 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.mm @@ -16,6 +16,7 @@ #include "qcocoamenuloader.h" #include <QtGui/private/qcoregraphics_p.h> #include <QtCore/qregularexpression.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/private/qapplekeymapper_p.h> #include <QtCore/QDebug> @@ -179,38 +180,71 @@ void QCocoaMenuItem::setNativeContents(WId item) m_itemView.needsDisplay = YES; } -static QPlatformMenuItem::MenuRole detectMenuRole(const QString &caption) +static QPlatformMenuItem::MenuRole detectMenuRole(const QString &captionWithPossibleMnemonic) { - QString captionNoAmpersand(caption); - captionNoAmpersand.remove(u'&'); - const QString aboutString = QCoreApplication::translate("QCocoaMenuItem", "About"); - if (captionNoAmpersand.startsWith(aboutString, Qt::CaseInsensitive) - || captionNoAmpersand.endsWith(aboutString, Qt::CaseInsensitive)) { + QString itemCaption(captionWithPossibleMnemonic); + itemCaption.remove(u'&'); + + static const std::tuple<QPlatformMenuItem::MenuRole, std::vector<std::tuple<Qt::MatchFlags, const char *>>> roleMap[] = { + { QPlatformMenuItem::AboutRole, { + { Qt::MatchStartsWith | Qt::MatchEndsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "About") } + }}, + { QPlatformMenuItem::PreferencesRole, { + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Config") }, + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Preference") }, + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Options") }, + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Setting") }, + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Setup") }, + }}, + { QPlatformMenuItem::QuitRole, { + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Quit") }, + { Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Exit") }, + }}, + { QPlatformMenuItem::CutRole, { + { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Cut") } + }}, + { QPlatformMenuItem::CopyRole, { + { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Copy") } + }}, + { QPlatformMenuItem::PasteRole, { + { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Paste") } + }}, + { QPlatformMenuItem::SelectAllRole, { + { Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Select All") } + }}, + }; + + auto match = [](const QString &caption, const QString &itemCaption, Qt::MatchFlags matchFlags) { + if (matchFlags.testFlag(Qt::MatchExactly)) + return !itemCaption.compare(caption, Qt::CaseInsensitive); + if (matchFlags.testFlag(Qt::MatchStartsWith) && itemCaption.startsWith(caption, Qt::CaseInsensitive)) + return true; + if (matchFlags.testFlag(Qt::MatchEndsWith) && itemCaption.endsWith(caption, Qt::CaseInsensitive)) + return true; + return false; + }; + + QPlatformMenuItem::MenuRole detectedRole = [&]{ + for (const auto &[role, captions] : roleMap) { + for (const auto &[matchFlags, caption] : captions) { + // Check for untranslated match + if (match(caption, itemCaption, matchFlags)) + return role; + // Then translated with the current Qt translation + if (match(QCoreApplication::translate("QCocoaMenuItem", caption), itemCaption, matchFlags)) + return role; + } + } + return QPlatformMenuItem::NoRole; + }(); + + if (detectedRole == QPlatformMenuItem::AboutRole) { static const QRegularExpression qtRegExp("qt$"_L1, QRegularExpression::CaseInsensitiveOption); - if (captionNoAmpersand.contains(qtRegExp)) - return QPlatformMenuItem::AboutQtRole; - return QPlatformMenuItem::AboutRole; - } - if (captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Config"), Qt::CaseInsensitive) - || captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Preference"), Qt::CaseInsensitive) - || captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Options"), Qt::CaseInsensitive) - || captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Setting"), Qt::CaseInsensitive) - || captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Setup"), Qt::CaseInsensitive)) { - return QPlatformMenuItem::PreferencesRole; + if (itemCaption.contains(qtRegExp)) + detectedRole = QPlatformMenuItem::AboutQtRole; } - if (captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Quit"), Qt::CaseInsensitive) - || captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Exit"), Qt::CaseInsensitive)) { - return QPlatformMenuItem::QuitRole; - } - if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Cut"), Qt::CaseInsensitive)) - return QPlatformMenuItem::CutRole; - if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Copy"), Qt::CaseInsensitive)) - return QPlatformMenuItem::CopyRole; - if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Paste"), Qt::CaseInsensitive)) - return QPlatformMenuItem::PasteRole; - if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Select All"), Qt::CaseInsensitive)) - return QPlatformMenuItem::SelectAllRole; - return QPlatformMenuItem::NoRole; + + return detectedRole; } NSMenuItem *QCocoaMenuItem::sync() @@ -382,7 +416,7 @@ QKeySequence QCocoaMenuItem::mergeAccel() void QCocoaMenuItem::syncMerged() { if (!m_merged) { - qWarning("Trying to sync a non-merged item"); + qCWarning(lcQpaMenus) << "Trying to sync non-merged" << this; return; } @@ -439,7 +473,20 @@ void QCocoaMenuItem::resolveTargetAction() roleAction = @selector(selectAll:); break; default: - roleAction = @selector(qt_itemFired:); + if (m_menu) { + // Menu items that represent sub menus should have submenuAction: as their + // action, so that clicking the menu item opens the sub menu without closing + // the entire menu hierarchy. A menu item with this action and a valid submenu + // will disable NSMenuValidation for the item, which is normally not an issue + // as NSMenuItems are enabled by default. But in our case, we haven't attached + // the submenu yet, which results in AppKit concluding that there's no validator + // for the item (the target is nil, and nothing responds to submenuAction:), and + // will in response disable the menu item. To work around this we explicitly + // enable the menu item in QCocoaMenu::setAttachedItem() once we have a submenu. + roleAction = @selector(submenuAction:); + } else { + roleAction = @selector(qt_itemFired:); + } } m_native.action = roleAction; diff --git a/src/plugins/platforms/cocoa/qcocoamenuloader.mm b/src/plugins/platforms/cocoa/qcocoamenuloader.mm index 6d3c668b87..b5ee3479aa 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuloader.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuloader.mm @@ -186,8 +186,8 @@ if (appMenu.supermenu) unparentAppMenu(appMenu.supermenu); - NSMenuItem *appMenuItem = [[NSMenuItem alloc] initWithTitle:@"Apple" - action:nil keyEquivalent:@""]; + NSMenuItem *appMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Apple" + action:nil keyEquivalent:@""] autorelease]; appMenuItem.submenu = appMenu; [menu insertItem:appMenuItem atIndex:0]; } diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.h b/src/plugins/platforms/cocoa/qcocoamessagedialog.h new file mode 100644 index 0000000000..b8c273469a --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.h @@ -0,0 +1,38 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QCOCOAMESSAGEDIALOG_H +#define QCOCOAMESSAGEDIALOG_H + +#include <qpa/qplatformdialoghelper.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(NSAlert); +typedef long NSInteger; +typedef NSInteger NSModalResponse; + +QT_BEGIN_NAMESPACE + +class QEventLoop; + +class QCocoaMessageDialog : public QPlatformMessageDialogHelper +{ +public: + QCocoaMessageDialog() = default; + ~QCocoaMessageDialog(); + + void exec() override; + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + +private: + Qt::WindowModality modality() const; + NSAlert *m_alert = nullptr; + QEventLoop *m_eventLoop = nullptr; + NSModalResponse runModal() const; + void processResponse(NSModalResponse response); +}; + +QT_END_NAMESPACE + +#endif // QCOCOAMESSAGEDIALOG_H + diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm new file mode 100644 index 0000000000..84525099c9 --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm @@ -0,0 +1,425 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qcocoamessagedialog.h" + +#include "qcocoawindow.h" +#include "qcocoahelpers.h" +#include "qcocoaeventdispatcher.h" + +#include <QtCore/qmetaobject.h> +#include <QtCore/qscopedvaluerollback.h> +#include <QtCore/qtimer.h> + +#include <QtGui/qtextdocument.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/private/qcoregraphics_p.h> +#include <QtGui/qpa/qplatformtheme.h> + +#include <AppKit/NSAlert.h> +#include <AppKit/NSButton.h> + +QT_USE_NAMESPACE + +using namespace Qt::StringLiterals; + +QT_BEGIN_NAMESPACE + +QCocoaMessageDialog::~QCocoaMessageDialog() +{ + hide(); + [m_alert release]; +} + +static QString toPlainText(const QString &text) +{ + // FIXME: QMessageDialog supports Qt::TextFormat, which + // nowadays includes Qt::MarkdownText, but we don't have + // the machinery to deal with that yet. We should as a + // start plumb the dialog's text format to the platform + // via the dialog options. + + if (!Qt::mightBeRichText(text)) + return text; + + QTextDocument textDocument; + textDocument.setHtml(text); + return textDocument.toPlainText(); +} + +static NSControlStateValue controlStateFor(Qt::CheckState state) +{ + switch (state) { + case Qt::Checked: return NSControlStateValueOn; + case Qt::Unchecked: return NSControlStateValueOff; + case Qt::PartiallyChecked: return NSControlStateValueMixed; + } + Q_UNREACHABLE(); +} + +/* + Called from QDialogPrivate::setNativeDialogVisible() when the message box + is ready to be shown. + + At this point the options() will reflect the specific dialog shown. + + Returns true if the helper could successfully show the dialog, or + false if the cross platform fallback dialog should be used instead. +*/ +bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) +{ + Q_UNUSED(windowFlags); + + qCDebug(lcQpaDialogs) << "Asked to show" << windowModality << "dialog with parent" << parent; + + if (m_alert.window.visible) { + qCDebug(lcQpaDialogs) << "Dialog already visible, ignoring request to show"; + return true; // But we don't want to show the fallback dialog instead + } + + // We can only do application and window modal dialogs + if (windowModality == Qt::NonModal) + return false; + + // And only window modal if we have a parent + if (windowModality == Qt::WindowModal && (!parent || !parent->handle())) { + qCWarning(lcQpaDialogs, "Cannot run window modal dialog without parent window"); + return false; + } + + // And without options we don't know what to show + if (!options()) + return false; + + // NSAlert doesn't have a section for detailed text + if (!options()->detailedText().isEmpty()) { + qCWarning(lcQpaDialogs, "Message box contains detailed text"); + return false; + } + + if (Qt::mightBeRichText(options()->text()) || + Qt::mightBeRichText(options()->informativeText())) { + // Let's fallback to non-native message box, + // we only have plain NSString/text in NSAlert. + qCDebug(lcQpaDialogs, "Message box contains text in rich text format"); + return false; + } + + Q_ASSERT(!m_alert); + m_alert = [NSAlert new]; + m_alert.window.title = options()->windowTitle().toNSString(); + + const QString text = toPlainText(options()->text()); + m_alert.messageText = text.toNSString(); + m_alert.informativeText = toPlainText(options()->informativeText()).toNSString(); + + switch (options()->standardIcon()) { + case QMessageDialogOptions::NoIcon: { + // We only reflect the pixmap icon if the standard icon is unset, + // as setting a standard icon will also set a corresponding pixmap + // icon, which we don't want since it conflicts with the platform. + // If the user has set an explicit pixmap icon however, the standard + // icon will be NoIcon, so we're good. + QPixmap iconPixmap = options()->iconPixmap(); + if (!iconPixmap.isNull()) + m_alert.icon = [NSImage imageFromQImage:iconPixmap.toImage()]; + break; + } + case QMessageDialogOptions::Information: + case QMessageDialogOptions::Question: + [m_alert setAlertStyle:NSAlertStyleInformational]; + break; + case QMessageDialogOptions::Warning: + [m_alert setAlertStyle:NSAlertStyleWarning]; + break; + case QMessageDialogOptions::Critical: + [m_alert setAlertStyle:NSAlertStyleCritical]; + break; + } + + auto defaultButton = options()->defaultButton(); + auto escapeButton = options()->escapeButton(); + + const auto addButton = [&](auto title, auto tag, auto role) { + title = QPlatformTheme::removeMnemonics(title); + NSButton *button = [m_alert addButtonWithTitle:title.toNSString()]; + + // Calling addButtonWithTitle places buttons starting at the right side/top of the alert + // and going toward the left/bottom. By default, the first button has a key equivalent of + // Return, any button with a title of "Cancel" has a key equivalent of Escape, and any button + // with the title "Don't Save" has a key equivalent of Command-D (but only if it's not the first + // button). If an explicit default or escape button has been set, we respect these, + // and otherwise we fall back to role-based default and escape buttons. + + qCDebug(lcQpaDialogs).verbosity(0) << "Adding button" << title << "with" << role; + + if (!defaultButton && role == AcceptRole) + defaultButton = tag; + + if (tag == defaultButton) + button.keyEquivalent = @"\r"; + else if ([button.keyEquivalent isEqualToString:@"\r"]) + button.keyEquivalent = @""; + + if (!escapeButton && role == RejectRole) + escapeButton = tag; + + // Don't override default button with escape button, to match AppKit default + if (tag == escapeButton && ![button.keyEquivalent isEqualToString:@"\r"]) + button.keyEquivalent = @"\e"; + else if ([button.keyEquivalent isEqualToString:@"\e"]) + button.keyEquivalent = @""; + + if (@available(macOS 11, *)) + button.hasDestructiveAction = role == DestructiveRole; + + // The NSModalResponse of showing an NSAlert normally depends on the order of the + // button that was clicked, starting from the right with NSAlertFirstButtonReturn (1000), + // NSAlertSecondButtonReturn (1001), NSAlertThirdButtonReturn (1002), and after that + // NSAlertThirdButtonReturn + n. The response can also be customized per button via its + // tag, which, following the above logic, can include any positive value from 1000 and up. + // In addition the system reserves the values from -1000 and down for its own modal responses, + // such as NSModalResponseStop, NSModalResponseAbort, and NSModalResponseContinue. + // Luckily for us, the QPlatformDialogHelper::StandardButton enum values all fall within + // the positive range, so we can use the standard button value as the tag directly. + // The same applies to the custom button IDs, as these are generated in sequence after + // the QPlatformDialogHelper::LastButton. + Q_ASSERT(tag >= NSAlertFirstButtonReturn); + button.tag = tag; + }; + + // Resolve all dialog buttons from the options, both standard and custom + + struct Button { QString title; int identifier; ButtonRole role; }; + std::vector<Button> buttons; + + const auto *platformTheme = QGuiApplicationPrivate::platformTheme(); + if (auto standardButtons = options()->standardButtons()) { + for (int standardButton = FirstButton; standardButton <= LastButton; standardButton <<= 1) { + if (standardButtons & standardButton) { + auto title = platformTheme->standardButtonText(standardButton); + buttons.push_back({ + title, standardButton, buttonRole(StandardButton(standardButton)) + }); + } + } + } + const auto customButtons = options()->customButtons(); + for (auto customButton : customButtons) + buttons.push_back({customButton.label, customButton.id, customButton.role}); + + // Sort them according to the QPlatformDialogHelper::ButtonLayout for macOS + + // The ButtonLayout adds one additional role, AlternateRole, which is used + // for any AcceptRole beyond the first one, and should be ordered before the + // AcceptRole. Set this up by fixing the roles up front. + bool seenAccept = false; + for (auto &button : buttons) { + if (button.role == AcceptRole) { + if (!seenAccept) + seenAccept = true; + else + button.role = AlternateRole; + } + } + + std::vector<Button> orderedButtons; + const int *layoutEntry = buttonLayout(Qt::Horizontal, ButtonLayout::MacLayout); + while (*layoutEntry != QPlatformDialogHelper::EOL) { + const auto role = ButtonRole(*layoutEntry & ~ButtonRole::Reverse); + const bool reverse = *layoutEntry & ButtonRole::Reverse; + + auto addButton = [&](const Button &button) { + if (button.role == role) + orderedButtons.push_back(button); + }; + + if (reverse) + std::for_each(std::crbegin(buttons), std::crend(buttons), addButton); + else + std::for_each(std::cbegin(buttons), std::cend(buttons), addButton); + + ++layoutEntry; + } + + // Add them to the alert in reverse order, since buttons are added right to left + for (auto button = orderedButtons.crbegin(); button != orderedButtons.crend(); ++button) + addButton(button->title, button->identifier, button->role); + + // If we didn't find a an explicit or implicit default button above + // we restore the AppKit behavior of making the first button default. + if (!defaultButton) + m_alert.buttons.firstObject.keyEquivalent = @"\r"; + + if (auto checkBoxLabel = options()->checkBoxLabel(); !checkBoxLabel.isNull()) { + checkBoxLabel = QPlatformTheme::removeMnemonics(checkBoxLabel); + m_alert.suppressionButton.title = checkBoxLabel.toNSString(); + auto state = options()->checkBoxState(); + m_alert.suppressionButton.allowsMixedState = state == Qt::PartiallyChecked; + m_alert.suppressionButton.state = controlStateFor(state); + m_alert.showsSuppressionButton = YES; + } + + qCDebug(lcQpaDialogs) << "Showing" << m_alert; + + if (windowModality == Qt::WindowModal) { + auto *cocoaWindow = static_cast<QCocoaWindow*>(parent->handle()); + [m_alert beginSheetModalForWindow:cocoaWindow->nativeWindow() + completionHandler:^(NSModalResponse response) { + processResponse(response); + } + ]; + } else { + // The dialog is application modal, so we need to call runModal, + // but we can't call it here as the nativeDialogInUse state of QDialog + // depends on the result of show(), and we can't rely on doing it + // in exec(), as we can't guarantee that the user will call exec() + // after showing the dialog. As a workaround, we call it from exec(), + // but also make sure that if the user returns to the main runloop + // we'll run the modal dialog from there. + QTimer::singleShot(0, this, [this]{ + if (m_alert && !m_alert.window.visible) { + qCDebug(lcQpaDialogs) << "Running deferred modal" << m_alert; + QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); + processResponse(runModal()); + } + }); + } + + return true; +} + +// We shouldn't get NSModalResponseContinue as a response from NSAlert::runModal, +// and processResponse must not be called with that value (if we are there, it's +// too late to do anything about it. +// However, as QTBUG-114546 shows, there are scenarios where we might get that +// response anyway. We interpret it to keep the modal loop running, and we only +// return if we got something else to pass to processResponse. +NSModalResponse QCocoaMessageDialog::runModal() const +{ + NSModalResponse response = NSModalResponseContinue; + while (response == NSModalResponseContinue) + response = [m_alert runModal]; + return response; +} + +void QCocoaMessageDialog::exec() +{ + Q_ASSERT(m_alert); + + if (modality() == Qt::WindowModal) { + qCDebug(lcQpaDialogs) << "Running local event loop for window modal" << m_alert; + QEventLoop eventLoop; + QScopedValueRollback updateGuard(m_eventLoop, &eventLoop); + m_eventLoop->exec(QEventLoop::DialogExec); + } else { + qCDebug(lcQpaDialogs) << "Running modal" << m_alert; + QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); + processResponse(runModal()); + } +} + +// Custom modal response code to record that the dialog was hidden by us +static const NSInteger kModalResponseDialogHidden = NSAlertThirdButtonReturn + 1; + +static Qt::CheckState checkStateFor(NSControlStateValue state) +{ + switch (state) { + case NSControlStateValueOn: return Qt::Checked; + case NSControlStateValueOff: return Qt::Unchecked; + case NSControlStateValueMixed: return Qt::PartiallyChecked; + } + Q_UNREACHABLE(); +} + +void QCocoaMessageDialog::processResponse(NSModalResponse response) +{ + qCDebug(lcQpaDialogs) << "Processing response" << response << "for" << m_alert; + + // We can't re-use the same dialog for the next show() anyways, + // since the options may have changed, so get rid of it now, + // before we emit anything that might recurse back to hide/show/etc. + auto alert = std::exchange(m_alert, nil); + [alert autorelease]; + + if (alert.showsSuppressionButton) + emit checkBoxStateChanged(checkStateFor(alert.suppressionButton.state)); + + if (response >= NSAlertFirstButtonReturn) { + // Safe range for user-defined modal responses + if (response == kModalResponseDialogHidden) { + // Dialog was explicitly hidden by us, so nothing to report + qCDebug(lcQpaDialogs) << "Dialog was hidden; ignoring response"; + } else { + // Dialog buttons + if (response <= StandardButton::LastButton) { + Q_ASSERT(response >= StandardButton::FirstButton); + auto standardButton = StandardButton(response); + emit clicked(standardButton, buttonRole(standardButton)); + } else { + auto *customButton = options()->customButton(response); + Q_ASSERT(customButton); + emit clicked(StandardButton(customButton->id), customButton->role); + } + } + } else { + // We have to consider NSModalResponses beyond the ones specific to + // the alert buttons as the alert may be canceled programmatically. + + switch (response) { + case NSModalResponseContinue: + // Modal session is continuing (returned by runModalSession: only) + Q_UNREACHABLE(); + case NSModalResponseOK: + emit accept(); + break; + case NSModalResponseCancel: + case NSModalResponseStop: // Modal session was broken with stopModal + case NSModalResponseAbort: // Modal session was broken with abortModal + emit reject(); + break; + default: + qCWarning(lcQpaDialogs) << "Unrecognized modal response" << response; + } + } + + if (m_eventLoop) + m_eventLoop->exit(response); +} + +void QCocoaMessageDialog::hide() +{ + if (!m_alert) + return; + + if (m_alert.window.visible) { + qCDebug(lcQpaDialogs) << "Hiding" << modality() << m_alert; + + // Note: Just hiding or closing the NSAlert's NWindow here is not sufficient, + // as the dialog is running a modal event loop as well, which we need to end. + + if (modality() == Qt::WindowModal) { + // Will call processResponse() synchronously + [m_alert.window.sheetParent endSheet:m_alert.window returnCode:kModalResponseDialogHidden]; + } else { + if (NSApp.modalWindow == m_alert.window) { + // Will call processResponse() asynchronously + [NSApp stopModalWithCode:kModalResponseDialogHidden]; + } else { + qCWarning(lcQpaDialogs, "Dialog is not top level modal window. Cannot hide."); + } + } + } else { + qCDebug(lcQpaDialogs) << "No need to hide already hidden" << m_alert; + auto alert = std::exchange(m_alert, nil); + [alert autorelease]; + } +} + +Qt::WindowModality QCocoaMessageDialog::modality() const +{ + Q_ASSERT(m_alert && m_alert.window); + return m_alert.window.sheetParent ? Qt::WindowModal : Qt::ApplicationModal; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoamimetypes.mm b/src/plugins/platforms/cocoa/qcocoamimetypes.mm index 6a8a57d9a1..52e5c64538 100644 --- a/src/plugins/platforms/cocoa/qcocoamimetypes.mm +++ b/src/plugins/platforms/cocoa/qcocoamimetypes.mm @@ -4,7 +4,7 @@ #include <AppKit/AppKit.h> #include "qcocoamimetypes.h" -#include <QtGui/private/qmacmime_p.h> +#include <QtGui/qutimimeconverter.h> #include "qcocoahelpers.h" #include <QtGui/private/qcoregraphics_p.h> @@ -12,49 +12,40 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -class QMacPasteboardMimeTraditionalMacPlainText : public QMacInternalPasteboardMime { +class QMacMimeTraditionalMacPlainText : public QUtiMimeConverter { public: - QMacPasteboardMimeTraditionalMacPlainText() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; }; -QString QMacPasteboardMimeTraditionalMacPlainText::convertorName() -{ - return "PlainText (traditional-mac-plain-text)"_L1; -} - -QString QMacPasteboardMimeTraditionalMacPlainText::flavorFor(const QString &mime) +QString QMacMimeTraditionalMacPlainText::utiForMime(const QString &mime) const { if (mime == "text/plain"_L1) return "com.apple.traditional-mac-plain-text"_L1; return QString(); } -QString QMacPasteboardMimeTraditionalMacPlainText::mimeFor(QString flav) +QString QMacMimeTraditionalMacPlainText::mimeForUti(const QString &uti) const { - if (flav == "com.apple.traditional-mac-plain-text"_L1) + if (uti == "com.apple.traditional-mac-plain-text"_L1) return "text/plain"_L1; return QString(); } -bool QMacPasteboardMimeTraditionalMacPlainText::canConvert(const QString &mime, QString flav) -{ - return flavorFor(mime) == flav; -} - -QVariant QMacPasteboardMimeTraditionalMacPlainText::convertToMime(const QString &mimetype, QList<QByteArray> data, QString flavor) +QVariant +QMacMimeTraditionalMacPlainText::convertToMime(const QString &mimetype, + const QList<QByteArray> &data, + const QString &uti) const { if (data.count() > 1) - qWarning("QMacPasteboardMimeTraditionalMacPlainText: Cannot handle multiple member data"); + qWarning("QMacMimeTraditionalMacPlainText: Cannot handle multiple member data"); const QByteArray &firstData = data.first(); QVariant ret; - if (flavor == "com.apple.traditional-mac-plain-text"_L1) { + if (uti == "com.apple.traditional-mac-plain-text"_L1) { return QString(QCFString(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(firstData.constData()), firstData.size(), CFStringGetSystemEncoding(), false))); @@ -64,18 +55,21 @@ QVariant QMacPasteboardMimeTraditionalMacPlainText::convertToMime(const QString return ret; } -QList<QByteArray> QMacPasteboardMimeTraditionalMacPlainText::convertFromMime(const QString &, QVariant data, QString flavor) +QList<QByteArray> +QMacMimeTraditionalMacPlainText::convertFromMime(const QString &, + const QVariant &data, + const QString &uti) const { QList<QByteArray> ret; QString string = data.toString(); - if (flavor == "com.apple.traditional-mac-plain-text"_L1) + if (uti == "com.apple.traditional-mac-plain-text"_L1) ret.append(string.toLatin1()); return ret; } void QCocoaMimeTypes::initializeMimeTypes() { - new QMacPasteboardMimeTraditionalMacPlainText; + new QMacMimeTraditionalMacPlainText; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoanativeinterface.h b/src/plugins/platforms/cocoa/qcocoanativeinterface.h index 344c8523ce..a406cae366 100644 --- a/src/plugins/platforms/cocoa/qcocoanativeinterface.h +++ b/src/plugins/platforms/cocoa/qcocoanativeinterface.h @@ -31,12 +31,6 @@ public Q_SLOTS: void onAppFocusWindowChanged(QWindow *window); private: - /* - Function to return the default background pixmap. - Needed by QWizard in the Qt widget module. - */ - Q_INVOKABLE QPixmap defaultBackgroundPixmapForQWizard(); - Q_INVOKABLE void clearCurrentThreadCocoaEventDispatcherInterruptFlag(); static void registerDraggedTypes(const QStringList &types); @@ -53,9 +47,6 @@ private: // deregisters. static void registerTouchWindow(QWindow *window, bool enable); - // Set the size of the unified title and toolbar area. - static void setContentBorderThickness(QWindow *window, int topThickness, int bottomThickness); - // Set the size for a unified toolbar content border area. // Multiple callers can register areas and the platform plugin // will extend the "unified" area to cover them. @@ -67,12 +58,6 @@ private: // Returns true if the given coordinate is inside the current // content border. static bool testContentBorderPosition(QWindow *window, int position); - - // Sets a NSToolbar instance for the given QWindow. The - // toolbar will be attached to the native NSWindow when - // that is created; - static void setNSToolbar(QWindow *window, void *nsToolbar); - }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoanativeinterface.mm b/src/plugins/platforms/cocoa/qcocoanativeinterface.mm index 38a6060256..58bda2706a 100644 --- a/src/plugins/platforms/cocoa/qcocoanativeinterface.mm +++ b/src/plugins/platforms/cocoa/qcocoanativeinterface.mm @@ -26,6 +26,7 @@ #include <QtGui/qguiapplication.h> #include <qdebug.h> +#include <QtGui/private/qmacmimeregistry_p.h> #include <QtGui/private/qcoregraphics_p.h> #if QT_CONFIG(vulkan) @@ -64,49 +65,16 @@ QPlatformNativeInterface::NativeResourceForIntegrationFunction QCocoaNativeInter return NativeResourceForIntegrationFunction(QCocoaNativeInterface::registerTouchWindow); if (resource.toLower() == "setembeddedinforeignview") return NativeResourceForIntegrationFunction(QCocoaNativeInterface::setEmbeddedInForeignView); - if (resource.toLower() == "setcontentborderthickness") - return NativeResourceForIntegrationFunction(QCocoaNativeInterface::setContentBorderThickness); if (resource.toLower() == "registercontentborderarea") return NativeResourceForIntegrationFunction(QCocoaNativeInterface::registerContentBorderArea); if (resource.toLower() == "setcontentborderareaenabled") return NativeResourceForIntegrationFunction(QCocoaNativeInterface::setContentBorderAreaEnabled); - if (resource.toLower() == "setnstoolbar") - return NativeResourceForIntegrationFunction(QCocoaNativeInterface::setNSToolbar); if (resource.toLower() == "testcontentborderposition") return NativeResourceForIntegrationFunction(QCocoaNativeInterface::testContentBorderPosition); return nullptr; } -QPixmap QCocoaNativeInterface::defaultBackgroundPixmapForQWizard() -{ - // Note: starting with macOS 10.14, the KeyboardSetupAssistant app bundle no - // longer contains the "Background.png" image. This function then returns a - // null pixmap. - const int ExpectedImageWidth = 242; - const int ExpectedImageHeight = 414; - QCFType<CFArrayRef> urls = LSCopyApplicationURLsForBundleIdentifier( - CFSTR("com.apple.KeyboardSetupAssistant"), nullptr); - if (urls && CFArrayGetCount(urls) > 0) { - CFURLRef url = (CFURLRef)CFArrayGetValueAtIndex(urls, 0); - QCFType<CFBundleRef> bundle = CFBundleCreate(kCFAllocatorDefault, url); - if (bundle) { - url = CFBundleCopyResourceURL(bundle, CFSTR("Background"), CFSTR("png"), nullptr); - if (url) { - QCFType<CGImageSourceRef> imageSource = CGImageSourceCreateWithURL(url, nullptr); - QCFType<CGImageRef> image = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr); - if (image) { - int width = CGImageGetWidth(image); - int height = CGImageGetHeight(image); - if (width == ExpectedImageWidth && height == ExpectedImageHeight) - return QPixmap::fromImage(qt_mac_toQImage(image)); - } - } - } - } - return QPixmap(); -} - void QCocoaNativeInterface::clearCurrentThreadCocoaEventDispatcherInterruptFlag() { QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); @@ -120,7 +88,7 @@ void QCocoaNativeInterface::onAppFocusWindowChanged(QWindow *window) void QCocoaNativeInterface::registerDraggedTypes(const QStringList &types) { - qt_mac_registerDraggedTypes(types); + QMacMimeRegistry::registerDraggedTypes(types); } void QCocoaNativeInterface::setEmbeddedInForeignView(QPlatformWindow *window, bool embedded) @@ -140,16 +108,6 @@ void QCocoaNativeInterface::registerTouchWindow(QWindow *window, bool enable) cocoaWindow->registerTouch(enable); } -void QCocoaNativeInterface::setContentBorderThickness(QWindow *window, int topThickness, int bottomThickness) -{ - if (!window) - return; - - QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()); - if (cocoaWindow) - cocoaWindow->setContentBorderThickness(topThickness, bottomThickness); -} - void QCocoaNativeInterface::registerContentBorderArea(QWindow *window, quintptr identifier, int upper, int lower) { if (!window) @@ -170,15 +128,6 @@ void QCocoaNativeInterface::setContentBorderAreaEnabled(QWindow *window, quintpt cocoaWindow->setContentBorderAreaEnabled(identifier, enable); } -void QCocoaNativeInterface::setNSToolbar(QWindow *window, void *nsToolbar) -{ - QCocoaIntegration::instance()->setToolbar(window, static_cast<NSToolbar *>(nsToolbar)); - - QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()); - if (cocoaWindow) - cocoaWindow->updateNSToolbar(); -} - bool QCocoaNativeInterface::testContentBorderPosition(QWindow *window, int position) { if (!window) diff --git a/src/plugins/platforms/cocoa/qcocoansmenu.mm b/src/plugins/platforms/cocoa/qcocoansmenu.mm index 4bc9b0b5f9..ba222a3ef4 100644 --- a/src/plugins/platforms/cocoa/qcocoansmenu.mm +++ b/src/plugins/platforms/cocoa/qcocoansmenu.mm @@ -16,6 +16,8 @@ #include <QtCore/qvarlengtharray.h> #include <QtGui/private/qapplekeymapper_p.h> +#include <QtCore/qpointer.h> + static NSString *qt_mac_removePrivateUnicode(NSString *string) { if (const int len = string.length) { diff --git a/src/plugins/platforms/cocoa/qcocoascreen.h b/src/plugins/platforms/cocoa/qcocoascreen.h index 435a6b95d8..7708dc1968 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.h +++ b/src/plugins/platforms/cocoa/qcocoascreen.h @@ -41,13 +41,14 @@ public: QWindow *topLevelAt(const QPoint &point) const override; QList<QPlatformScreen *> virtualSiblings() const override; QPlatformScreen::SubpixelAntialiasingType subpixelAntialiasingTypeHint() const override; + Qt::ScreenOrientation orientation() const override; // ---------------------------------------------------- static NSScreen *nativeScreenForDisplayId(CGDirectDisplayID displayId); NSScreen *nativeScreen() const; - void requestUpdate(); + bool requestUpdate(); void deliverUpdateRequests(); bool isRunningDisplayLink() const; @@ -90,6 +91,7 @@ private: QSizeF m_physicalSize; QCocoaCursor *m_cursor; qreal m_devicePixelRatio = 0; + qreal m_rotation = 0; CVDisplayLinkRef m_displayLink = nullptr; dispatch_source_t m_displayLinkSource = nullptr; diff --git a/src/plugins/platforms/cocoa/qcocoascreen.mm b/src/plugins/platforms/cocoa/qcocoascreen.mm index 08cc22cdf3..be562e5455 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -15,7 +15,9 @@ #include <IOKit/graphics/IOGraphicsLib.h> #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qhighdpiscaling_p.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtCore/private/qeventdispatcher_cf_p.h> QT_BEGIN_NAMESPACE @@ -195,11 +197,10 @@ QCocoaScreen::~QCocoaScreen() dispatch_release(m_displayLinkSource); } -#if QT_MACOS_DEPLOYMENT_TARGET_BELOW(__MAC_10_15) static QString displayName(CGDirectDisplayID displayID) { QIOType<io_iterator_t> iterator; - if (IOServiceGetMatchingServices(kIOMasterPortDefault, + if (IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IODisplayConnect"), &iterator)) return QString(); @@ -227,7 +228,6 @@ static QString displayName(CGDirectDisplayID displayID) return QString(); } -#endif void QCocoaScreen::update(CGDirectDisplayID displayId) { @@ -248,6 +248,7 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) const QRect previousGeometry = m_geometry; const QRect previousAvailableGeometry = m_availableGeometry; const qreal previousRefreshRate = m_refreshRate; + const double previousRotation = m_rotation; // The reference screen for the geometry is always the primary screen QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID())); @@ -260,7 +261,9 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) m_depth = NSBitsPerPixelFromDepth(nsScreen.depth); m_colorSpace = QColorSpace::fromIccProfile(QByteArray::fromNSData(nsScreen.colorSpace.ICCProfileData)); if (!m_colorSpace.isValid()) { - qWarning() << "macOS generated a color-profile Qt couldn't parse. This shouldn't happen."; + qCWarning(lcQpaScreen) << "Failed to parse ICC profile for" << nsScreen.colorSpace + << "with ICC data" << nsScreen.colorSpace.ICCProfileData + << "- Falling back to sRGB"; m_colorSpace = QColorSpace::SRgb; } @@ -270,6 +273,7 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId); float refresh = CGDisplayModeGetRefreshRate(displayMode); m_refreshRate = refresh > 0 ? refresh : 60.0; + m_rotation = CGDisplayRotation(displayId); if (@available(macOS 10.15, *)) m_name = QString::fromNSString(nsScreen.localizedName); @@ -278,6 +282,9 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) const bool didChangeGeometry = m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry; + if (m_rotation != previousRotation) + QWindowSystemInterface::handleScreenOrientationChange(screen(), orientation()); + if (didChangeGeometry) QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), availableGeometry()); if (m_refreshRate != previousRefreshRate) @@ -288,24 +295,33 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) Q_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg); -void QCocoaScreen::requestUpdate() +bool QCocoaScreen::requestUpdate() { Q_ASSERT(m_displayId); if (!isOnline()) { qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request"; - return; + return false; } if (!m_displayLink) { - CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink); + qCDebug(lcQpaScreenUpdates) << "Creating display link for" << this; + if (CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink) != kCVReturnSuccess) { + qCWarning(lcQpaScreenUpdates) << "Failed to create display link for" << this; + return false; + } + if (auto displayId = CVDisplayLinkGetCurrentCGDisplay(m_displayLink); displayId != m_displayId) { + qCWarning(lcQpaScreenUpdates) << "Unexpected display" << displayId << "for display link"; + CVDisplayLinkRelease(m_displayLink); + m_displayLink = nullptr; + return false; + } CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*, const CVTimeStamp*, CVOptionFlags, CVOptionFlags*, void* displayLinkContext) -> int { // FIXME: It would be nice if update requests would include timing info static_cast<QCocoaScreen*>(displayLinkContext)->deliverUpdateRequests(); return kCVReturnSuccess; }, this); - qCDebug(lcQpaScreenUpdates) << "Display link created for" << this; // During live window resizing -[NSWindow _resizeWithEvent:] will spin a local event loop // in event-tracking mode, dequeuing only the mouse drag events needed to update the window's @@ -360,6 +376,8 @@ void QCocoaScreen::requestUpdate() qCDebug(lcQpaScreenUpdates) << "Starting display link for" << this; CVDisplayLinkStart(m_displayLink); } + + return true; } // Helper to allow building up debug output in multiple steps @@ -453,6 +471,25 @@ void QCocoaScreen::deliverUpdateRequests() if (!platformWindow->updatesWithDisplayLink()) continue; + // QTBUG-107198: Skip updates in a live resize for a better resize experience. + if (platformWindow->isContentView() && platformWindow->view().inLiveResize) { + const QSurface::SurfaceType surfaceType = window->surfaceType(); + const bool usesMetalLayer = surfaceType == QWindow::MetalSurface || surfaceType == QWindow::VulkanSurface; + const bool usesNonDefaultContentsPlacement = [platformWindow->view() layerContentsPlacement] + != NSViewLayerContentsPlacementScaleAxesIndependently; + if (usesMetalLayer && usesNonDefaultContentsPlacement) { + static bool deliverDisplayLinkUpdatesDuringLiveResize = + qEnvironmentVariableIsSet("QT_MAC_DISPLAY_LINK_UPDATE_IN_RESIZE"); + if (!deliverDisplayLinkUpdatesDuringLiveResize) { + // Must keep the link running, we do not know what the event + // handlers for UpdateRequest (which is not sent now) would do, + // would they trigger a new requestUpdate() or not. + pauseUpdates = false; + continue; + } + } + } + platformWindow->deliverUpdateRequest(); // Another update request was triggered, keep the display link running @@ -490,41 +527,56 @@ QPlatformScreen::SubpixelAntialiasingType QCocoaScreen::subpixelAntialiasingType return type; } +Qt::ScreenOrientation QCocoaScreen::orientation() const +{ + if (m_rotation == 0) + return Qt::LandscapeOrientation; + if (m_rotation == 90) + return Qt::PortraitOrientation; + if (m_rotation == 180) + return Qt::InvertedLandscapeOrientation; + if (m_rotation == 270) + return Qt::InvertedPortraitOrientation; + return QPlatformScreen::orientation(); +} + QWindow *QCocoaScreen::topLevelAt(const QPoint &point) const { - NSPoint screenPoint = mapToNative(point); - - // Search (hit test) for the top-level window. [NSWidow windowNumberAtPoint: - // belowWindowWithWindowNumber] may return windows that are not interesting - // to Qt. The search iterates until a suitable window or no window is found. - NSInteger topWindowNumber = 0; - QWindow *window = nullptr; - do { - // Get the top-most window, below any previously rejected window. - topWindowNumber = [NSWindow windowNumberAtPoint:screenPoint - belowWindowWithWindowNumber:topWindowNumber]; - - // Continue the search if the window does not belong to this process. - NSWindow *nsWindow = [NSApp windowWithWindowNumber:topWindowNumber]; - if (!nsWindow) - continue; + __block QWindow *window = nullptr; + [NSApp enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack + usingBlock:^(NSWindow *nsWindow, BOOL *stop) { + if (!nsWindow) + return; - // Continue the search if the window does not belong to Qt. - if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) - continue; + // Continue the search if the window does not belong to Qt + if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) + return; - QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow; - if (!cocoaWindow) - continue; - window = cocoaWindow->window(); + QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow; + if (!cocoaWindow) + return; + + QWindow *w = cocoaWindow->window(); + if (!w->isVisible()) + return; - // Continue the search if the window is not a top-level window. - if (!window->isTopLevel()) - continue; + auto nativeGeometry = QHighDpi::toNativePixels(w->geometry(), w); + if (!nativeGeometry.contains(point)) + return; - // Stop searching. The current window is the correct window. - break; - } while (topWindowNumber > 0); + QRegion mask = QHighDpi::toNativeLocalPosition(w->mask(), w); + if (!mask.isEmpty() && !mask.contains(point - nativeGeometry.topLeft())) + return; + + window = w; + + // Continue the search if the window is not a top-level window + if (!window->isTopLevel()) + return; + + *stop = true; + } + ]; return window; } @@ -793,10 +845,10 @@ QDebug operator<<(QDebug debug, const QCocoaScreen *screen) } #endif // !QT_NO_DEBUG_STREAM -#include "qcocoascreen.moc" - QT_END_NAMESPACE +#include "qcocoascreen.moc" + @implementation NSScreen (QtExtras) - (CGDirectDisplayID)qt_displayId diff --git a/src/plugins/platforms/cocoa/qcocoaservices.h b/src/plugins/platforms/cocoa/qcocoaservices.h index 20d9b67760..b6299570e8 100644 --- a/src/plugins/platforms/cocoa/qcocoaservices.h +++ b/src/plugins/platforms/cocoa/qcocoaservices.h @@ -4,6 +4,8 @@ #ifndef QCOCOADESKTOPSERVICES_H #define QCOCOADESKTOPSERVICES_H +#include <QtCore/qurl.h> + #include <qpa/qplatformservices.h> QT_BEGIN_NAMESPACE @@ -11,8 +13,16 @@ QT_BEGIN_NAMESPACE class QCocoaServices : public QPlatformServices { public: + bool hasCapability(Capability capability) const override; + bool openUrl(const QUrl &url) override; bool openDocument(const QUrl &url) override; + bool handleUrl(const QUrl &url); + + QPlatformServiceColorPicker *colorPicker(QWindow *parent) override; + +private: + QUrl m_handlingUrl; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoaservices.mm b/src/plugins/platforms/cocoa/qcocoaservices.mm index 29f8ed96e6..87212c265c 100644 --- a/src/plugins/platforms/cocoa/qcocoaservices.mm +++ b/src/plugins/platforms/cocoa/qcocoaservices.mm @@ -4,26 +4,70 @@ #include "qcocoaservices.h" #include <AppKit/NSWorkspace.h> +#include <AppKit/NSColorSampler.h> #include <Foundation/NSURL.h> #include <QtCore/QUrl> +#include <QtCore/qscopedvaluerollback.h> + +#include <QtGui/qdesktopservices.h> +#include <QtGui/private/qcoregraphics_p.h> QT_BEGIN_NAMESPACE bool QCocoaServices::openUrl(const QUrl &url) { - const QString scheme = url.scheme(); - if (scheme.isEmpty()) - return openDocument(url); + // avoid recursing back into self + if (url == m_handlingUrl) + return false; + return [[NSWorkspace sharedWorkspace] openURL:url.toNSURL()]; } bool QCocoaServices::openDocument(const QUrl &url) { - if (!url.isValid()) - return false; + return openUrl(url); +} + +/* Callback from macOS that the application should handle a URL */ +bool QCocoaServices::handleUrl(const QUrl &url) +{ + QScopedValueRollback<QUrl> rollback(m_handlingUrl, url); + // FIXME: Add platform services callback from QDesktopServices::setUrlHandler + // so that we can warn the user if calling setUrlHandler without also setting + // up the matching keys in the Info.plist file (CFBundleURLTypes and friends). + return QDesktopServices::openUrl(url); +} - return [[NSWorkspace sharedWorkspace] openFile:url.toLocalFile().toNSString()]; +class QCocoaColorPicker : public QPlatformServiceColorPicker +{ +public: + QCocoaColorPicker() : m_colorSampler([NSColorSampler new]) {} + ~QCocoaColorPicker() { [m_colorSampler release]; } + + void pickColor() override + { + [m_colorSampler showSamplerWithSelectionHandler:^(NSColor *selectedColor) { + emit colorPicked(qt_mac_toQColor(selectedColor)); + }]; + } +private: + NSColorSampler *m_colorSampler = nullptr; +}; + + +QPlatformServiceColorPicker *QCocoaServices::colorPicker(QWindow *parent) +{ + Q_UNUSED(parent); + return new QCocoaColorPicker; +} + +bool QCocoaServices::hasCapability(Capability capability) const +{ + switch (capability) { + case ColorPicking: return true; + default: return false; + } } QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.h b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.h index 414560e119..75c33cc5a3 100644 --- a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.h +++ b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.h @@ -45,12 +45,11 @@ public: bool isSystemTrayAvailable() const override; bool supportsMessages() const override; - void statusItemClicked(); + void emitActivated(); private: NSStatusItem *m_statusItem = nullptr; QStatusItemDelegate *m_delegate = nullptr; - QCocoaMenu *m_menu = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm index c004cd69b5..cec8301cf6 100644 --- a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm +++ b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm @@ -56,6 +56,12 @@ #include "qcocoascreen.h" #include <QtGui/private/qcoregraphics_p.h> +#warning NSUserNotification was deprecated in macOS 11. \ +We should be using UserNotifications.framework instead. \ +See QTBUG-110998 for more information. +#define NSUserNotificationCenter QT_IGNORE_DEPRECATIONS(NSUserNotificationCenter) +#define NSUserNotification QT_IGNORE_DEPRECATIONS(NSUserNotification) + QT_BEGIN_NAMESPACE void QCocoaSystemTrayIcon::init() @@ -64,6 +70,8 @@ void QCocoaSystemTrayIcon::init() m_delegate = [[QStatusItemDelegate alloc] initWithSysTray:this]; + // In case the status item does not have a menu assigned to it + // we fall back to the item's button to detect activation. m_statusItem.button.target = m_delegate; m_statusItem.button.action = @selector(statusItemClicked); [m_statusItem.button sendActionOn:NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown]; @@ -81,8 +89,6 @@ void QCocoaSystemTrayIcon::cleanup() [m_delegate release]; m_delegate = nil; - - m_menu = nullptr; } QRect QCocoaSystemTrayIcon::geometry() const @@ -178,12 +184,31 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon) void QCocoaSystemTrayIcon::updateMenu(QPlatformMenu *menu) { - // We don't set the menu property of the NSStatusItem here, - // as that would prevent us from receiving the action for the - // click, and we wouldn't be able to emit the activated signal. - // Instead we show the menu manually when the status item is - // clicked. - m_menu = static_cast<QCocoaMenu *>(menu); + auto *nsMenu = menu ? static_cast<QCocoaMenu *>(menu)->nsMenu() : nil; + if (m_statusItem.menu == nsMenu) + return; + + if (m_statusItem.menu) { + [NSNotificationCenter.defaultCenter removeObserver:m_delegate + name:NSMenuDidBeginTrackingNotification + object:m_statusItem.menu + ]; + } + + m_statusItem.menu = nsMenu; + + if (m_statusItem.menu) { + // When a menu is assigned, NSStatusBarButtonCell will intercept the mouse + // down to pop up the menu, and we never see the NSStatusBarButton action. + // To ensure we emit the 'activated' signal in both cases we detect when + // menu starts tracking, which happens before the menu delegate is sent + // the menuWillOpen callback we use to emit aboutToShow for the menu. + [NSNotificationCenter.defaultCenter addObserver:m_delegate + selector:@selector(statusItemMenuBeganTracking:) + name:NSMenuDidBeginTrackingNotification + object:m_statusItem.menu + ]; + } } void QCocoaSystemTrayIcon::updateToolTip(const QString &toolTip) @@ -226,7 +251,7 @@ void QCocoaSystemTrayIcon::showMessage(const QString &title, const QString &mess } } -void QCocoaSystemTrayIcon::statusItemClicked() +void QCocoaSystemTrayIcon::emitActivated() { auto *mouseEvent = NSApp.currentEvent; @@ -245,9 +270,6 @@ void QCocoaSystemTrayIcon::statusItemClicked() } emit activated(activationReason); - - if (NSMenu *menu = m_menu ? m_menu->nsMenu() : nil) - QT_IGNORE_DEPRECATIONS([m_statusItem popUpStatusItemMenu:menu]); } QT_END_NAMESPACE @@ -270,7 +292,12 @@ QT_END_NAMESPACE - (void)statusItemClicked { - self.platformSystemTray->statusItemClicked(); + self.platformSystemTray->emitActivated(); +} + +- (void)statusItemMenuBeganTracking:(NSNotification*)notification +{ + self.platformSystemTray->emitActivated(); } - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification diff --git a/src/plugins/platforms/cocoa/qcocoatheme.h b/src/plugins/platforms/cocoa/qcocoatheme.h index db8ade1fe7..97e0f633a7 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.h +++ b/src/plugins/platforms/cocoa/qcocoatheme.h @@ -35,14 +35,16 @@ public: const QFont *font(Font type = SystemFont) const override; QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions options = {}) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; QString standardButtonText(int button) const override; QKeySequence standardButtonShortcut(int button) const override; static const char *name; + void requestColorScheme(Qt::ColorScheme scheme) override; void handleSystemThemeChange(); #ifndef QT_NO_SHORTCUT @@ -54,6 +56,9 @@ private: QMacNotificationObserver m_systemColorObserver; mutable QHash<QPlatformTheme::Palette, QPalette*> m_palettes; QMacKeyValueObserver m_appearanceObserver; + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + void updateColorScheme(); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index fecb2b8c58..d9135c76c8 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -15,12 +15,14 @@ #include "qcocoahelpers.h" #include <QtCore/qfileinfo.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/private/qfont_p.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qcoregraphics_p.h> #include <QtGui/qpainter.h> #include <QtGui/qtextformat.h> #include <QtGui/private/qcoretextfontdatabase_p.h> +#include <QtGui/private/qappleiconengine_p.h> #include <QtGui/private/qfontengine_coretext_p.h> #include <QtGui/private/qabstractfileiconengine_p.h> #include <qpa/qplatformdialoghelper.h> @@ -30,21 +32,10 @@ #include "qcocoacolordialoghelper.h" #include "qcocoafiledialoghelper.h" #include "qcocoafontdialoghelper.h" +#include "qcocoamessagedialog.h" #include <CoreServices/CoreServices.h> -#if !QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_14) -@interface NSColor (MojaveForwardDeclarations) -@property (class, strong, readonly) NSColor *selectedContentBackgroundColor NS_AVAILABLE_MAC(10_14); -@property (class, strong, readonly) NSColor *unemphasizedSelectedTextBackgroundColor NS_AVAILABLE_MAC(10_14); -@property (class, strong, readonly) NSColor *unemphasizedSelectedTextColor NS_AVAILABLE_MAC(10_14); -@property (class, strong, readonly) NSColor *unemphasizedSelectedContentBackgroundColor NS_AVAILABLE_MAC(10_14); -@property (class, strong, readonly) NSArray<NSColor *> *alternatingContentBackgroundColors NS_AVAILABLE_MAC(10_14); -// Missing from non-Mojave SDKs, even if introduced in 10.10 -@property (class, strong, readonly) NSColor *linkColor NS_AVAILABLE_MAC(10_10); -@end -#endif - QT_BEGIN_NAMESPACE static QPalette *qt_mac_createSystemPalette() @@ -74,14 +65,9 @@ static QPalette *qt_mac_createSystemPalette() // System palette initialization: QBrush br = qt_mac_toQBrush([NSColor selectedControlColor]); palette->setBrush(QPalette::Active, QPalette::Highlight, br); - if (__builtin_available(macOS 10.14, *)) { - const auto inactiveHighlight = qt_mac_toQBrush([NSColor unemphasizedSelectedContentBackgroundColor]); - palette->setBrush(QPalette::Inactive, QPalette::Highlight, inactiveHighlight); - palette->setBrush(QPalette::Disabled, QPalette::Highlight, inactiveHighlight); - } else { - palette->setBrush(QPalette::Inactive, QPalette::Highlight, br); - palette->setBrush(QPalette::Disabled, QPalette::Highlight, br); - } + const auto inactiveHighlight = qt_mac_toQBrush([NSColor unemphasizedSelectedContentBackgroundColor]); + palette->setBrush(QPalette::Inactive, QPalette::Highlight, inactiveHighlight); + palette->setBrush(QPalette::Disabled, QPalette::Highlight, inactiveHighlight); palette->setBrush(QPalette::Shadow, qt_mac_toQColor([NSColor shadowColor])); @@ -109,6 +95,9 @@ static QPalette *qt_mac_createSystemPalette() palette->setColor(QPalette::Inactive, QPalette::PlaceholderText, qc); palette->setColor(QPalette::Disabled, QPalette::PlaceholderText, qc); + qc = qt_mac_toQColor([NSColor controlAccentColor]); + palette->setColor(QPalette::Accent, qc); + return palette; } @@ -166,17 +155,8 @@ static QHash<QPlatformTheme::Palette, QPalette*> qt_mac_createRolePalettes() } if (mac_widget_colors[i].paletteRole == QPlatformTheme::MenuPalette || mac_widget_colors[i].paletteRole == QPlatformTheme::MenuBarPalette) { - NSColor *selectedMenuItemColor = nil; - if (__builtin_available(macOS 10.14, *)) { - // Cheap approximation for NSVisualEffectView (see deprecation note for selectedMenuItemTextColor) - selectedMenuItemColor = [[NSColor controlAccentColor] highlightWithLevel:0.3]; - } else { - // selectedMenuItemColor would presumably be the correct color to use as the background - // for selected menu items. But that color is always blue, and doesn't follow the - // appearance color in system preferences. So we therefore deliberately choose to use - // keyboardFocusIndicatorColor instead, which appears to have the same color value. - selectedMenuItemColor = [NSColor keyboardFocusIndicatorColor]; - } + // Cheap approximation for NSVisualEffectView (see deprecation note for selectedMenuItemTextColor) + auto selectedMenuItemColor = [[NSColor controlAccentColor] highlightWithLevel:0.3]; pal.setBrush(QPalette::Highlight, qt_mac_toQColor(selectedMenuItemColor)); qc = qt_mac_toQColor([NSColor labelColor]); pal.setBrush(QPalette::ButtonText, qc); @@ -197,17 +177,10 @@ static QHash<QPlatformTheme::Palette, QPalette*> qt_mac_createRolePalettes() } else if (mac_widget_colors[i].paletteRole == QPlatformTheme::ItemViewPalette) { NSArray<NSColor *> *baseColors = nil; NSColor *activeHighlightColor = nil; - if (__builtin_available(macOS 10.14, *)) { - baseColors = [NSColor alternatingContentBackgroundColors]; - activeHighlightColor = [NSColor selectedContentBackgroundColor]; - pal.setBrush(QPalette::Inactive, QPalette::HighlightedText, - qt_mac_toQBrush([NSColor unemphasizedSelectedTextColor])); - } else { - baseColors = [NSColor controlAlternatingRowBackgroundColors]; - activeHighlightColor = [NSColor alternateSelectedControlColor]; - pal.setBrush(QPalette::Inactive, QPalette::HighlightedText, - pal.brush(QPalette::Active, QPalette::Text)); - } + baseColors = [NSColor alternatingContentBackgroundColors]; + activeHighlightColor = [NSColor selectedContentBackgroundColor]; + pal.setBrush(QPalette::Inactive, QPalette::HighlightedText, + qt_mac_toQBrush([NSColor unemphasizedSelectedTextColor])); pal.setBrush(QPalette::Base, qt_mac_toQBrush(baseColors[0])); pal.setBrush(QPalette::AlternateBase, qt_mac_toQBrush(baseColors[1])); pal.setBrush(QPalette::Active, QPalette::Highlight, @@ -241,21 +214,19 @@ const char *QCocoaTheme::name = "cocoa"; QCocoaTheme::QCocoaTheme() : m_systemPalette(nullptr) { -#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_14) if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSMojave) { m_appearanceObserver = QMacKeyValueObserver(NSApp, @"effectiveAppearance", [this] { - if (__builtin_available(macOS 10.14, *)) - NSAppearance.currentAppearance = NSApp.effectiveAppearance; - + NSAppearance.currentAppearance = NSApp.effectiveAppearance; handleSystemThemeChange(); }); } -#endif m_systemColorObserver = QMacNotificationObserver(nil, NSSystemColorsDidChangeNotification, [this] { handleSystemThemeChange(); }); + + updateColorScheme(); } QCocoaTheme::~QCocoaTheme() @@ -274,6 +245,9 @@ void QCocoaTheme::reset() void QCocoaTheme::handleSystemThemeChange() { reset(); + + updateColorScheme(); + m_systemPalette = qt_mac_createSystemPalette(); m_palettes = qt_mac_createRolePalettes(); @@ -287,13 +261,15 @@ void QCocoaTheme::handleSystemThemeChange() bool QCocoaTheme::usePlatformNativeDialog(DialogType dialogType) const { - if (dialogType == QPlatformTheme::FileDialog) - return true; - if (dialogType == QPlatformTheme::ColorDialog) - return true; - if (dialogType == QPlatformTheme::FontDialog) + switch (dialogType) { + case QPlatformTheme::FileDialog: + case QPlatformTheme::ColorDialog: + case QPlatformTheme::FontDialog: + case QPlatformTheme::MessageDialog: return true; - return false; + default: + return false; + } } QPlatformDialogHelper *QCocoaTheme::createPlatformDialogHelper(DialogType dialogType) const @@ -305,6 +281,8 @@ QPlatformDialogHelper *QCocoaTheme::createPlatformDialogHelper(DialogType dialog return new QCocoaColorDialogHelper(); case QPlatformTheme::FontDialog: return new QCocoaFontDialogHelper(); + case QPlatformTheme::MessageDialog: + return new QCocoaMessageDialog; default: return nullptr; } @@ -412,11 +390,11 @@ QPixmap QCocoaTheme::standardPixmap(StandardPixmap sp, const QSizeF &size) const if (iconType != 0) { QPixmap pixmap; IconRef icon = nullptr; - GetIconRef(kOnSystemDisk, kSystemIconsCreator, iconType, &icon); + QT_IGNORE_DEPRECATIONS(GetIconRef(kOnSystemDisk, kSystemIconsCreator, iconType, &icon)); if (icon) { pixmap = qt_mac_convert_iconref(icon, size.width(), size.height()); - ReleaseIconRef(icon); + QT_IGNORE_DEPRECATIONS(ReleaseIconRef(icon)); } return pixmap; @@ -432,19 +410,8 @@ public: QPlatformTheme::IconOptions opts) : QAbstractFileIconEngine(info, opts) {} - static QList<QSize> availableIconSizes() - { - const qreal devicePixelRatio = qGuiApp->devicePixelRatio(); - const int sizes[] = { - qRound(16 * devicePixelRatio), qRound(32 * devicePixelRatio), - qRound(64 * devicePixelRatio), qRound(128 * devicePixelRatio), - qRound(256 * devicePixelRatio) - }; - return QAbstractFileIconEngine::toSizeList(sizes, sizes + sizeof(sizes) / sizeof(sizes[0])); - } - QList<QSize> availableSizes(QIcon::Mode = QIcon::Normal, QIcon::State = QIcon::Off) override - { return QCocoaFileIconEngine::availableIconSizes(); } + { return QAppleIconEngine::availableIconSizes(); } protected: QPixmap filePixmap(const QSize &size, QIcon::Mode, QIcon::State) override @@ -463,6 +430,11 @@ QIcon QCocoaTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptio return QIcon(new QCocoaFileIconEngine(fileInfo, iconOptions)); } +QIconEngine *QCocoaTheme::createIconEngine(const QString &iconName) const +{ + return new QAppleIconEngine(iconName); +} + QVariant QCocoaTheme::themeHint(ThemeHint hint) const { switch (hint) { @@ -476,7 +448,7 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const return QVariant([[NSApplication sharedApplication] isFullKeyboardAccessEnabled] ? int(Qt::TabFocusAllControls) : int(Qt::TabFocusTextControls | Qt::TabFocusListControls)); case IconPixmapSizes: - return QVariant::fromValue(QCocoaFileIconEngine::availableIconSizes()); + return QVariant::fromValue(QAppleIconEngine::availableIconSizes()); case QPlatformTheme::PasswordMaskCharacter: return QVariant(QChar(0x2022)); case QPlatformTheme::UiEffects: @@ -489,15 +461,50 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const return !NSScreen.screensHaveSeparateSpaces; case QPlatformTheme::ShowDirectoriesFirst: return false; + case QPlatformTheme::MouseDoubleClickInterval: + return NSEvent.doubleClickInterval * 1000; + case QPlatformTheme::KeyboardInputInterval: + return NSEvent.keyRepeatDelay * 1000; + case QPlatformTheme::KeyboardAutoRepeatRate: + return 1.0 / NSEvent.keyRepeatInterval; default: break; } return QPlatformTheme::themeHint(hint); } -QPlatformTheme::Appearance QCocoaTheme::appearance() const +Qt::ColorScheme QCocoaTheme::colorScheme() const +{ + return m_colorScheme; +} + +void QCocoaTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + NSAppearance *appearance = nil; + switch (scheme) { + case Qt::ColorScheme::Dark: + appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + break; + case Qt::ColorScheme::Light: + appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + break; + case Qt::ColorScheme::Unknown: + break; + } + if (appearance != NSApp.effectiveAppearance) + NSApplication.sharedApplication.appearance = appearance; +} + +/* + Update the theme's color scheme based on the current appearance. + + We can only reference the appearance on the main thread, but the + CoreText font engine needs to know the color scheme, and might be + used from secondary threads, so we cache the color scheme. +*/ +void QCocoaTheme::updateColorScheme() { - return qt_mac_applicationIsInDarkMode() ? Appearance::Dark : Appearance::Light; + m_colorScheme = qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; } QString QCocoaTheme::standardButtonText(int button) const @@ -515,12 +522,16 @@ QKeySequence QCocoaTheme::standardButtonShortcut(int button) const QPlatformMenuItem *QCocoaTheme::createPlatformMenuItem() const { - return new QCocoaMenuItem(); + auto *menuItem = new QCocoaMenuItem(); + qCDebug(lcQpaMenus) << "Created" << menuItem; + return menuItem; } QPlatformMenu *QCocoaTheme::createPlatformMenu() const { - return new QCocoaMenu(); + auto *menu = new QCocoaMenu(); + qCDebug(lcQpaMenus) << "Created" << menu; + return menu; } QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const @@ -533,7 +544,9 @@ QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const SLOT(onAppFocusWindowChanged(QWindow*))); } - return new QCocoaMenuBar(); + auto *menuBar = new QCocoaMenuBar(); + qCDebug(lcQpaMenus) << "Created" << menuBar; + return menuBar; } #ifndef QT_NO_SHORTCUT diff --git a/src/plugins/platforms/cocoa/qcocoawindow.h b/src/plugins/platforms/cocoa/qcocoawindow.h index 67fabadb01..ba1bc052fb 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.h +++ b/src/plugins/platforms/cocoa/qcocoawindow.h @@ -39,9 +39,9 @@ class QDebug; // QCocoaWindow // // QCocoaWindow is an NSView (not an NSWindow!) in the sense -// that it relies on a NSView for all event handling and -// graphics output and does not require a NSWindow, except for -// for the window-related functions like setWindowTitle. +// that it relies on an NSView for all event handling and +// graphics output and does not require an NSWindow, except for +// the window-related functions like setWindowTitle. // // As a consequence of this it is possible to embed the QCocoaWindow // in an NSView hierarchy by getting a pointer to the "backing" @@ -158,18 +158,19 @@ public: void setWindowCursor(NSCursor *cursor); void registerTouch(bool enable); - void setContentBorderThickness(int topThickness, int bottomThickness); + void registerContentBorderArea(quintptr identifier, int upper, int lower); void setContentBorderAreaEnabled(quintptr identifier, bool enable); void setContentBorderEnabled(bool enable) override; bool testContentBorderAreaPosition(int position) const; void applyContentBorderThickness(NSWindow *window = nullptr); - void updateNSToolbar(); qreal devicePixelRatio() const override; QWindow *childWindowAt(QPoint windowPoint); bool shouldRefuseKeyWindowAndFirstResponder(); + bool windowEvent(QEvent *event) override; + QPoint bottomLeftClippedByNSWindowOffset() const override; void updateNormalGeometry(); @@ -189,7 +190,7 @@ protected: void recreateWindowIfNeeded(); QCocoaNSWindow *createNSWindow(bool shouldBePanel); - Qt::WindowState windowState() const; + Qt::WindowStates windowState() const; void applyWindowState(Qt::WindowStates newState); void toggleMaximized(); void toggleFullScreen(); @@ -215,32 +216,35 @@ public: // for QNSView void handleWindowStateChanged(HandleFlags flags = NoHandleFlags); void handleExposeEvent(const QRegion ®ion); - NSView *m_view; - QCocoaNSWindow *m_nsWindow; + static void closeAllPopups(); + static void setupPopupMonitor(); + static void removePopupMonitor(); + + NSView *m_view = nil; + QCocoaNSWindow *m_nsWindow = nil; - Qt::WindowStates m_lastReportedWindowState; - Qt::WindowModality m_windowModality; + Qt::WindowStates m_lastReportedWindowState = Qt::WindowNoState; + Qt::WindowModality m_windowModality = Qt::NonModal; static QPointer<QCocoaWindow> s_windowUnderMouse; - bool m_initialized; - bool m_inSetVisible; - bool m_inSetGeometry; - bool m_inSetStyleMask; - QCocoaMenuBar *m_menubar; + bool m_initialized = false; + bool m_inSetVisible = false; + bool m_inSetGeometry = false; + bool m_inSetStyleMask = false; - bool m_frameStrutEventsEnabled; + QCocoaMenuBar *m_menubar = nullptr; + + bool m_frameStrutEventsEnabled = false; QRect m_exposedRect; QRect m_normalGeometry; - int m_registerTouchCount; - bool m_resizableTransientParent; + int m_registerTouchCount = 0; + bool m_resizableTransientParent = false; static const int NoAlertRequest; - NSInteger m_alertRequest; + NSInteger m_alertRequest = NoAlertRequest; - bool m_drawContentBorderGradient; - int m_topContentBorderThickness; - int m_bottomContentBorderThickness; + bool m_drawContentBorderGradient = false; struct BorderRange { BorderRange(quintptr i, int u, int l) : identifier(i), upper(u), lower(l) { } @@ -254,6 +258,9 @@ public: // for QNSView QHash<quintptr, BorderRange> m_contentBorderAreas; // identifier -> uppper/lower QHash<quintptr, bool> m_enabledContentBorderAreas; // identifier -> enabled state (true/false) + static inline id s_globalMouseMonitor = 0; + static inline id s_applicationActivationObserver = 0; + #if QT_CONFIG(vulkan) VkSurfaceKHR m_vulkanSurface = nullptr; #endif diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index f20aad6bb9..d2c9bb0196 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -98,24 +98,7 @@ Q_CONSTRUCTOR_FUNCTION(qRegisterNotificationCallbacks) const int QCocoaWindow::NoAlertRequest = -1; QPointer<QCocoaWindow> QCocoaWindow::s_windowUnderMouse; -QCocoaWindow::QCocoaWindow(QWindow *win, WId nativeHandle) - : QPlatformWindow(win) - , m_view(nil) - , m_nsWindow(nil) - , m_lastReportedWindowState(Qt::WindowNoState) - , m_windowModality(Qt::NonModal) - , m_initialized(false) - , m_inSetVisible(false) - , m_inSetGeometry(false) - , m_inSetStyleMask(false) - , m_menubar(nullptr) - , m_frameStrutEventsEnabled(false) - , m_registerTouchCount(0) - , m_resizableTransientParent(false) - , m_alertRequest(NoAlertRequest) - , m_drawContentBorderGradient(false) - , m_topContentBorderThickness(0) - , m_bottomContentBorderThickness(0) +QCocoaWindow::QCocoaWindow(QWindow *win, WId nativeHandle) : QPlatformWindow(win) { qCDebug(lcQpaWindow) << "QCocoaWindow::QCocoaWindow" << window(); @@ -134,16 +117,24 @@ void QCocoaWindow::initialize() if (!m_view) m_view = [[QNSView alloc] initWithCocoaWindow:this]; - // Compute the initial geometry based on the geometry set on the - // QWindow. This geometry has already been reflected to the - // QPlatformWindow in the constructor, so to ensure that the - // resulting setGeometry call does not think the geometry has - // already been applied, we reset the QPlatformWindow's view - // of the geometry first. - auto initialGeometry = QPlatformWindow::initialGeometry(window(), - windowGeometry(), defaultWindowWidth, defaultWindowHeight); - QPlatformWindow::d_ptr->rect = QRect(); - setGeometry(initialGeometry); + if (!isForeignWindow()) { + // Compute the initial geometry based on the geometry set on the + // QWindow. This geometry has already been reflected to the + // QPlatformWindow in the constructor, so to ensure that the + // resulting setGeometry call does not think the geometry has + // already been applied, we reset the QPlatformWindow's view + // of the geometry first. + auto initialGeometry = QPlatformWindow::initialGeometry(window(), + windowGeometry(), defaultWindowWidth, defaultWindowHeight); + QPlatformWindow::d_ptr->rect = QRect(); + setGeometry(initialGeometry); + + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); + + } else { + // Pick up essential foreign window state + QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect()); + } recreateWindowIfNeeded(); @@ -159,7 +150,10 @@ QCocoaWindow::~QCocoaWindow() QMacAutoReleasePool pool; [m_nsWindow makeFirstResponder:nil]; [m_nsWindow setContentView:nil]; - if ([m_view superview]) + + // Remove from superview only if we have a Qt window parent, + // as we don't want to affect window container foreign windows. + if (QPlatformWindow::parent()) [m_view removeFromSuperview]; // Make sure to disconnect observer in all case if view is valid @@ -183,8 +177,7 @@ QCocoaWindow::~QCocoaWindow() object:m_view]; [m_view release]; - [m_nsWindow close]; - [m_nsWindow release]; + [m_nsWindow closeAndRelease]; } QSurfaceFormat QCocoaWindow::format() const @@ -315,6 +308,31 @@ void QCocoaWindow::setVisible(bool visible) { qCDebug(lcQpaWindow) << "QCocoaWindow::setVisible" << window() << visible; + // Our implementation of setVisible below is not idempotent, as for + // modal windows it calls beginSheet/endSheet or starts/ends modal + // sessions. However we can't simply guard for m_view.hidden already + // having the right state, as the behavior of this function differs + // based on whether the window has been initialized or not, as + // handleGeometryChange will bail out if the window is still + // initializing. Since we know we'll get a second setVisible + // call after creation, we can check for that case specifically, + // which means we can then safely guard on m_view.hidden changing. + + if (!m_initialized) { + qCDebug(lcQpaWindow) << "Window still initializing, skipping setting visibility"; + return; // We'll get another setVisible call after create is done + } + + if (visible == !m_view.hidden && (!isContentView() || visible == m_view.window.visible)) { + qCDebug(lcQpaWindow) << "No change in visible status. Ignoring."; + return; + } + + if (m_inSetVisible) { + qCWarning(lcQpaWindow) << "Already setting window visible!"; + return; + } + QScopedValueRollback<bool> rollback(m_inSetVisible, true); QMacAutoReleasePool pool; @@ -353,6 +371,9 @@ void QCocoaWindow::setVisible(bool visible) } + // Make the NSView visible first, before showing the NSWindow (in case of top level windows) + m_view.hidden = NO; + if (isContentView()) { QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); @@ -390,13 +411,6 @@ void QCocoaWindow::setVisible(bool visible) } } } - - // In some cases, e.g. QDockWidget, the content view is hidden before moving to its own - // Cocoa window, and then shown again. Therefore, we test for the view being hidden even - // if it's attached to an NSWindow. - if ([m_view isHidden]) - [m_view setHidden:NO]; - } else { // Window not visible, hide it if (isContentView()) { @@ -425,10 +439,28 @@ void QCocoaWindow::setVisible(bool visible) if (mainWindow && [mainWindow canBecomeKeyWindow]) [mainWindow makeKeyWindow]; } - } else { - [m_view setHidden:YES]; } + // AppKit will in some cases set up the key view loop for child views, even if we + // don't set autorecalculatesKeyViewLoop, nor call recalculateKeyViewLoop ourselves. + // When a child window is promoted to a top level, AppKit will maintain the key view + // loop between the views, even if these views now cross NSWindows, even after we + // explicitly call recalculateKeyViewLoop. When the top level is then hidden, AppKit + // will complain when -[NSView _setHidden:setNeedsDisplay:] tries to transfer first + // responder by reading the nextValidKeyView, and it turns out to live in a different + // window. We mitigate this by a last second reset of the first responder, which is + // what AppKit also falls back to. It's unclear if the original situation of views + // having their nextKeyView pointing to views in other windows is kosher or not. + if (m_view.window.firstResponder == m_view && m_view.nextValidKeyView + && m_view.nextValidKeyView.window != m_view.window) { + qCDebug(lcQpaWindow) << "Detected nextValidKeyView" << m_view.nextValidKeyView + << "in different window" << m_view.nextValidKeyView.window + << "Resetting" << m_view.window << "first responder to nil."; + [m_view.window makeFirstResponder:nil]; + } + + m_view.hidden = YES; + if (parentCocoaWindow && window()->type() == Qt::Popup) { NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow(); if (m_resizableTransientParent @@ -482,7 +514,7 @@ NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags) auto *nsWindow = transientCocoaWindow->nativeWindow(); // We only upgrade the window level for "special" windows, to work - // around Qt Designer parenting the designer windows to the widget + // around Qt Widgets Designer parenting the designer windows to the widget // palette window (QTBUG-31779). This should be fixed in designer. if (type != Qt::Window) windowLevel = qMax(windowLevel, nsWindow.level); @@ -526,9 +558,11 @@ NSUInteger QCocoaWindow::windowStyleMask(Qt::WindowFlags flags) // working (for example minimizing frameless windows, or resizing // windows that don't have zoom or fullscreen titlebar buttons). styleMask |= NSWindowStyleMaskClosable - | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable; + if (type != Qt::Popup) // We only care about popups exactly. + styleMask |= NSWindowStyleMaskResizable; + if (type == Qt::Tool) styleMask |= NSWindowStyleMaskUtilityWindow; @@ -556,8 +590,6 @@ void QCocoaWindow::updateTitleBarButtons(Qt::WindowFlags windowFlags) if (!isContentView()) return; - NSWindow *window = m_view.window; - static constexpr std::pair<NSWindowButton, Qt::WindowFlags> buttons[] = { { NSWindowCloseButton, Qt::WindowCloseButtonHint }, { NSWindowMiniaturizeButton, Qt::WindowMinimizeButtonHint}, @@ -566,20 +598,38 @@ void QCocoaWindow::updateTitleBarButtons(Qt::WindowFlags windowFlags) bool hideButtons = true; for (const auto &[button, buttonHint] : buttons) { + // Set up Qt defaults based on window type bool enabled = true; + if (button == NSWindowMiniaturizeButton) + enabled = window()->type() != Qt::Dialog; + + // Let users override via CustomizeWindowHint if (windowFlags & Qt::CustomizeWindowHint) enabled = windowFlags & buttonHint; + // Then do some final sanitizations + if (button == NSWindowZoomButton && isFixedSize()) enabled = false; - [window standardWindowButton:button].enabled = enabled; + // Mimic what macOS natively does for parent windows of modal + // sheets, which is to disable the close button, but leave the + // other buttons as they were. + if (button == NSWindowCloseButton && enabled + && QWindowPrivate::get(window())->blockedByModalWindow) { + enabled = false; + // If we end up having no enabled buttons, our workaround + // should not be a reason for hiding all of them. + hideButtons = false; + } + + [m_view.window standardWindowButton:button].enabled = enabled; hideButtons &= !enabled; } // Hide buttons in case we disabled all of them for (const auto &[button, buttonHint] : buttons) - [window standardWindowButton:button].hidden = hideButtons; + [m_view.window standardWindowButton:button].hidden = hideButtons; } void QCocoaWindow::setWindowFlags(Qt::WindowFlags flags) @@ -658,7 +708,7 @@ void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState) if (!isContentView()) return; - const Qt::WindowState currentState = windowState(); + const Qt::WindowState currentState = QWindowPrivate::effectiveState(windowState()); const Qt::WindowState newState = QWindowPrivate::effectiveState(requestedState); if (newState == currentState) @@ -690,9 +740,10 @@ void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState) switch (currentState) { case Qt::WindowMinimized: [nsWindow deminiaturize:sender]; - Q_ASSERT_X(windowState() != Qt::WindowMinimized, "QCocoaWindow", - "[NSWindow deminiaturize:] is synchronous"); - break; + // Deminiaturizing is not synchronous, so we need to wait for the + // NSWindowDidMiniaturizeNotification before continuing to apply + // the new state. + return; case Qt::WindowFullScreen: { toggleFullScreen(); // Exiting fullscreen is not synchronous, so we need to wait for the @@ -726,23 +777,27 @@ void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState) } } -Qt::WindowState QCocoaWindow::windowState() const +Qt::WindowStates QCocoaWindow::windowState() const { - // FIXME: Support compound states (Qt::WindowStates) - + Qt::WindowStates states = Qt::WindowNoState; NSWindow *window = m_view.window; + if (window.miniaturized) - return Qt::WindowMinimized; - if (window.qt_fullScreen) - return Qt::WindowFullScreen; - if ((window.zoomed && !isTransitioningToFullScreen()) - || (m_lastReportedWindowState == Qt::WindowMaximized && isTransitioningToFullScreen())) - return Qt::WindowMaximized; + states |= Qt::WindowMinimized; + + // Full screen and maximized are mutually exclusive, as macOS + // will report a full screen window as zoomed. + if (window.qt_fullScreen) { + states |= Qt::WindowFullScreen; + } else if ((window.zoomed && !isTransitioningToFullScreen()) + || (m_lastReportedWindowState == Qt::WindowMaximized && isTransitioningToFullScreen())) { + states |= Qt::WindowMaximized; + } // Note: We do not report Qt::WindowActive, even if isActive() // is true, as QtGui does not expect this window state to be set. - return Qt::WindowNoState; + return states; } void QCocoaWindow::toggleMaximized() @@ -858,12 +913,20 @@ void QCocoaWindow::windowDidDeminiaturize() if (!isContentView()) return; + Qt::WindowState requestedState = window()->windowState(); + handleWindowStateChanged(); + + if (requestedState != windowState() && requestedState != Qt::WindowMinimized) { + // We were only going out of minimized as an intermediate step before + // progressing into the final step, so re-sync the desired state. + applyWindowState(requestedState); + } } void QCocoaWindow::handleWindowStateChanged(HandleFlags flags) { - Qt::WindowState currentState = windowState(); + Qt::WindowStates currentState = windowState(); if (!(flags & HandleUnconditionally) && currentState == m_lastReportedWindowState) return; @@ -1078,28 +1141,14 @@ void QCocoaWindow::setMask(const QRegion ®ion) } } -bool QCocoaWindow::setKeyboardGrabEnabled(bool grab) +bool QCocoaWindow::setKeyboardGrabEnabled(bool) { - qCDebug(lcQpaWindow) << "QCocoaWindow::setKeyboardGrabEnabled" << window() << grab; - if (!isContentView()) - return false; - - if (grab && ![m_view.window isKeyWindow]) - [m_view.window makeKeyWindow]; - - return true; + return false; // FIXME (QTBUG-106597) } -bool QCocoaWindow::setMouseGrabEnabled(bool grab) +bool QCocoaWindow::setMouseGrabEnabled(bool) { - qCDebug(lcQpaWindow) << "QCocoaWindow::setMouseGrabEnabled" << window() << grab; - if (!isContentView()) - return false; - - if (grab && ![m_view.window isKeyWindow]) - [m_view.window makeKeyWindow]; - - return true; + return false; // FIXME (QTBUG-106597) } WId QCocoaWindow::winId() const @@ -1197,36 +1246,37 @@ void QCocoaWindow::windowDidEndLiveResize() void QCocoaWindow::windowDidBecomeKey() { - if (!isContentView()) + // The NSWindow we're part of become key. Check if we're the first + // responder, and if so, deliver focus window change to our window. + if (m_view.window.firstResponder != m_view) return; - if (isForeignWindow()) - return; + qCDebug(lcQpaWindow) << m_view.window << "became key window." + << "Updating focus window to" << this << "with view" << m_view; - QNSView *firstResponderView = qt_objc_cast<QNSView *>(m_view.window.firstResponder); - if (!firstResponderView) - return; - - const QCocoaWindow *focusCocoaWindow = firstResponderView.platformWindow; - if (focusCocoaWindow->windowIsPopupType()) + if (windowIsPopupType()) { + qCDebug(lcQpaWindow) << "Window is popup. Skipping focus window change."; return; + } // See also [QNSView becomeFirstResponder] - QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>( - focusCocoaWindow->window(), Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>( + window(), Qt::ActiveWindowFocusReason); } void QCocoaWindow::windowDidResignKey() { - if (!isContentView()) + // The NSWindow we're part of lost key. Check if we're the first + // responder, and if so, deliver window deactivation to our window. + if (m_view.window.firstResponder != m_view) return; - if (isForeignWindow()) - return; + qCDebug(lcQpaWindow) << m_view.window << "resigned key window." + << "Clearing focus window" << this << "with view" << m_view; // Make sure popups are closed before we deliver activation changes, which are // otherwise ignored by QApplication. - QGuiApplicationPrivate::instance()->closeAllPopups(); + closeAllPopups(); // The current key window will be non-nil if another window became key. If that // window is a Qt window, we delay the window activation event until the didBecomeKey @@ -1234,12 +1284,14 @@ void QCocoaWindow::windowDidResignKey() NSWindow *newKeyWindow = [NSApp keyWindow]; if (newKeyWindow && newKeyWindow != m_view.window && [newKeyWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) { + qCDebug(lcQpaWindow) << "New key window" << newKeyWindow + << "is Qt window. Deferring focus window change."; return; } // Lost key window, go ahead and set the active window to zero if (!windowIsPopupType()) { - QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>( + QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>( nullptr, Qt::ActiveWindowFocusReason); } } @@ -1276,8 +1328,14 @@ void QCocoaWindow::windowDidOrderOffScreen() void QCocoaWindow::windowDidChangeOcclusionState() { + // Note, we don't take the view's hiddenOrHasHiddenAncestor state into + // account here, but instead leave that up to handleExposeEvent, just + // like all the other signals that could potentially change the exposed + // state of the window. bool visible = m_view.window.occlusionState & NSWindowOcclusionStateVisible; - qCDebug(lcQpaWindow) << "QCocoaWindow::windowDidChangeOcclusionState" << window() << "is now" << (visible ? "visible" : "occluded"); + qCDebug(lcQpaWindow) << "Occlusion state of" << m_view.window << "for" + << window() << "changed to" << (visible ? "visible" : "occluded"); + if (visible) [m_view setNeedsDisplay:YES]; else @@ -1445,14 +1503,30 @@ void QCocoaWindow::recreateWindowIfNeeded() QMacAutoReleasePool pool; QPlatformWindow *parentWindow = QPlatformWindow::parent(); - - const bool isEmbeddedView = isEmbedded(); - RecreationReasons recreateReason = RecreationNotNeeded; + auto *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow); QCocoaWindow *oldParentCocoaWindow = nullptr; if (QNSView *qnsView = qnsview_cast(m_view.superview)) oldParentCocoaWindow = qnsView.platformWindow; + if (isForeignWindow()) { + // A foreign window is created as such, and can never move between being + // foreign and not, so we don't need to get rid of any existing NSWindows, + // nor create new ones, as a foreign window is a single simple NSView. + qCDebug(lcQpaWindow) << "Skipping NSWindow management for foreign window" << this; + + // We do however need to manage the parent relationship + if (parentCocoaWindow) + [parentCocoaWindow->m_view addSubview:m_view]; + else if (oldParentCocoaWindow) + [m_view removeFromSuperview]; + + return; + } + + const bool isEmbeddedView = isEmbedded(); + RecreationReasons recreateReason = RecreationNotNeeded; + if (parentWindow != oldParentCocoaWindow) recreateReason |= ParentChanged; @@ -1483,8 +1557,6 @@ void QCocoaWindow::recreateWindowIfNeeded() if (recreateReason == RecreationNotNeeded) return; - QCocoaWindow *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow); - // Remove current window (if any) if ((isContentView() && !shouldBeContentView) || (recreateReason & PanelChanged)) { if (m_nsWindow) { @@ -1514,31 +1586,10 @@ void QCocoaWindow::recreateWindowIfNeeded() } } - if (isEmbeddedView) { - // An embedded window doesn't have its own NSWindow. - } else if (!parentWindow) { - // QPlatformWindow subclasses must sync up with QWindow on creation: - propagateSizeHints(); - setWindowFlags(window()->flags()); - setWindowTitle(window()->title()); - setWindowFilePath(window()->filePath()); // Also sets window icon - setWindowState(window()->windowState()); - } else { + if (parentCocoaWindow) { // Child windows have no NSWindow, re-parent to superview instead [parentCocoaWindow->m_view addSubview:m_view]; - [m_view setHidden:!window()->isVisible()]; } - - const qreal opacity = qt_window_private(window())->opacity; - if (!qFuzzyCompare(opacity, qreal(1.0))) - setOpacity(opacity); - - setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); - - // top-level QWindows may have an attached NSToolBar, call - // update function which will attach to the NSWindow. - if (!parentWindow && !isEmbeddedView) - updateNSToolbar(); } void QCocoaWindow::requestUpdate() @@ -1547,7 +1598,10 @@ void QCocoaWindow::requestUpdate() << "using" << (updatesWithDisplayLink() ? "display-link" : "timer"); if (updatesWithDisplayLink()) { - static_cast<QCocoaScreen *>(screen())->requestUpdate(); + if (!static_cast<QCocoaScreen *>(screen())->requestUpdate()) { + qCDebug(lcQpaDrawing) << "Falling back to timer-based update request"; + QPlatformWindow::requestUpdate(); + } } else { // Fall back to the un-throttled timer-based callback QPlatformWindow::requestUpdate(); @@ -1573,6 +1627,75 @@ void QCocoaWindow::requestActivateWindow() [m_view.window makeKeyWindow]; } +/* + Closes all popups, and removes observers and monitors. +*/ +void QCocoaWindow::closeAllPopups() +{ + QGuiApplicationPrivate::instance()->closeAllPopups(); + + removePopupMonitor(); +} + +void QCocoaWindow::removePopupMonitor() +{ + if (s_globalMouseMonitor) { + [NSEvent removeMonitor:s_globalMouseMonitor]; + s_globalMouseMonitor = nil; + } + if (s_applicationActivationObserver) { + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:s_applicationActivationObserver]; + s_applicationActivationObserver = nil; + } +} + +void QCocoaWindow::setupPopupMonitor() +{ + // we open a popup window while we are not active. None of our existing event + // handlers will get called if the user now clicks anywhere outside the application + // or activates another window. Use a global event monitor to watch for mouse + // presses, and close popups. We also want mouse tracking in the popup to work, so + // also watch for MouseMoved. + if (!s_globalMouseMonitor) { + // we only get LeftMouseDown events when we also set LeftMouseUp. + constexpr NSEventMask mouseButtonMask = NSEventTypeLeftMouseDown | NSEventTypeLeftMouseUp + | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown + | NSEventMaskMouseMoved; + s_globalMouseMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mouseButtonMask + handler:^(NSEvent *e){ + if (!QGuiApplicationPrivate::instance()->popupActive()) { + removePopupMonitor(); + return; + } + const auto eventType = cocoaEvent2QtMouseEvent(e); + if (eventType == QEvent::MouseMove) { + if (s_windowUnderMouse) { + QWindow *window = s_windowUnderMouse->window(); + const auto button = cocoaButton2QtButton(e); + const auto buttons = currentlyPressedMouseButtons(); + const auto globalPoint = QCocoaScreen::mapFromNative(NSEvent.mouseLocation); + const auto localPoint = window->mapFromGlobal(globalPoint.toPoint()); + QWindowSystemInterface::handleMouseEvent(window, localPoint, globalPoint, + buttons, button, eventType); + } + } else { + closeAllPopups(); + } + }]; + } + // The activation observer also gets called when we become active because the user clicks + // into the popup. This should not close the popup, so QCocoaApplicationDelegate's + // applicationDidBecomeActive implementation removes this observer. + if (!s_applicationActivationObserver) { + s_applicationActivationObserver = [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserverForName:NSWorkspaceDidActivateApplicationNotification + object:nil queue:nil + usingBlock:^(NSNotification *){ + closeAllPopups(); + }]; + } +} + QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel) { QMacAutoReleasePool pool; @@ -1672,12 +1795,15 @@ QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel) // Qt::Tool windows hide on app deactivation, unless Qt::WA_MacAlwaysShowToolWindow is set nsWindow.hidesOnDeactivate = ((type & Qt::Tool) == Qt::Tool) && !alwaysShowToolWindow(); - // Make popup windows show on the same desktop as the parent full-screen window - nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary; + // Make popup windows show on the same desktop as the parent window + nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary + | NSWindowCollectionBehaviorMoveToActiveSpace; if ((type & Qt::Popup) == Qt::Popup) { nsWindow.hasShadow = YES; nsWindow.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; + if (QGuiApplication::applicationState() != Qt::ApplicationActive) + setupPopupMonitor(); } } @@ -1686,11 +1812,15 @@ QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel) applyContentBorderThickness(nsWindow); - if (QColorSpace colorSpace = format().colorSpace(); colorSpace.isValid()) { - NSData *iccData = colorSpace.iccProfile().toNSData(); - nsWindow.colorSpace = [[[NSColorSpace alloc] initWithICCProfileData:iccData] autorelease]; - qCDebug(lcQpaDrawing) << "Set" << this << "color space to" << nsWindow.colorSpace; - } + // We propagate the view's color space granulary to both the IOSurfaces + // used for QSurface::RasterSurface, as well as the CAMetalLayer used for + // QSurface::MetalSurface, but for QSurface::OpenGLSurface we don't have + // that option as we use NSOpenGLContext instead of CAOpenGLLayer. As a + // workaround we set the NSWindow's color space, which affects GL drawing + // with NSOpenGLContext as well. This does not conflict with the granular + // modifications we do to each surface for raster or Metal. + if (auto *qtView = qnsview_cast(m_view)) + nsWindow.colorSpace = qtView.colorSpace; return nsWindow; } @@ -1725,22 +1855,32 @@ void QCocoaWindow::setWindowCursor(NSCursor *cursor) if (isForeignWindow()) return; + qCInfo(lcQpaMouse) << "Setting" << this << "cursor to" << cursor; + QNSView *view = qnsview_cast(m_view); if (cursor == view.cursor) return; view.cursor = cursor; + // We're not using the the legacy cursor rects API to manage our + // cursor, but calling this function also invalidates AppKit's + // view of whether or not we need a cursorUpdate callback for + // our tracking area. [m_view.window invalidateCursorRectsForView:m_view]; - // There's a bug in AppKit where calling invalidateCursorRectsForView when - // there's an override cursor active (for example when hovering over the - // window frame), will not result in a cursorUpdate: callback. To work around - // this we synthesize a cursor update event and call the callback ourselves, - // if we detect that the mouse is currently over the view. + // We've informed AppKit that we need a cursorUpdate, but cursor + // updates for tracking areas are deferred in some cases, such as + // when the mouse is down, whereas we want a synchronous update. + // To ensure an updated cursor we synthesize a cursor update event + // now if the window is otherwise allowed to change the cursor. auto locationInWindow = m_view.window.mouseLocationOutsideOfEventStream; auto locationInSuperview = [m_view.superview convertPoint:locationInWindow fromView:nil]; - if ([m_view hitTest:locationInSuperview] == m_view) { + bool mouseIsOverView = [m_view hitTest:locationInSuperview] == m_view; + auto utilityMask = NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskTitled; + bool isUtilityWindow = (m_view.window.styleMask & utilityMask) == utilityMask; + if (mouseIsOverView && (m_view.window.keyWindow || isUtilityWindow)) { + qCDebug(lcQpaMouse) << "Synthesizing cursor update"; [m_view cursorUpdate:[NSEvent enterExitEventWithType:NSEventTypeCursorUpdate location:locationInWindow modifierFlags:0 timestamp:0 windowNumber:m_view.window.windowNumber context:nil @@ -1757,16 +1897,6 @@ void QCocoaWindow::registerTouch(bool enable) m_view.allowedTouchTypes &= ~NSTouchTypeMaskIndirect; } -void QCocoaWindow::setContentBorderThickness(int topThickness, int bottomThickness) -{ - m_topContentBorderThickness = topThickness; - m_bottomContentBorderThickness = bottomThickness; - bool enable = (topThickness > 0 || bottomThickness > 0); - m_drawContentBorderGradient = enable; - - applyContentBorderThickness(); -} - void QCocoaWindow::registerContentBorderArea(quintptr identifier, int upper, int lower) { m_contentBorderAreas.insert(identifier, BorderRange(identifier, upper, lower)); @@ -1803,7 +1933,7 @@ void QCocoaWindow::applyContentBorderThickness(NSWindow *window) // Find consecutive registered border areas, starting from the top. std::vector<BorderRange> ranges(m_contentBorderAreas.cbegin(), m_contentBorderAreas.cend()); std::sort(ranges.begin(), ranges.end()); - int effectiveTopContentBorderThickness = m_topContentBorderThickness; + int effectiveTopContentBorderThickness = 0; for (BorderRange range : ranges) { // Skip disiabled ranges (typically hidden tool bars) if (!m_enabledContentBorderAreas.value(range.identifier, false)) @@ -1818,7 +1948,7 @@ void QCocoaWindow::applyContentBorderThickness(NSWindow *window) break; } - int effectiveBottomContentBorderThickness = m_bottomContentBorderThickness; + int effectiveBottomContentBorderThickness = 0; [window setStyleMask:[window styleMask] | NSWindowStyleMaskTexturedBackground]; window.titlebarAppearsTransparent = YES; @@ -1840,21 +1970,6 @@ void QCocoaWindow::applyContentBorderThickness(NSWindow *window) [[[window contentView] superview] setNeedsDisplay:YES]; } -void QCocoaWindow::updateNSToolbar() -{ - if (!isContentView()) - return; - - NSToolbar *toolbar = QCocoaIntegration::instance()->toolbar(window()); - const NSWindow *window = m_view.window; - - if (window.toolbar == toolbar) - return; - - window.toolbar = toolbar; - window.showsToolbarButton = YES; -} - bool QCocoaWindow::testContentBorderAreaPosition(int position) const { if (!m_drawContentBorderGradient || !isContentView()) @@ -1874,7 +1989,7 @@ qreal QCocoaWindow::devicePixelRatio() const { // The documented way to observe the relationship between device-independent // and device pixels is to use one for the convertToBacking functions. Other - // methods such as [NSWindow backingScaleFacor] might not give the correct + // methods such as [NSWindow backingScaleFactor] might not give the correct // result, for example if setWantsBestResolutionOpenGLSurface is not set or // or ignored by the OpenGL driver. NSSize backingSize = [m_view convertSizeToBacking:NSMakeSize(1.0, 1.0)]; @@ -1901,6 +2016,22 @@ bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder() if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput)) return true; + // For application modal windows, as well as direct parent windows + // of window modal windows, AppKit takes care of blocking interaction. + // The Qt expectation however, is that all transient parents of a + // window modal window is blocked, as reflected by QGuiApplication. + // We reflect this by returning false from this function for transient + // parents blocked by a modal window, but limit it to the cases not + // covered by AppKit to avoid potential unwanted side effects. + QWindow *modalWindow = nullptr; + if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) { + if (modalWindow->modality() == Qt::WindowModal && modalWindow->transientParent() != window()) { + qCDebug(lcQpaWindow) << "Refusing key window for" << this << "due to being" + << "blocked by" << modalWindow; + return true; + } + } + if (m_inSetVisible) { QVariant showWithoutActivating = window()->property("_q_showWithoutActivating"); if (showWithoutActivating.isValid() && showWithoutActivating.toBool()) @@ -1910,6 +2041,20 @@ bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder() return false; } +bool QCocoaWindow::windowEvent(QEvent *event) +{ + switch (event->type()) { + case QEvent::WindowBlocked: + case QEvent::WindowUnblocked: + updateTitleBarButtons(window()->flags()); + break; + default: + break; + } + + return QPlatformWindow::windowEvent(event); +} + QPoint QCocoaWindow::bottomLeftClippedByNSWindowOffset() const { if (!m_view) @@ -1953,6 +2098,6 @@ QDebug operator<<(QDebug debug, const QCocoaWindow *window) } #endif // !QT_NO_DEBUG_STREAM -#include "moc_qcocoawindow.cpp" - QT_END_NAMESPACE + +#include "moc_qcocoawindow.cpp" diff --git a/src/plugins/platforms/cocoa/qmacclipboard.h b/src/plugins/platforms/cocoa/qmacclipboard.h index 658497f743..95267565f2 100644 --- a/src/plugins/platforms/cocoa/qmacclipboard.h +++ b/src/plugins/platforms/cocoa/qmacclipboard.h @@ -5,49 +5,57 @@ #define QMACCLIPBOARD_H #include <QtGui> -#include <QtGui/private/qmacmime_p.h> +#include <QtGui/qutimimeconverter.h> + +#include <QtCore/qpointer.h> #include <ApplicationServices/ApplicationServices.h> QT_BEGIN_NAMESPACE -class QMacMimeData; +class QUtiMimeConverter; + class QMacPasteboard { public: enum DataRequestType { EagerRequest, LazyRequest }; private: struct Promise { - Promise() : itemId(0), convertor(nullptr) { } + Promise() : itemId(0), converter(nullptr) { } - static Promise eagerPromise(int itemId, QMacInternalPasteboardMime *c, QString m, QMacMimeData *d, int o = 0); - static Promise lazyPromise(int itemId, QMacInternalPasteboardMime *c, QString m, QMacMimeData *d, int o = 0); - Promise(int itemId, QMacInternalPasteboardMime *c, QString m, QMacMimeData *md, int o, DataRequestType drt); + static Promise eagerPromise(int itemId, const QUtiMimeConverter *c, const QString &m, QMimeData *d, int o = 0); + static Promise lazyPromise(int itemId, const QUtiMimeConverter *c, const QString &m, QMimeData *d, int o = 0); + Promise(int itemId, const QUtiMimeConverter *c, const QString &m, QMimeData *md, int o, DataRequestType drt); int itemId, offset; - QMacInternalPasteboardMime *convertor; + const QUtiMimeConverter *converter; QString mime; - QPointer<QMacMimeData> mimeData; + QPointer<QMimeData> mimeData; QVariant variantData; DataRequestType dataRequestType; + // QMimeData can be set from QVariant, holding + // QPixmap. When converting, this triggers + // QPixmap's ctor which in turn requires QGuiApplication + // to exist and thus will abort the application + // abnormally if not. + bool isPixmap = false; }; QList<Promise> promises; PasteboardRef paste; - uchar mime_type; + const QUtiMimeConverter::HandlerScope scope; mutable QPointer<QMimeData> mime; mutable bool mac_mime_source; bool resolvingBeforeDestruction; static OSStatus promiseKeeper(PasteboardRef, PasteboardItemID, CFStringRef, void *); void clear_helper(); public: - QMacPasteboard(PasteboardRef p, uchar mime_type=0); - QMacPasteboard(uchar mime_type); - QMacPasteboard(CFStringRef name=nullptr, uchar mime_type=0); + QMacPasteboard(PasteboardRef p, QUtiMimeConverter::HandlerScope scope = QUtiMimeConverter::HandlerScopeFlag::All); + QMacPasteboard(QUtiMimeConverter::HandlerScope scope); + QMacPasteboard(CFStringRef name=nullptr, QUtiMimeConverter::HandlerScope scope = QUtiMimeConverter::HandlerScopeFlag::All); ~QMacPasteboard(); - bool hasFlavor(QString flavor) const; - bool hasOSType(int c_flavor) const; + bool hasUti(const QString &uti) const; PasteboardRef pasteBoard() const; QMimeData *mimeData() const; @@ -56,7 +64,7 @@ public: QStringList formats() const; bool hasFormat(const QString &format) const; - QVariant retrieveData(const QString &format, QMetaType) const; + QVariant retrieveData(const QString &format) const; void clear(); bool sync() const; diff --git a/src/plugins/platforms/cocoa/qmacclipboard.mm b/src/plugins/platforms/cocoa/qmacclipboard.mm index 86dbc9c3d0..edafa3b6a1 100644 --- a/src/plugins/platforms/cocoa/qmacclipboard.mm +++ b/src/plugins/platforms/cocoa/qmacclipboard.mm @@ -4,11 +4,15 @@ #include <AppKit/AppKit.h> #include "qmacclipboard.h" +#include <QtGui/private/qmacmimeregistry_p.h> +#include <QtGui/qutimimeconverter.h> #include <QtGui/qclipboard.h> #include <QtGui/qguiapplication.h> #include <QtGui/qbitmap.h> #include <QtCore/qdatetime.h> +#include <QtCore/qmetatype.h> #include <QtCore/qdebug.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/qguiapplication.h> #include <QtGui/qevent.h> #include <QtCore/qurl.h> @@ -50,45 +54,47 @@ private: QMacMimeData(); }; -QMacPasteboard::Promise::Promise(int itemId, QMacInternalPasteboardMime *c, QString m, QMacMimeData *md, int o, DataRequestType drt) - : itemId(itemId), offset(o), convertor(c), mime(m), dataRequestType(drt) +QMacPasteboard::Promise::Promise(int itemId, const QUtiMimeConverter *c, const QString &m, QMimeData *md, int o, DataRequestType drt) + : itemId(itemId), offset(o), converter(c), mime(m), dataRequestType(drt) { // Request the data from the application immediately for eager requests. if (dataRequestType == QMacPasteboard::EagerRequest) { - variantData = md->variantData(m); + variantData = static_cast<QMacMimeData *>(md)->variantData(m); + isPixmap = variantData.metaType().id() == QMetaType::QPixmap; mimeData = nullptr; } else { mimeData = md; + if (md->hasImage()) + isPixmap = md->imageData().metaType().id() == QMetaType::QPixmap; } } -QMacPasteboard::QMacPasteboard(PasteboardRef p, uchar mt) +QMacPasteboard::QMacPasteboard(PasteboardRef p, QUtiMimeConverter::HandlerScope scope) + : scope(scope) { mac_mime_source = false; - mime_type = mt ? mt : uchar(QMacInternalPasteboardMime::MIME_ALL); paste = p; CFRetain(paste); resolvingBeforeDestruction = false; } -QMacPasteboard::QMacPasteboard(uchar mt) +QMacPasteboard::QMacPasteboard(QUtiMimeConverter::HandlerScope scope) + : scope(scope) { mac_mime_source = false; - mime_type = mt ? mt : uchar(QMacInternalPasteboardMime::MIME_ALL); paste = nullptr; OSStatus err = PasteboardCreate(nullptr, &paste); - if (err == noErr) { + if (err == noErr) PasteboardSetPromiseKeeper(paste, promiseKeeper, this); - } else { + else qDebug("PasteBoard: Error creating pasteboard: [%d]", (int)err); - } resolvingBeforeDestruction = false; } -QMacPasteboard::QMacPasteboard(CFStringRef name, uchar mt) +QMacPasteboard::QMacPasteboard(CFStringRef name, QUtiMimeConverter::HandlerScope scope) + : scope(scope) { mac_mime_source = false; - mime_type = mt ? mt : uchar(QMacInternalPasteboardMime::MIME_ALL); paste = nullptr; OSStatus err = PasteboardCreate(name, &paste); if (err == noErr) { @@ -105,128 +111,91 @@ QMacPasteboard::~QMacPasteboard() Commit all promises for paste when shutting down, unless we are the stack-allocated clipboard used by QCocoaDrag. */ - if (mime_type == QMacInternalPasteboardMime::MIME_DND) + if (scope == QUtiMimeConverter::HandlerScopeFlag::DnD) resolvingBeforeDestruction = true; PasteboardResolvePromises(paste); if (paste) CFRelease(paste); } -PasteboardRef -QMacPasteboard::pasteBoard() const +PasteboardRef QMacPasteboard::pasteBoard() const { return paste; } -OSStatus QMacPasteboard::promiseKeeper(PasteboardRef paste, PasteboardItemID id, CFStringRef flavor, void *_qpaste) +OSStatus QMacPasteboard::promiseKeeper(PasteboardRef paste, PasteboardItemID id, + CFStringRef uti, void *_qpaste) { QMacPasteboard *qpaste = (QMacPasteboard*)_qpaste; const long promise_id = (long)id; // Find the kept promise - QList<QMacInternalPasteboardMime*> availableConverters - = QMacInternalPasteboardMime::all(QMacInternalPasteboardMime::MIME_ALL); - const QString flavorAsQString = QString::fromCFString(flavor); + const QList<QUtiMimeConverter*> availableConverters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All); + const QString utiAsQString = QString::fromCFString(uti); QMacPasteboard::Promise promise; for (int i = 0; i < qpaste->promises.size(); i++){ - QMacPasteboard::Promise tmp = qpaste->promises[i]; - if (!availableConverters.contains(tmp.convertor)) { + const QMacPasteboard::Promise tmp = qpaste->promises[i]; + if (!availableConverters.contains(tmp.converter)) { // promise.converter is a pointer initialized by the value found - // in QMacInternalPasteboardMime's global list of QMacInternalPasteboardMimes. - // We add pointers to this list in QMacInternalPasteboardMime's ctor; - // we remove these pointers in QMacInternalPasteboardMime's dtor. + // in QUtiMimeConverter's global list of QMacMimes. + // We add pointers to this list in QUtiMimeConverter's ctor; + // we remove these pointers in QUtiMimeConverter's dtor. // If tmp.converter was not found in this list, we probably have a // dangling pointer so let's skip it. continue; } - if (tmp.itemId == promise_id && tmp.convertor->canConvert(tmp.mime, flavorAsQString)){ + if (tmp.itemId == promise_id && tmp.converter->canConvert(tmp.mime, utiAsQString)) { promise = tmp; break; } } - if (!promise.itemId && flavorAsQString == "com.trolltech.qt.MimeTypeName"_L1) { + if (!promise.itemId && utiAsQString == "com.trolltech.qt.MimeTypeName"_L1) { // we have promised this data, but won't be able to convert, so return null data. // This helps in making the application/x-qt-mime-type-name hidden from normal use. QByteArray ba; - QCFType<CFDataRef> data = CFDataCreate(nullptr, (UInt8*)ba.constData(), ba.size()); - PasteboardPutItemFlavor(paste, id, flavor, data, kPasteboardFlavorNoFlags); + const QCFType<CFDataRef> data = CFDataCreate(nullptr, (UInt8*)ba.constData(), ba.size()); + PasteboardPutItemFlavor(paste, id, uti, data, kPasteboardFlavorNoFlags); return noErr; } if (!promise.itemId) { // There was no promise that could deliver data for the - // given id and flavor. This should not happen. - qDebug("Pasteboard: %d: Request for %ld, %s, but no promise found!", __LINE__, promise_id, qPrintable(flavorAsQString)); + // given id and uti. This should not happen. + qDebug("Pasteboard: %d: Request for %ld, %s, but no promise found!", __LINE__, promise_id, qPrintable(utiAsQString)); return cantGetFlavorErr; } - qCDebug(lcQpaClipboard, "PasteBoard: Calling in promise for %s[%ld] [%s] (%s) [%d]", qPrintable(promise.mime), promise_id, - qPrintable(flavorAsQString), qPrintable(promise.convertor->convertorName()), promise.offset); + qCDebug(lcQpaClipboard, "PasteBoard: Calling in promise for %s[%ld] [%s] [%d]", qPrintable(promise.mime), promise_id, + qPrintable(utiAsQString), promise.offset); // Get the promise data. If this is a "lazy" promise call variantData() // to request the data from the application. QVariant promiseData; if (promise.dataRequestType == LazyRequest) { - if (!qpaste->resolvingBeforeDestruction && !promise.mimeData.isNull()) - promiseData = promise.mimeData->variantData(promise.mime); + if (!qpaste->resolvingBeforeDestruction && !promise.mimeData.isNull()) { + if (promise.isPixmap && !QGuiApplication::instance()) { + qCWarning(lcQpaClipboard, + "Cannot keep promise, data contains QPixmap and requires livining QGuiApplication"); + return cantGetFlavorErr; + } + promiseData = static_cast<QMacMimeData *>(promise.mimeData.data())->variantData(promise.mime); + } } else { promiseData = promise.variantData; } - QList<QByteArray> md = promise.convertor->convertFromMime(promise.mime, promiseData, flavorAsQString); + const QList<QByteArray> md = promise.converter->convertFromMime(promise.mime, promiseData, utiAsQString); if (md.size() <= promise.offset) return cantGetFlavorErr; const QByteArray &ba = md[promise.offset]; - QCFType<CFDataRef> data = CFDataCreate(nullptr, (UInt8*)ba.constData(), ba.size()); - PasteboardPutItemFlavor(paste, id, flavor, data, kPasteboardFlavorNoFlags); + const QCFType<CFDataRef> data = CFDataCreate(nullptr, (UInt8*)ba.constData(), ba.size()); + PasteboardPutItemFlavor(paste, id, uti, data, kPasteboardFlavorNoFlags); return noErr; } -bool -QMacPasteboard::hasOSType(int c_flavor) const -{ - if (!paste) - return false; - - sync(); - - ItemCount cnt = 0; - if (PasteboardGetItemCountSafe(paste, &cnt) || !cnt) - return false; - - qCDebug(lcQpaClipboard, "PasteBoard: hasOSType [%c%c%c%c]", (c_flavor>>24)&0xFF, (c_flavor>>16)&0xFF, - (c_flavor>>8)&0xFF, (c_flavor>>0)&0xFF); - for (uint index = 1; index <= cnt; ++index) { - - PasteboardItemID id; - if (PasteboardGetItemIdentifier(paste, index, &id) != noErr) - return false; - - QCFType<CFArrayRef> types; - if (PasteboardCopyItemFlavors(paste, id, &types ) != noErr) - return false; - - const int type_count = CFArrayGetCount(types); - for (int i = 0; i < type_count; ++i) { - CFStringRef flavor = (CFStringRef)CFArrayGetValueAtIndex(types, i); - CFStringRef preferredTag = UTTypeCopyPreferredTagWithClass(flavor, kUTTagClassOSType); - const int os_flavor = UTGetOSTypeFromString(preferredTag); - if (preferredTag) - CFRelease(preferredTag); - if (os_flavor == c_flavor) { - qCDebug(lcQpaClipboard, " - Found!"); - return true; - } - } - } - qCDebug(lcQpaClipboard, " - NotFound!"); - return false; -} - -bool -QMacPasteboard::hasFlavor(QString c_flavor) const +bool QMacPasteboard::hasUti(const QString &uti) const { if (!paste) return false; @@ -237,7 +206,8 @@ QMacPasteboard::hasFlavor(QString c_flavor) const if (PasteboardGetItemCountSafe(paste, &cnt) || !cnt) return false; - qCDebug(lcQpaClipboard, "PasteBoard: hasFlavor [%s]", qPrintable(c_flavor)); + qCDebug(lcQpaClipboard, "PasteBoard: hasUti [%s]", qPrintable(uti)); + const QCFString c_uti(uti); for (uint index = 1; index <= cnt; ++index) { PasteboardItemID id; @@ -245,7 +215,7 @@ QMacPasteboard::hasFlavor(QString c_flavor) const return false; PasteboardFlavorFlags flags; - if (PasteboardGetItemFlavorFlags(paste, id, QCFString(c_flavor), &flags) == noErr) { + if (PasteboardGetItemFlavorFlags(paste, id, c_uti, &flags) == noErr) { qCDebug(lcQpaClipboard, " - Found!"); return true; } @@ -254,17 +224,20 @@ QMacPasteboard::hasFlavor(QString c_flavor) const return false; } -class QMacPasteboardMimeSource : public QMimeData { +class QMacPasteboardMimeSource : public QMimeData +{ const QMacPasteboard *paste; public: QMacPasteboardMimeSource(const QMacPasteboard *p) : QMimeData(), paste(p) { } ~QMacPasteboardMimeSource() { } - virtual QStringList formats() const { return paste->formats(); } - virtual QVariant retrieveData(const QString &format, QMetaType type) const { return paste->retrieveData(format, type); } + QStringList formats() const override { return paste->formats(); } + QVariant retrieveData(const QString &format, QMetaType) const override + { + return paste->retrieveData(format); + } }; -QMimeData -*QMacPasteboard::mimeData() const +QMimeData *QMacPasteboard::mimeData() const { if (!mime) { mac_mime_source = true; @@ -274,8 +247,7 @@ QMimeData return mime; } -void -QMacPasteboard::setMimeData(QMimeData *mime_src, DataRequestType dataRequestType) +void QMacPasteboard::setMimeData(QMimeData *mime_src, DataRequestType dataRequestType) { if (!paste) return; @@ -286,7 +258,7 @@ QMacPasteboard::setMimeData(QMimeData *mime_src, DataRequestType dataRequestType delete mime; mime = mime_src; - QList<QMacInternalPasteboardMime*> availableConverters = QMacInternalPasteboardMime::all(mime_type); + const QList<QUtiMimeConverter*> availableConverters = QMacMimeRegistry::all(scope); if (mime != nullptr) { clear_helper(); QStringList formats = mime_src->formats(); @@ -297,35 +269,31 @@ QMacPasteboard::setMimeData(QMimeData *mime_src, DataRequestType dataRequestType QString dummyMimeType("application/x-qt-mime-type-name"_L1); if (!formats.contains(dummyMimeType)) { QByteArray dummyType = mime_src->data(dummyMimeType); - if (!dummyType.isEmpty()) { + if (!dummyType.isEmpty()) formats.append(dummyMimeType); - } } - for (int f = 0; f < formats.size(); ++f) { - QString mimeType = formats.at(f); - for (QList<QMacInternalPasteboardMime *>::Iterator it = availableConverters.begin(); it != availableConverters.end(); ++it) { - QMacInternalPasteboardMime *c = (*it); + for (const auto &mimeType : formats) { + for (const auto *c : availableConverters) { // Hack: The Rtf handler converts incoming Rtf to Html. We do // not want to convert outgoing Html to Rtf but instead keep // posting it as Html. Skip the Rtf handler here. - if (c->convertorName() == "Rtf"_L1) + if (c->utiForMime("text/html"_L1) == "public.rtf"_L1) continue; - QString flavor(c->flavorFor(mimeType)); - if (!flavor.isEmpty()) { - QMacMimeData *mimeData = static_cast<QMacMimeData*>(mime_src); + const QString uti(c->utiForMime(mimeType)); + if (!uti.isEmpty()) { - int numItems = c->count(mime_src); + const int numItems = c->count(mime_src); for (int item = 0; item < numItems; ++item) { - const NSInteger itemID = item+1; //id starts at 1 + const NSInteger itemID = item + 1; //id starts at 1 //QMacPasteboard::Promise promise = (dataRequestType == QMacPasteboard::EagerRequest) ? // QMacPasteboard::Promise::eagerPromise(itemID, c, mimeType, mimeData, item) : // QMacPasteboard::Promise::lazyPromise(itemID, c, mimeType, mimeData, item); - QMacPasteboard::Promise promise(itemID, c, mimeType, mimeData, item, dataRequestType); + const QMacPasteboard::Promise promise(itemID, c, mimeType, mime_src, item, dataRequestType); promises.append(promise); - PasteboardPutItemFlavor(paste, reinterpret_cast<PasteboardItemID>(itemID), QCFString(flavor), 0, kPasteboardFlavorNoFlags); - qCDebug(lcQpaClipboard, " - adding %ld %s [%s] <%s> [%d]", - itemID, qPrintable(mimeType), qPrintable(flavor), qPrintable(c->convertorName()), item); + PasteboardPutItemFlavor(paste, reinterpret_cast<PasteboardItemID>(itemID), QCFString(uti), 0, kPasteboardFlavorNoFlags); + qCDebug(lcQpaClipboard, " - adding %ld %s [%s] [%d]", + itemID, qPrintable(mimeType), qPrintable(uti), item); } } } @@ -333,8 +301,7 @@ QMacPasteboard::setMimeData(QMimeData *mime_src, DataRequestType dataRequestType } } -QStringList -QMacPasteboard::formats() const +QStringList QMacPasteboard::formats() const { if (!paste) return QStringList(); @@ -359,11 +326,11 @@ QMacPasteboard::formats() const const int type_count = CFArrayGetCount(types); for (int i = 0; i < type_count; ++i) { - const QString flavor = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(types, i)); - qCDebug(lcQpaClipboard, " -%s", qPrintable(QString(flavor))); - QString mimeType = QMacInternalPasteboardMime::flavorToMime(mime_type, flavor); + const QString uti = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(types, i)); + qCDebug(lcQpaClipboard, " -%s", qPrintable(QString(uti))); + const QString mimeType = QMacMimeRegistry::flavorToMime(scope, uti); if (!mimeType.isEmpty() && !ret.contains(mimeType)) { - qCDebug(lcQpaClipboard, " -<%lld> %s [%s]", ret.size(), qPrintable(mimeType), qPrintable(QString(flavor))); + qCDebug(lcQpaClipboard, " -<%lld> %s [%s]", ret.size(), qPrintable(mimeType), qPrintable(QString(uti))); ret << mimeType; } } @@ -371,8 +338,7 @@ QMacPasteboard::formats() const return ret; } -bool -QMacPasteboard::hasFormat(const QString &format) const +bool QMacPasteboard::hasFormat(const QString &format) const { if (!paste) return false; @@ -396,9 +362,9 @@ QMacPasteboard::hasFormat(const QString &format) const const int type_count = CFArrayGetCount(types); for (int i = 0; i < type_count; ++i) { - const QString flavor = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(types, i)); - qCDebug(lcQpaClipboard, " -%s [0x%x]", qPrintable(QString(flavor)), mime_type); - QString mimeType = QMacInternalPasteboardMime::flavorToMime(mime_type, flavor); + const QString uti = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(types, i)); + qCDebug(lcQpaClipboard, " -%s [0x%x]", qPrintable(uti), uchar(scope)); + QString mimeType = QMacMimeRegistry::flavorToMime(scope, uti); if (!mimeType.isEmpty()) qCDebug(lcQpaClipboard, " - %s", qPrintable(mimeType)); if (mimeType == format) @@ -408,8 +374,7 @@ QMacPasteboard::hasFormat(const QString &format) const return false; } -QVariant -QMacPasteboard::retrieveData(const QString &format, QMetaType) const +QVariant QMacPasteboard::retrieveData(const QString &format) const { if (!paste) return QVariant(); @@ -421,18 +386,17 @@ QMacPasteboard::retrieveData(const QString &format, QMetaType) const return QByteArray(); qCDebug(lcQpaClipboard, "Pasteboard: retrieveData [%s]", qPrintable(format)); - const QList<QMacInternalPasteboardMime *> mimes = QMacInternalPasteboardMime::all(mime_type); - for (int mime = 0; mime < mimes.size(); ++mime) { - QMacInternalPasteboardMime *c = mimes.at(mime); - QString c_flavor = c->flavorFor(format); - if (!c_flavor.isEmpty()) { + const QList<QUtiMimeConverter *> availableConverters = QMacMimeRegistry::all(scope); + for (const auto *c : availableConverters) { + const QString c_uti = c->utiForMime(format); + if (!c_uti.isEmpty()) { // Converting via PasteboardCopyItemFlavorData below will for some UITs result // in newlines mapping to '\r' instead of '\n'. To work around this we shortcut // the conversion via NSPasteboard's NSStringPboardType if possible. - if (c_flavor == "com.apple.traditional-mac-plain-text"_L1 - || c_flavor == "public.utf8-plain-text"_L1 - || c_flavor == "public.utf16-plain-text"_L1) { - QString str = qt_mac_get_pasteboardString(paste); + if (c_uti == "com.apple.traditional-mac-plain-text"_L1 + || c_uti == "public.utf8-plain-text"_L1 + || c_uti == "public.utf16-plain-text"_L1) { + const QString str = qt_mac_get_pasteboardString(paste); if (!str.isEmpty()) return str; } @@ -450,26 +414,29 @@ QMacPasteboard::retrieveData(const QString &format, QMetaType) const const int type_count = CFArrayGetCount(types); for (int i = 0; i < type_count; ++i) { - CFStringRef flavor = static_cast<CFStringRef>(CFArrayGetValueAtIndex(types, i)); - if (c_flavor == QString::fromCFString(flavor)) { + const CFStringRef uti = static_cast<CFStringRef>(CFArrayGetValueAtIndex(types, i)); + if (c_uti == QString::fromCFString(uti)) { QCFType<CFDataRef> macBuffer; - if (PasteboardCopyItemFlavorData(paste, id, flavor, &macBuffer) == noErr) { - QByteArray buffer((const char *)CFDataGetBytePtr(macBuffer), CFDataGetLength(macBuffer)); + if (PasteboardCopyItemFlavorData(paste, id, uti, &macBuffer) == noErr) { + QByteArray buffer((const char *)CFDataGetBytePtr(macBuffer), + CFDataGetLength(macBuffer)); if (!buffer.isEmpty()) { - qCDebug(lcQpaClipboard, " - %s [%s] (%s)", qPrintable(format), qPrintable(c_flavor), qPrintable(c->convertorName())); + qCDebug(lcQpaClipboard, " - %s [%s]", qPrintable(format), + qPrintable(c_uti)); buffer.detach(); //detach since we release the macBuffer retList.append(buffer); break; //skip to next element } } } else { - qCDebug(lcQpaClipboard, " - NoMatch %s [%s] (%s)", qPrintable(c_flavor), qPrintable(QString::fromCFString(flavor)), qPrintable(c->convertorName())); + qCDebug(lcQpaClipboard, " - NoMatch %s [%s]", qPrintable(c_uti), + qPrintable(QString::fromCFString(uti))); } } } if (!retList.isEmpty()) { - ret = c->convertToMime(format, retList, c_flavor); + ret = c->convertToMime(format, retList, c_uti); return ret; } } @@ -484,15 +451,13 @@ void QMacPasteboard::clear_helper() promises.clear(); } -void -QMacPasteboard::clear() +void QMacPasteboard::clear() { qCDebug(lcQpaClipboard, "PasteBoard: clear!"); clear_helper(); } -bool -QMacPasteboard::sync() const +bool QMacPasteboard::sync() const { if (!paste) return false; diff --git a/src/plugins/platforms/cocoa/qmultitouch_mac.mm b/src/plugins/platforms/cocoa/qmultitouch_mac.mm index 73d103c5e3..f818b04acd 100644 --- a/src/plugins/platforms/cocoa/qmultitouch_mac.mm +++ b/src/plugins/platforms/cocoa/qmultitouch_mac.mm @@ -12,10 +12,10 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -QHash<qint64, QCocoaTouch*> QCocoaTouch::_currentTouches; -QHash<quint64, QPointingDevice*> QCocoaTouch::_touchDevices; -QPointF QCocoaTouch::_screenReferencePos; -QPointF QCocoaTouch::_trackpadReferencePos; +Q_CONSTINIT QHash<qint64, QCocoaTouch*> QCocoaTouch::_currentTouches; +Q_CONSTINIT QHash<quint64, QPointingDevice*> QCocoaTouch::_touchDevices; +Q_CONSTINIT QPointF QCocoaTouch::_screenReferencePos; +Q_CONSTINIT QPointF QCocoaTouch::_trackpadReferencePos; int QCocoaTouch::_idAssignmentCount = 0; int QCocoaTouch::_touchCount = 0; bool QCocoaTouch::_updateInternalStateOnly = true; diff --git a/src/plugins/platforms/cocoa/qnsview.h b/src/plugins/platforms/cocoa/qnsview.h index 1858c2660d..7f845a5c3b 100644 --- a/src/plugins/platforms/cocoa/qnsview.h +++ b/src/plugins/platforms/cocoa/qnsview.h @@ -4,6 +4,8 @@ #ifndef QNSVIEW_H #define QNSVIEW_H +#include <AppKit/NSView.h> + #include <QtCore/private/qcore_mac_p.h> QT_BEGIN_NAMESPACE @@ -30,6 +32,12 @@ QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QNSView, NSView - (void)cancelComposingText; @end +Q_FORWARD_DECLARE_OBJC_CLASS(NSColorSpace); + +@interface QNSView (DrawingAPI) +@property (nonatomic, readonly) NSColorSpace *colorSpace; +@end + @interface QNSView (QtExtras) @property (nonatomic, readonly) QCocoaWindow *platformWindow; @end diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index dd0f1d6511..48ffa5c1cc 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -5,6 +5,7 @@ #include <AppKit/AppKit.h> #include <MetalKit/MetalKit.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> #include "qnsview.h" #include "qcocoawindow.h" @@ -21,6 +22,7 @@ #include <QtCore/QPointer> #include <QtCore/QSet> #include <QtCore/qsysinfo.h> +#include <QtCore/private/qcore_mac_p.h> #include <QtGui/QAccessible> #include <QtGui/QImage> #include <private/qguiapplication_p.h> @@ -33,13 +35,7 @@ #include "qcocoaglcontext.h" #endif #include "qcocoaintegration.h" - -// Private interface -@interface QNSView () -- (BOOL)isTransparentForUserInput; -@property (assign) NSView* previousSuperview; -@property (assign) NSWindow* previousWindow; -@end +#include <QtGui/private/qmacmimeregistry_p.h> @interface QNSView (Drawing) <CALayerDelegate> - (void)initDrawing; @@ -81,6 +77,21 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); @end @interface QNSView (ComplexText) <NSTextInputClient> +@property (readonly) QObject* focusObject; +@end + +@interface QT_MANGLE_NAMESPACE(QNSViewMenuHelper) : NSObject +- (instancetype)initWithView:(QNSView *)theView; +@end +QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); + +// Private interface +@interface QNSView () +- (BOOL)isTransparentForUserInput; +@property (assign) NSView* previousSuperview; +@property (assign) NSWindow* previousWindow; +@property (retain) QNSViewMenuHelper* menuHelper; +@property (nonatomic, retain) NSColorSpace *colorSpace; @end @implementation QNSView { @@ -100,19 +111,30 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); // Keys bool m_lastKeyDead; bool m_sendKeyEvent; + bool m_sendKeyEventWithoutText; NSEvent *m_currentlyInterpretedKeyEvent; QSet<quint32> m_acceptedKeyDowns; // Text QString m_composingText; QPointer<QObject> m_composingFocusObject; + NSDraggingContext m_lastSeenContext; } +@synthesize colorSpace = m_colorSpace; + - (instancetype)initWithCocoaWindow:(QCocoaWindow *)platformWindow { if ((self = [super initWithFrame:NSZeroRect])) { m_platformWindow = platformWindow; + // NSViews are by default visible, but QWindows are not. + // We should ideally pick up the actual QWindow state here, + // but QWindowPrivate::setVisible() expects to control the + // order of events tightly, so we need to wait for a call + // to QCocoaWindow::setVisible(). + self.hidden = YES; + self.focusRingType = NSFocusRingTypeNone; self.previousSuperview = nil; @@ -127,6 +149,9 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); m_lastKeyDead = false; m_sendKeyEvent = false; m_currentlyInterpretedKeyEvent = nil; + m_lastSeenContext = NSDraggingContextWithinApplication; + + self.menuHelper = [[[QNSViewMenuHelper alloc] initWithView:self] autorelease]; } return self; } @@ -252,15 +277,29 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); return focusWindow; } +/* + Invoked when the view is hidden, either directly, + or in response to an ancestor being hidden. +*/ - (void)viewDidHide { + qCDebug(lcQpaWindow) << "Did hide" << self; + if (!m_platformWindow->isExposed()) return; m_platformWindow->handleExposeEvent(QRegion()); +} + +/* + Invoked when the view is unhidden, either directly, + or in response to an ancestor being unhidden. +*/ +- (void)viewDidUnhide +{ + qCDebug(lcQpaWindow) << "Did unhide" << self; - // Note: setNeedsDisplay is automatically called for - // viewDidUnhide so no reason to override it here. + [self setNeedsDisplay:YES]; } - (BOOL)isTransparentForUserInput @@ -293,7 +332,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); // QWindow activation from QCocoaWindow::windowDidBecomeKey instead. The only // exception is if the window can never become key, in which case we naturally // cannot wait for that to happen. - QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>( + QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>( [self topLevelWindow], Qt::ActiveWindowFocusReason); } diff --git a/src/plugins/platforms/cocoa/qnsview_accessibility.mm b/src/plugins/platforms/cocoa/qnsview_accessibility.mm index 3f3898fd18..e781f21a6c 100644 --- a/src/plugins/platforms/cocoa/qnsview_accessibility.mm +++ b/src/plugins/platforms/cocoa/qnsview_accessibility.mm @@ -13,6 +13,15 @@ @implementation QNSView (Accessibility) +- (void)activateQtAccessibility +{ + // Activate the Qt accessibility machinery for all entry points + // below that may be triggered by system accessibility queries, + // as otherwise Qt is not aware that the system needs to know + // about all accessibility state changes in Qt. + QCocoaIntegration::instance()->accessibility()->setActive(true); +} + - (id)childAccessibleElement { QCocoaWindow *platformWindow = self.platformWindow; @@ -32,8 +41,7 @@ - (id)accessibilityAttributeValue:(NSString *)attribute { - // activate accessibility updates - QCocoaIntegration::instance()->accessibility()->setActive(true); + [self activateQtAccessibility]; if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) return NSAccessibilityUnignoredChildrenForOnlyChild([self childAccessibleElement]); @@ -43,11 +51,13 @@ - (id)accessibilityHitTest:(NSPoint)point { + [self activateQtAccessibility]; return [[self childAccessibleElement] accessibilityHitTest:point]; } - (id)accessibilityFocusedUIElement { + [self activateQtAccessibility]; return [[self childAccessibleElement] accessibilityFocusedUIElement]; } diff --git a/src/plugins/platforms/cocoa/qnsview_complextext.mm b/src/plugins/platforms/cocoa/qnsview_complextext.mm index 4be8126299..d7f8f4baf0 100644 --- a/src/plugins/platforms/cocoa/qnsview_complextext.mm +++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm @@ -7,6 +7,17 @@ // ------------- Text insertion ------------- +- (QObject*)focusObject +{ + // The text input system may still hold a reference to our QNSView, + // even after QCocoaWindow has been destructed, delivering text input + // events to us, so we need to guard for this situation explicitly. + if (!m_platformWindow) + return nullptr; + + return m_platformWindow->window()->focusObject(); +} + /* Inserts the given text, potentially replacing existing text. @@ -52,8 +63,7 @@ } } - QObject *focusObject = m_platformWindow->window()->focusObject(); - if (queryInputMethod(focusObject)) { + if (queryInputMethod(self.focusObject)) { QInputMethodEvent inputMethodEvent; const bool isAttributedString = [text isKindOfClass:NSAttributedString.class]; @@ -75,7 +85,7 @@ inputMethodEvent.setCommitString(commitString, replaceFrom, replaceLength); } - QCoreApplication::sendEvent(focusObject, &inputMethodEvent); + QCoreApplication::sendEvent(self.focusObject, &inputMethodEvent); } m_composingText.clear(); @@ -86,6 +96,9 @@ { Q_UNUSED(sender); + if (!m_platformWindow) + return; + // Depending on the input method, pressing enter may // result in simply dismissing the input method editor, // without confirming the composition. In other cases @@ -112,9 +125,14 @@ KeyEvent newlineEvent(m_currentlyInterpretedKeyEvent ? m_currentlyInterpretedKeyEvent : NSApp.currentEvent); newlineEvent.type = QEvent::KeyPress; - newlineEvent.key = Qt::Key_Return; - newlineEvent.text = QLatin1Char(kReturnCharCode); - newlineEvent.nativeVirtualKey = kVK_Return; + + const bool isEnter = newlineEvent.modifiers & Qt::KeypadModifier; + newlineEvent.key = isEnter ? Qt::Key_Enter : Qt::Key_Return; + newlineEvent.text = isEnter ? QLatin1Char(kEnterCharCode) + : QLatin1Char(kReturnCharCode); + newlineEvent.nativeVirtualKey = isEnter ? quint32(kVK_ANSI_KeypadEnter) + : quint32(kVK_Return); + qCDebug(lcQpaKeys) << "Inserting newline via" << newlineEvent; newlineEvent.sendWindowSystemEvent(m_platformWindow->window()); } @@ -237,7 +255,7 @@ // Update the composition, now that we've computed the replacement range m_composingText = preeditString; - if (QObject *focusObject = m_platformWindow->window()->focusObject()) { + if (QObject *focusObject = self.focusObject) { m_composingFocusObject = focusObject; if (queryInputMethod(focusObject)) { QInputMethodEvent event(preeditString, preeditAttributes); @@ -279,8 +297,7 @@ */ - (NSRange)markedRange { - QObject *focusObject = m_platformWindow->window()->focusObject(); - if (auto queryResult = queryInputMethod(focusObject, Qt::ImAbsolutePosition)) { + if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImAbsolutePosition)) { int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt(); // The cursor position as reflected by Qt::ImAbsolutePosition is not @@ -315,7 +332,7 @@ << "for focus object" << m_composingFocusObject; if (!m_composingText.isEmpty()) { - QObject *focusObject = m_platformWindow->window()->focusObject(); + QObject *focusObject = self.focusObject; if (queryInputMethod(focusObject)) { QInputMethodEvent e; e.setCommitString(m_composingText); @@ -362,8 +379,20 @@ // pass the originating key event up the responder chain if applicable. qCDebug(lcQpaKeys) << "Trying to perform command" << selector; - if (![self tryToPerform:selector with:self]) + if (![self tryToPerform:selector with:self]) { m_sendKeyEvent = true; + + if (![NSStringFromSelector(selector) hasPrefix:@"insert"]) { + // The text input system determined that the key event was not + // meant for text insertion, and instead asked us to treat it + // as a (possibly noop) command. This typically happens for key + // events with either ⌘ or ⌃, function keys such as F1-F35, + // arrow keys, etc. We reflect that when sending the key event + // later on, by removing the text from the event, so that the + // event does not result in text insertion on the client side. + m_sendKeyEventWithoutText = true; + } + } } // ------------- Various text properties ------------- @@ -376,8 +405,7 @@ */ - (NSRange)selectedRange { - QObject *focusObject = m_platformWindow->window()->focusObject(); - if (auto queryResult = queryInputMethod(focusObject, + if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition)) { // Unfortunately the Qt::InputMethodQuery values are all relative @@ -424,8 +452,7 @@ */ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - QObject *focusObject = m_platformWindow->window()->focusObject(); - if (auto queryResult = queryInputMethod(focusObject, + if (auto queryResult = queryInputMethod(self.focusObject, Qt::ImAbsolutePosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor)) { const int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt(); const QString textBeforeCursor = queryResult.value(Qt::ImTextBeforeCursor).toString(); @@ -461,8 +488,8 @@ Q_UNUSED(range); Q_UNUSED(actualRange); - QWindow *window = m_platformWindow->window(); - if (queryInputMethod(window->focusObject())) { + QWindow *window = m_platformWindow ? m_platformWindow->window() : nullptr; + if (window && queryInputMethod(window->focusObject())) { QRect cursorRect = qApp->inputMethod()->cursorRectangle().toRect(); cursorRect.moveBottomLeft(window->mapToGlobal(cursorRect.bottomLeft())); return QCocoaScreen::mapToNative(cursorRect); @@ -478,6 +505,32 @@ return NSNotFound; } +/* + Returns the window level of the text input. + + This allows the input method to place its input panel + above the text input. +*/ +- (NSInteger)windowLevel +{ + // The default level assumed by input methods is NSFloatingWindowLevel, + // but our NSWindow level could be higher than that for many reasons, + // including being set via QWindow::setFlags() or directly on the + // NSWindow, or because we're embedded into a native view hierarchy. + // Return the actual window level to account for this. + auto level = m_platformWindow ? m_platformWindow->nativeWindow().level + : NSNormalWindowLevel; + + // The logic above only covers our own window though. In some cases, + // such as when a completer is active, the text input has a lower + // window level than another window that's also visible, and we don't + // want the input panel to be sandwiched between these two windows. + // Account for this by explicitly using NSPopUpMenuWindowLevel as + // the minimum window level, which corresponds to the highest level + // one can get via QWindow::setFlags(), except for Qt::ToolTip. + return qMax(level, NSPopUpMenuWindowLevel); +} + // ------------- Helper functions ------------- /* diff --git a/src/plugins/platforms/cocoa/qnsview_dragging.mm b/src/plugins/platforms/cocoa/qnsview_dragging.mm index c88e5d502f..4f7d35a0d6 100644 --- a/src/plugins/platforms/cocoa/qnsview_dragging.mm +++ b/src/plugins/platforms/cocoa/qnsview_dragging.mm @@ -16,12 +16,12 @@ NSPasteboardTypeRTF, NSPasteboardTypeTabularText, NSPasteboardTypeFont, NSPasteboardTypeRuler, NSFileContentsPboardType, NSPasteboardTypeRTFD , NSPasteboardTypeHTML, - NSPasteboardTypeURL, NSPasteboardTypePDF, (NSString *)kUTTypeVCard, - (NSString *)kPasteboardTypeFileURLPromise, (NSString *)kUTTypeInkText, + NSPasteboardTypeURL, NSPasteboardTypePDF, UTTypeVCard.identifier, + (NSString *)kPasteboardTypeFileURLPromise, NSPasteboardTypeMultipleTextSelection, mimeTypeGeneric]]; // Add custom types supported by the application - for (const QString &customType : qt_mac_enabledDraggedTypes()) + for (const QString &customType : QMacMimeRegistry::enabledDraggedTypes()) [supportedTypes addObject:customType.toNSString()]; [self registerForDraggedTypes:supportedTypes]; @@ -45,8 +45,8 @@ static QPoint mapWindowCoordinates(QWindow *source, QWindow *target, QPoint poin - (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context { Q_UNUSED(session); - Q_UNUSED(context); + m_lastSeenContext = context; QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag(); return qt_mac_mapDropActions(nativeDrag->currentDrag()->supportedActions()); } @@ -61,8 +61,11 @@ static QPoint mapWindowCoordinates(QWindow *source, QWindow *target, QPoint poin // // Since Qt already takes care of tracking the keyboard modifiers, we // don't need (or want) Cocoa to filter anything. Instead, we'll let - // the application do the actual filtering. - return YES; + // the application do the actual filtering. But only while dragging + // within application, otherwise ignored modifiers may end up in a + // wrong drop operation executed. + + return m_lastSeenContext == NSDraggingContextWithinApplication; } - (BOOL)wantsPeriodicDraggingUpdates @@ -250,6 +253,8 @@ static QPoint mapWindowCoordinates(QWindow *source, QWindow *target, QPoint poin Q_UNUSED(screenPoint); Q_UNUSED(operation); + m_lastSeenContext = NSDraggingContextWithinApplication; + if (!m_platformWindow) return; diff --git a/src/plugins/platforms/cocoa/qnsview_drawing.mm b/src/plugins/platforms/cocoa/qnsview_drawing.mm index 472a5291e7..bf102e43f8 100644 --- a/src/plugins/platforms/cocoa/qnsview_drawing.mm +++ b/src/plugins/platforms/cocoa/qnsview_drawing.mm @@ -13,6 +13,14 @@ << " QT_MAC_WANTS_LAYER/_q_mac_wantsLayer has no effect."; } + // Pick up and persist requested color space from surface format + const QSurfaceFormat surfaceFormat = m_platformWindow->format(); + if (QColorSpace colorSpace = surfaceFormat.colorSpace(); colorSpace.isValid()) { + NSData *iccData = colorSpace.iccProfile().toNSData(); + self.colorSpace = [[[NSColorSpace alloc] initWithICCProfileData:iccData] autorelease]; + } + + // Trigger creation of the layer self.wantsLayer = YES; } @@ -28,6 +36,12 @@ return YES; } +- (NSColorSpace*)colorSpace +{ + // If no explicit color space was set, use the NSWindow's color space + return m_colorSpace ? m_colorSpace : self.window.colorSpace; +} + // ----------------------- Layer setup ----------------------- - (BOOL)shouldUseMetalLayer @@ -93,12 +107,7 @@ [super setLayer:layer]; - // When adding a view to a view hierarchy the backing properties will change - // which results in updating the contents scale, but in case of switching the - // layer on a view that's already in a view hierarchy we need to manually ensure - // the scale is up to date. - if (self.superview) - [self updateLayerContentsScale]; + [self propagateBackingProperties]; if (self.opaque && lcQpaDrawing().isDebugEnabled()) { // If the view claims to be opaque we expect it to fill the entire @@ -131,8 +140,7 @@ { qCDebug(lcQpaDrawing) << "Backing properties changed for" << self; - if (self.layer) - [self updateLayerContentsScale]; + [self propagateBackingProperties]; // Ideally we would plumb this situation through QPA in a way that lets // clients invalidate their own caches, recreate QBackingStore, etc. @@ -141,8 +149,11 @@ [self setNeedsDisplay:YES]; } -- (void)updateLayerContentsScale +- (void)propagateBackingProperties { + if (!self.layer) + return; + // We expect clients to fill the layer with retina aware content, // based on the devicePixelRatio of the QWindow, so we set the // layer's content scale to match that. By going via devicePixelRatio @@ -153,6 +164,12 @@ auto devicePixelRatio = m_platformWindow->devicePixelRatio(); qCDebug(lcQpaDrawing) << "Updating" << self.layer << "content scale to" << devicePixelRatio; self.layer.contentsScale = devicePixelRatio; + + if ([self.layer isKindOfClass:CAMetalLayer.class]) { + CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(self.layer); + metalLayer.colorspace = self.colorSpace.CGColorSpace; + qCDebug(lcQpaDrawing) << "Set" << metalLayer << "color space to" << metalLayer.colorspace; + } } /* @@ -178,8 +195,10 @@ - (void)drawRect:(NSRect)dirtyBoundingRect { Q_UNUSED(dirtyBoundingRect); - Q_ASSERT_X(!self.layer, "QNSView", - "The drawRect code path should not be hit when we are layer backed"); + // As we are layer backed we shouldn't really end up here, but AppKit will + // in some cases call this method just because we implement it. + // FIXME: Remove drawRect and switch from displayLayer to updateLayer + qCWarning(lcQpaDrawing) << "[QNSView drawRect] called for layer backed view"; } /* diff --git a/src/plugins/platforms/cocoa/qnsview_keys.mm b/src/plugins/platforms/cocoa/qnsview_keys.mm index 88e94f2c1c..abee622e65 100644 --- a/src/plugins/platforms/cocoa/qnsview_keys.mm +++ b/src/plugins/platforms/cocoa/qnsview_keys.mm @@ -3,8 +3,63 @@ // This file is included from qnsview.mm, and only used to organize the code +/* + Determines if the text represents one of the "special keys" on macOS + + As a legacy from OpenStep, macOS reserves the range 0xF700-0xF8FF of the + Unicode private use area for representing function keys on the keyboard: + + http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT + + https://developer.apple.com/documentation/appkit/nsevent/specialkey + + These code points are not supposed to have any glyphs associated with them, + but since we can't guarantee that the system doesn't have a font that does + provide glyphs for this range (Arial Unicode MS e.g.) we need to filter + the text of our key events up front. +*/ +static bool isSpecialKey(const QString &text) +{ + if (text.length() != 1) + return false; + + const char16_t unicode = text.at(0).unicode(); + if (unicode >= 0xF700 && unicode <= 0xF8FF) + return true; + + return false; +} + +static bool sendAsShortcut(const KeyEvent &keyEvent, QWindow *window) +{ + KeyEvent shortcutEvent = keyEvent; + shortcutEvent.type = QEvent::Shortcut; + qCDebug(lcQpaKeys) << "Trying potential shortcuts in" << window + << "for" << shortcutEvent; + + if (shortcutEvent.sendWindowSystemEvent(window)) { + qCDebug(lcQpaKeys) << "Found matching shortcut; will not send as key event"; + return true; + } + qCDebug(lcQpaKeys) << "No matching shortcuts; continuing with key event delivery"; + return false; +} + @implementation QNSView (Keys) +- (bool)performKeyEquivalent:(NSEvent *)nsevent +{ + // Implemented to handle shortcuts for modified Tab keys, which are + // handled by Cocoa and not delivered to your keyDown implementation. + if (nsevent.type == NSEventTypeKeyDown && m_composingText.isEmpty()) { + const bool ctrlDown = [nsevent modifierFlags] & NSEventModifierFlagControl; + const bool isTabKey = nsevent.keyCode == kVK_Tab; + if (ctrlDown && isTabKey && sendAsShortcut(KeyEvent(nsevent), [self topLevelWindow])) + return YES; + } + return NO; +} + - (bool)handleKeyEvent:(NSEvent *)nsevent { qCDebug(lcQpaKeys) << "Handling" << nsevent; @@ -16,23 +71,20 @@ // We will send a key event unless the input method handles it QBoolBlocker sendKeyEventGuard(m_sendKeyEvent, true); + // Assume we should send key events with text, unless told + // otherwise by doCommandBySelector. + m_sendKeyEventWithoutText = false; + + bool didInterpretKeyEvent = false; + if (keyEvent.type == QEvent::KeyPress) { if (m_composingText.isEmpty()) { - KeyEvent shortcutEvent = keyEvent; - shortcutEvent.type = QEvent::Shortcut; - qCDebug(lcQpaKeys) << "Trying potential shortcuts in" << window - << "for" << shortcutEvent; - - if (shortcutEvent.sendWindowSystemEvent(window)) { - qCDebug(lcQpaKeys) << "Found matching shortcut; will not send as key event"; + if (sendAsShortcut(keyEvent, window)) return true; - } else { - qCDebug(lcQpaKeys) << "No matching shortcuts; continuing with key event delivery"; - } } - QObject *focusObject = m_platformWindow->window()->focusObject(); + QObject *focusObject = m_platformWindow ? m_platformWindow->window()->focusObject() : nullptr; if (m_sendKeyEvent && focusObject) { if (auto queryResult = queryInputMethod(focusObject, Qt::ImHints)) { auto hints = static_cast<Qt::InputMethodHints>(queryResult.value(Qt::ImHints).toUInt()); @@ -61,8 +113,12 @@ qCDebug(lcQpaKeys) << "Interpreting key event for focus object" << focusObject; m_currentlyInterpretedKeyEvent = nsevent; - [self interpretKeyEvents:@[nsevent]]; + if (![self.inputContext handleEvent:nsevent]) { + qCDebug(lcQpaKeys) << "Input context did not consume event"; + m_sendKeyEvent = true; + } m_currentlyInterpretedKeyEvent = 0; + didInterpretKeyEvent = true; // If the last key we sent was dead, then pass the next // key to the IM as well to complete composition. @@ -75,7 +131,10 @@ bool accepted = true; if (m_sendKeyEvent && m_composingText.isEmpty()) { - KeyEvent keyEvent(nsevent); + // Trust text input system on whether to send the event with text or not, + // or otherwise apply heuristics to filter out private use symbols. + if (didInterpretKeyEvent ? m_sendKeyEventWithoutText : isSpecialKey(keyEvent.text)) + keyEvent.text = {}; qCDebug(lcQpaKeys) << "Sending as" << keyEvent; accepted = keyEvent.sendWindowSystemEvent(window); } @@ -207,9 +266,16 @@ KeyEvent::KeyEvent(NSEvent *nsevent) default: break; // Must be manually set } - if (nsevent.type == NSEventTypeKeyDown || nsevent.type == NSEventTypeKeyUp) { + switch (nsevent.type) { + case NSEventTypeKeyDown: + case NSEventTypeKeyUp: + case NSEventTypeFlagsChanged: nativeVirtualKey = nsevent.keyCode; + default: + break; + } + if (nsevent.type == NSEventTypeKeyDown || nsevent.type == NSEventTypeKeyUp) { NSString *charactersIgnoringModifiers = nsevent.charactersIgnoringModifiers; NSString *characters = nsevent.characters; @@ -229,11 +295,7 @@ KeyEvent::KeyEvent(NSEvent *nsevent) key = QAppleKeyMapper::fromCocoaKey(character); } - // Ignore text for the U+F700-U+F8FF range. This is used by Cocoa when - // delivering function keys (e.g. arrow keys, backspace, F1-F35, etc.) - if (!(modifiers & (Qt::ControlModifier | Qt::MetaModifier)) - && (character.unicode() < 0xf700 || character.unicode() > 0xf8ff)) - text = QString::fromNSString(characters); + text = QString::fromNSString(characters); isRepeat = nsevent.ARepeat; } @@ -250,10 +312,9 @@ bool KeyEvent::sendWindowSystemEvent(QWindow *window) const case QEvent::KeyPress: case QEvent::KeyRelease: { static const int count = 1; - static const bool tryShortcutOverride = false; QWindowSystemInterface::handleExtendedKeyEvent(window, timestamp, type, key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers, - text, isRepeat, count, tryShortcutOverride); + text, isRepeat, count); // FIXME: Make handleExtendedKeyEvent synchronous return QWindowSystemInterface::flushWindowSystemEvents(); } diff --git a/src/plugins/platforms/cocoa/qnsview_menus.mm b/src/plugins/platforms/cocoa/qnsview_menus.mm index 7f2b3d387c..2840936975 100644 --- a/src/plugins/platforms/cocoa/qnsview_menus.mm +++ b/src/plugins/platforms/cocoa/qnsview_menus.mm @@ -9,22 +9,56 @@ #include "qcocoamenu.h" #include "qcocoamenubar.h" -static bool selectorIsCutCopyPaste(SEL selector) +@implementation QNSView (Menus) + +// Qt does not (yet) have a mechanism for propagating generic actions, +// so we can only support actions that originate from a QCocoaNSMenuItem, +// where we can forward the action by emitting QPlatformMenuItem::activated(). +// But waiting for forwardInvocation to check that the sender is a +// QCocoaNSMenuItem is too late, as AppKit has at that point chosen +// our view as the target for the action, and if we can't handle it +// the action will not propagate up the responder chain as it should. +// Instead, we hook in early in the process of determining the target +// via the supplementalTargetForAction API, and if we can support the +// action we forward it to a helper. The helper must be tied to the +// view, as the menu validation logic depends on the view's state. + +- (id)supplementalTargetForAction:(SEL)action sender:(id)sender { - return (selector == @selector(cut:) - || selector == @selector(copy:) - || selector == @selector(paste:) - || selector == @selector(selectAll:)); + qCDebug(lcQpaMenus) << "Resolving action target for" << action << "from" << sender << "via" << self; + + if (qt_objc_cast<QCocoaNSMenuItem *>(sender)) { + // The supplemental target must support the selector, but we + // determine so dynamically, so check here before continuing. + if ([self.menuHelper respondsToSelector:action]) + return self.menuHelper; + } else { + qCDebug(lcQpaMenus) << "Ignoring action for menu item we didn't create"; + } + + return [super supplementalTargetForAction:action sender:sender]; } -@interface QNSView (Menus) -- (void)qt_itemFired:(QCocoaNSMenuItem *)item; @end -@implementation QNSView (Menus) +@interface QNSViewMenuHelper () +@property (assign) QNSView* view; +@end + +@implementation QNSViewMenuHelper + +- (instancetype)initWithView:(QNSView *)theView +{ + if ((self = [super init])) + self.view = theView; + + return self; +} - (BOOL)validateMenuItem:(NSMenuItem*)item { + qCDebug(lcQpaMenus) << "Validating" << item << "for" << self.view; + auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item); if (!nativeItem) return item.enabled; // FIXME Test with with Qt as plugin or embedded QWindow. @@ -51,7 +85,7 @@ static bool selectorIsCutCopyPaste(SEL selector) } if ((!menuWindow || menuWindow->window() != QGuiApplication::modalWindow()) - && (!menubar || menubar->cocoaWindow() != self.platformWindow)) + && (!menubar || menubar->cocoaWindow() != self.view.platformWindow)) return NO; } @@ -60,41 +94,42 @@ static bool selectorIsCutCopyPaste(SEL selector) - (BOOL)respondsToSelector:(SEL)selector { - // Not exactly true. Both copy: and selectAll: can work on non key views. - if (selectorIsCutCopyPaste(selector)) - return ([NSApp keyWindow] == self.window) && (self.window.firstResponder == self); + // See QCocoaMenuItem::resolveTargetAction() + + if (selector == @selector(cut:) + || selector == @selector(copy:) + || selector == @selector(paste:) + || selector == @selector(selectAll:)) { + // Not exactly true. Both copy: and selectAll: can work on non key views. + return NSApp.keyWindow == self.view.window + && self.view.window.firstResponder == self.view; + } - return [super respondsToSelector:selector]; -} + if (selector == @selector(qt_itemFired:)) + return YES; -- (void)qt_itemFired:(QCocoaNSMenuItem *)item -{ - auto *appDelegate = [QCocoaApplicationDelegate sharedDelegate]; - [appDelegate qt_itemFired:item]; + return [super respondsToSelector:selector]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { - if (selectorIsCutCopyPaste(selector)) { - NSMethodSignature *itemFiredSign = [super methodSignatureForSelector:@selector(qt_itemFired:)]; - return itemFiredSign; - } + // Double check, in case something has cached that we respond + // to the selector, but the result has changed since then. + if (![self respondsToSelector:selector]) + return nil; - return [super methodSignatureForSelector:selector]; + auto *appDelegate = [QCocoaApplicationDelegate sharedDelegate]; + return [appDelegate methodSignatureForSelector:@selector(qt_itemFired:)]; } - (void)forwardInvocation:(NSInvocation *)invocation { - if (selectorIsCutCopyPaste(invocation.selector)) { - NSObject *sender; - [invocation getArgument:&sender atIndex:2]; - if (auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(sender)) { - [self qt_itemFired:nativeItem]; - return; - } - } - - [super forwardInvocation:invocation]; + NSObject *sender; + [invocation getArgument:&sender atIndex:2]; + qCDebug(lcQpaMenus) << "Forwarding" << invocation.selector << "from" << sender; + Q_ASSERT(qt_objc_cast<QCocoaNSMenuItem *>(sender)); + invocation.selector = @selector(qt_itemFired:); + [invocation invokeWithTarget:[QCocoaApplicationDelegate sharedDelegate]]; } @end diff --git a/src/plugins/platforms/cocoa/qnsview_mouse.mm b/src/plugins/platforms/cocoa/qnsview_mouse.mm index 89125ca91f..2fd57fe68e 100644 --- a/src/plugins/platforms/cocoa/qnsview_mouse.mm +++ b/src/plugins/platforms/cocoa/qnsview_mouse.mm @@ -209,20 +209,19 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) case NSEventTypeOtherMouseUp: return QEvent::NonClientAreaMouseButtonRelease; + case NSEventTypeMouseMoved: case NSEventTypeLeftMouseDragged: case NSEventTypeRightMouseDragged: case NSEventTypeOtherMouseDragged: return QEvent::NonClientAreaMouseMove; default: - break; + Q_UNREACHABLE(); } - - return QEvent::None; }(); qCInfo(lcQpaMouse) << eventType << "at" << qtWindowPoint << "with" << m_frameStrutButtons << "in" << self.window; - QWindowSystemInterface::handleFrameStrutMouseEvent(m_platformWindow->window(), + QWindowSystemInterface::handleMouseEvent(m_platformWindow->window(), timestamp, qtWindowPoint, qtScreenPoint, m_frameStrutButtons, button, eventType); } @end @@ -484,10 +483,6 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) - (void)cursorUpdate:(NSEvent *)theEvent { - // Note: We do not get this callback when moving from a subview that - // uses the legacy cursorRect API, so the cursor is reset to the arrow - // cursor. See rdar://34183708 - if (!NSApp.active) return; @@ -548,6 +543,30 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) [self handleMouseEvent: theEvent]; } +- (BOOL)shouldPropagateMouseEnterExit +{ + Q_ASSERT(m_platformWindow); + + // We send out enter and leave events mainly from mouse move events (mouseMovedImpl), + // but in some case (see mouseEnteredImpl:) we also want to propagate enter/leave + // events from the platform. We only do this for windows that themselves are not + // handled by another parent QWindow. + + if (m_platformWindow->isContentView()) + return true; + + // Windows manually embedded into a native view does not have a QWindow parent + if (m_platformWindow->isEmbedded()) + return true; + + // Windows embedded via fromWinId do, but the parent isn't a QNSView + QPlatformWindow *parentWindow = m_platformWindow->QPlatformWindow::parent(); + if (parentWindow && parentWindow->isForeignWindow()) + return true; + + return false; +} + - (void)mouseEnteredImpl:(NSEvent *)theEvent { Q_UNUSED(theEvent); @@ -571,8 +590,7 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) // in time (s_windowUnderMouse). The latter is also used to also send out enter/leave // events when the application is activated/deactivated. - // Root (top level or embedded) windows generate enter events for sub-windows - if (!m_platformWindow->isContentView() && !m_platformWindow->isEmbedded()) + if (![self shouldPropagateMouseEnterExit]) return; QPointF windowPoint; @@ -598,8 +616,7 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) if (!m_platformWindow) return; - // Root (top level or embedded) windows generate enter events for sub-windows - if (!m_platformWindow->isContentView() && !m_platformWindow->isEmbedded()) + if (![self shouldPropagateMouseEnterExit]) return; QCocoaWindow *windowToLeave = QCocoaWindow::s_windowUnderMouse; diff --git a/src/plugins/platforms/cocoa/qnsview_touch.mm b/src/plugins/platforms/cocoa/qnsview_touch.mm index 6a147701fc..97ed5b7624 100644 --- a/src/plugins/platforms/cocoa/qnsview_touch.mm +++ b/src/plugins/platforms/cocoa/qnsview_touch.mm @@ -25,7 +25,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesBeganWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } - (void)touchesMovedWithEvent:(NSEvent *)event @@ -36,7 +39,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesMovedWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } - (void)touchesEndedWithEvent:(NSEvent *)event @@ -47,7 +53,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesEndedWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } - (void)touchesCancelledWithEvent:(NSEvent *)event @@ -58,7 +67,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesCancelledWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } @end diff --git a/src/plugins/platforms/cocoa/qnswindow.h b/src/plugins/platforms/cocoa/qnswindow.h index ae43f28e8e..8f842eba85 100644 --- a/src/plugins/platforms/cocoa/qnswindow.h +++ b/src/plugins/platforms/cocoa/qnswindow.h @@ -8,6 +8,9 @@ #include <QPointer> #include <QtCore/private/qcore_mac_p.h> +#include <AppKit/NSWindow.h> +#include <AppKit/NSPanel.h> + QT_FORWARD_DECLARE_CLASS(QCocoaWindow) #if defined(__OBJC__) @@ -33,6 +36,8 @@ QT_FORWARD_DECLARE_CLASS(QCocoaWindow) typedef NSWindow<QNSWindowProtocol> QCocoaNSWindow; +QCocoaNSWindow *qnswindow_cast(NSWindow *window); + #else class QCocoaNSWindow; #endif // __OBJC__ diff --git a/src/plugins/platforms/cocoa/qnswindow.mm b/src/plugins/platforms/cocoa/qnswindow.mm index cce76e4b20..74ba6f65ac 100644 --- a/src/plugins/platforms/cocoa/qnswindow.mm +++ b/src/plugins/platforms/cocoa/qnswindow.mm @@ -58,6 +58,15 @@ static bool isMouseEvent(NSEvent *ev) } @end + +NSWindow<QNSWindowProtocol> *qnswindow_cast(NSWindow *window) +{ + if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) + return static_cast<QCocoaNSWindow *>(window); + else + return nil; +} + @implementation QNSWindow #define QNSWINDOW_PROTOCOL_IMPLMENTATION 1 #include "qnswindow.mm" @@ -98,9 +107,10 @@ static bool isMouseEvent(NSEvent *ev) continue; if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) { - QCocoaWindow *cocoaWindow = static_cast<QCocoaNSWindow *>(window).platformWindow; - window.level = notification.name == NSApplicationWillResignActiveNotification ? - NSNormalWindowLevel : cocoaWindow->windowLevel(cocoaWindow->window()->flags()); + if (QCocoaWindow *cocoaWindow = static_cast<QCocoaNSWindow *>(window).platformWindow) { + window.level = notification.name == NSApplicationWillResignActiveNotification ? + NSNormalWindowLevel : cocoaWindow->windowLevel(cocoaWindow->window()->flags()); + } } // The documentation says that "when a window enters a new level, it’s ordered @@ -167,45 +177,6 @@ static bool isMouseEvent(NSEvent *ev) } @end -#if !defined(QT_APPLE_NO_PRIVATE_APIS) -// When creating an NSWindow the worksWhenModal function is queried, -// and the resulting state is used to set the corresponding window tag, -// which the window server uses to determine whether or not the window -// should be allowed to activate via mouse clicks in the title-bar. -// Unfortunately, prior to macOS 10.15, this window tag was never -// updated after the initial assignment in [NSWindow _commonAwake], -// which meant that windows that dynamically change their worksWhenModal -// state will behave as if they were never allowed to work when modal. -// We work around this by manually updating the window tag when needed. - -typedef uint32_t CGSConnectionID; -typedef uint32_t CGSWindowID; - -extern "C" { -CGSConnectionID CGSMainConnectionID() __attribute__((weak_import)); -OSStatus CGSSetWindowTags(const CGSConnectionID, const CGSWindowID, int *, int) __attribute__((weak_import)); -OSStatus CGSClearWindowTags(const CGSConnectionID, const CGSWindowID, int *, int) __attribute__((weak_import)); -} - -@interface QNSPanel (WorksWhenModalWindowTagWorkaround) @end -@implementation QNSPanel (WorksWhenModalWindowTagWorkaround) -- (void)setWorksWhenModal:(BOOL)worksWhenModal -{ - [super setWorksWhenModal:worksWhenModal]; - - if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSCatalina) { - if (CGSMainConnectionID && CGSSetWindowTags && CGSClearWindowTags) { - static int kWorksWhenModalWindowTag = 0x40; - auto *function = worksWhenModal ? CGSSetWindowTags : CGSClearWindowTags; - function(CGSMainConnectionID(), self.windowNumber, &kWorksWhenModalWindowTag, 64); - } else { - qWarning() << "Missing APIs for window tag handling, can not update worksWhenModal state"; - } - } -} -@end -#endif // QT_APPLE_NO_PRIVATE_APIS - #else // QNSWINDOW_PROTOCOL_IMPLMENTATION // The following content is mixed in to the QNSWindow and QNSPanel classes via includes @@ -235,6 +206,29 @@ OSStatus CGSClearWindowTags(const CGSConnectionID, const CGSWindowID, int *, int return m_platformWindow; } +- (void)setContentView:(NSView*)view +{ + [super setContentView:view]; + + if (!qnsview_cast(self.contentView)) + return; + + // Now that we're the content view, we can apply the properties of + // the QWindow. We do this here, instead of in init, so that we can + // use the same code paths for setting these properties during + // NSWindow initialization as we do when setting them later on. + const QWindow *window = m_platformWindow->window(); + qCDebug(lcQpaWindow) << "Reflecting" << window << "state to" << self; + + m_platformWindow->propagateSizeHints(); + m_platformWindow->setWindowFlags(window->flags()); + m_platformWindow->setWindowTitle(window->title()); + m_platformWindow->setWindowFilePath(window->filePath()); // Also sets window icon + m_platformWindow->setWindowState(window->windowState()); + m_platformWindow->setOpacity(window->opacity()); + m_platformWindow->setVisible(window->isVisible()); +} + - (NSString *)description { NSMutableString *description = [NSMutableString stringWithString:[super description]]; @@ -296,8 +290,13 @@ OSStatus CGSClearWindowTags(const CGSConnectionID, const CGSWindowID, int *, int // we assume that if you have translucent content, without a // frame then you intend to do all background drawing yourself. const QWindow *window = m_platformWindow ? m_platformWindow->window() : nullptr; - if (!self.opaque && window && window->flags().testFlag(Qt::FramelessWindowHint)) - return [NSColor clearColor]; + if (!self.opaque && window) { + // Qt::Popup also requires clearColor - in qmacstyle + // we fill background using a special path with rounded corners. + if (window->flags().testFlag(Qt::FramelessWindowHint) + || (window->flags() & Qt::WindowType_Mask) == Qt::Popup) + return [NSColor clearColor]; + } // This still allows you to have translucent content with a frame, // where the system background (or color set via NSWindow) will diff --git a/src/plugins/platforms/cocoa/qnswindowdelegate.mm b/src/plugins/platforms/cocoa/qnswindowdelegate.mm index 2d90fbf544..1db7772771 100644 --- a/src/plugins/platforms/cocoa/qnswindowdelegate.mm +++ b/src/plugins/platforms/cocoa/qnswindowdelegate.mm @@ -23,9 +23,7 @@ static inline bool isWhiteSpace(const QString &s) static QCocoaWindow *toPlatformWindow(NSWindow *window) { - if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) - return static_cast<QCocoaNSWindow *>(window).platformWindow; - return nullptr; + return qnswindow_cast(window).platformWindow; } @implementation QNSWindowDelegate diff --git a/src/plugins/platforms/direct2d/CMakeLists.txt b/src/plugins/platforms/direct2d/CMakeLists.txt index b88ac617aa..54e96b09e5 100644 --- a/src/plugins/platforms/direct2d/CMakeLists.txt +++ b/src/plugins/platforms/direct2d/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from direct2d.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QWindowsDirect2DIntegrationPlugin Plugin: @@ -11,17 +12,17 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin ../windows/qtwindowsglobal.h ../windows/qwin10helpers.cpp ../windows/qwin10helpers.h ../windows/qwindowsapplication.cpp ../windows/qwindowsapplication.h - ../windows/qwindowscombase.h ../windows/qwindowscontext.cpp ../windows/qwindowscontext.h ../windows/qwindowscursor.cpp ../windows/qwindowscursor.h ../windows/qwindowsdialoghelpers.cpp ../windows/qwindowsdialoghelpers.h ../windows/qwindowsdropdataobject.cpp ../windows/qwindowsdropdataobject.h + ../windows/qwindowsiconengine.cpp ../windows/qwindowsiconengine.h ../windows/qwindowsinputcontext.cpp ../windows/qwindowsinputcontext.h ../windows/qwindowsintegration.cpp ../windows/qwindowsintegration.h ../windows/qwindowsinternalmimedata.cpp ../windows/qwindowsinternalmimedata.h ../windows/qwindowskeymapper.cpp ../windows/qwindowskeymapper.h ../windows/qwindowsmenu.cpp ../windows/qwindowsmenu.h - ../windows/qwindowsmime.cpp ../windows/qwindowsmime.h + ../windows/qwindowsmimeregistry.cpp ../windows/qwindowsmimeregistry.h ../windows/qwindowsmousehandler.cpp ../windows/qwindowsmousehandler.h ../windows/qwindowsnativeinterface.cpp ../windows/qwindowsnativeinterface.h ../windows/qwindowsole.cpp ../windows/qwindowsole.h @@ -44,6 +45,8 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin qwindowsdirect2dplatformpixmap.cpp qwindowsdirect2dplatformpixmap.h qwindowsdirect2dplatformplugin.cpp qwindowsdirect2dwindow.cpp qwindowsdirect2dwindow.h + NO_UNITY_BUILD_SOURCES + ../windows/qwindowspointerhandler.cpp DEFINES QT_NO_CAST_FROM_ASCII QT_NO_FOREACH @@ -55,16 +58,17 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin Qt::Gui Qt::GuiPrivate advapi32 - d2d1 # special case + d2d1 d3d11 dwmapi - dwrite # special case + dwrite dxgi dxguid gdi32 imm32 ole32 oleaut32 + setupapi shell32 shlwapi user32 @@ -93,15 +97,9 @@ qt_internal_add_resource(QWindowsDirect2DIntegrationPlugin "openglblacklists" ${openglblacklists_resource_files} ) -#### Keys ignored in scope 1:.:.:direct2d.pro:<TRUE>: -# OTHER_FILES = "direct2d.json" - ## Scopes: ##################################################################### -#### Keys ignored in scope 2:.:.:direct2d.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" - qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION QT_FEATURE_opengl AND NOT QT_FEATURE_dynamicgl LIBRARIES opengl32 @@ -110,6 +108,8 @@ qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION QT_FEATURE qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION MINGW LIBRARIES uuid + NO_PCH_SOURCES + ../windows/qwindowspointerhandler.cpp ) qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION QT_FEATURE_opengl @@ -188,6 +188,7 @@ endif() qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION QT_FEATURE_accessibility SOURCES + ../windows/uiautomation/qwindowsuiautomation.cpp ../windows/uiautomation/qwindowsuiautomation.h ../windows/uiautomation/qwindowsuiaaccessibility.cpp ../windows/uiautomation/qwindowsuiaaccessibility.h ../windows/uiautomation/qwindowsuiabaseprovider.cpp ../windows/uiautomation/qwindowsuiabaseprovider.h ../windows/uiautomation/qwindowsuiaexpandcollapseprovider.cpp ../windows/uiautomation/qwindowsuiaexpandcollapseprovider.h @@ -209,13 +210,18 @@ qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION QT_FEATURE ../windows/uiautomation/qwindowsuiawindowprovider.cpp ../windows/uiautomation/qwindowsuiawindowprovider.h ) +if(QT_FEATURE_accessibility) + find_library(UI_AUTOMATION_LIBRARY uiautomationcore) + if(UI_AUTOMATION_LIBRARY) + qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin + LIBRARIES + ${UI_AUTOMATION_LIBRARY} + ) + endif() +endif() + qt_internal_extend_target(QWindowsDirect2DIntegrationPlugin CONDITION MINGW AND QT_FEATURE_accessibility LIBRARIES uuid ) -# begin special case -if (MINGW) - set_source_files_properties(../windows/qwindowspointerhandler.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON) -endif() -# end special case diff --git a/src/plugins/platforms/direct2d/qwindowsdirect2dpaintengine.cpp b/src/plugins/platforms/direct2d/qwindowsdirect2dpaintengine.cpp index bed06dc62d..bc304f78be 100644 --- a/src/plugins/platforms/direct2d/qwindowsdirect2dpaintengine.cpp +++ b/src/plugins/platforms/direct2d/qwindowsdirect2dpaintengine.cpp @@ -1651,7 +1651,7 @@ void QWindowsDirect2DPaintEngine::rasterFill(const QVectorPath &path, const QBru p.setPen(state()->pen); auto *extended = static_cast<QPaintEngineEx *>(engine); - for (const QPainterClipInfo &info : qAsConst(state()->clipInfo)) { + for (const QPainterClipInfo &info : std::as_const(state()->clipInfo)) { extended->state()->matrix = info.matrix; extended->transformChanged(); diff --git a/src/plugins/platforms/directfb/CMakeLists.txt b/src/plugins/platforms/directfb/CMakeLists.txt index c634afb4f8..7222168a37 100644 --- a/src/plugins/platforms/directfb/CMakeLists.txt +++ b/src/plugins/platforms/directfb/CMakeLists.txt @@ -1,9 +1,8 @@ -# Generated from directfb.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -# begin special case: qt_find_package(DirectFB) qt_find_package(EGL) -# end special case: ##################################################################### ## QDirectFbIntegrationPlugin Plugin: @@ -25,16 +24,13 @@ qt_internal_add_plugin(QDirectFbIntegrationPlugin qdirectfbwindow.cpp qdirectfbwindow.h LIBRARIES PkgConfig::DirectFB - EGL::EGL # special case + EGL::EGL Qt::Core Qt::CorePrivate Qt::Gui Qt::GuiPrivate ) -#### Keys ignored in scope 1:.:.:directfb.pro:<TRUE>: -# OTHER_FILES = "directfb.json" - ## Scopes: ##################################################################### @@ -50,13 +46,7 @@ qt_internal_extend_target(QDirectFbIntegrationPlugin CONDITION NOT DIRECTFB_PLAT DIRECTFB_PLATFORM_HOOKS ) -#### Keys ignored in scope 3:.:.:directfb.pro:NOT DIRECTFB_PLATFORM_HOOKS_SOURCES_ISEMPTY: -# QMAKE_LIBDIR = "$$DIRECTFB_PLATFORM_HOOKS_LIBDIR" - qt_internal_extend_target(QDirectFbIntegrationPlugin CONDITION DIRECTFB_PLATFORM_HOOKS_SOURCES_ISEMPTY SOURCES qdirectfbeglhooks_stub.cpp ) - -#### Keys ignored in scope 5:.:.:directfb.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/directfb/main.cpp b/src/plugins/platforms/directfb/main.cpp index 02cceeb487..4618696154 100644 --- a/src/plugins/platforms/directfb/main.cpp +++ b/src/plugins/platforms/directfb/main.cpp @@ -24,7 +24,7 @@ class QDirectFbIntegrationPlugin : public QPlatformIntegrationPlugin Q_OBJECT Q_PLUGIN_METADATA(IID QPlatformIntegrationFactoryInterface_iid FILE "directfb.json") public: - QPlatformIntegration *create(const QString&, const QStringList&); + QPlatformIntegration *create(const QString&, const QStringList&) override; }; QPlatformIntegration * QDirectFbIntegrationPlugin::create(const QString& system, const QStringList& paramList) diff --git a/src/plugins/platforms/directfb/qdirectfbbackingstore.h b/src/plugins/platforms/directfb/qdirectfbbackingstore.h index c51f29271d..625b672a40 100644 --- a/src/plugins/platforms/directfb/qdirectfbbackingstore.h +++ b/src/plugins/platforms/directfb/qdirectfbbackingstore.h @@ -18,10 +18,10 @@ class QDirectFbBackingStore : public QPlatformBackingStore public: QDirectFbBackingStore(QWindow *window); - QPaintDevice *paintDevice(); - void flush(QWindow *window, const QRegion ®ion, const QPoint &offset); - void resize (const QSize &size, const QRegion &staticContents); - bool scroll(const QRegion &area, int dx, int dy); + QPaintDevice *paintDevice() override; + void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; + void resize (const QSize &size, const QRegion &staticContents) override; + bool scroll(const QRegion &area, int dx, int dy) override; QImage toImage() const override; diff --git a/src/plugins/platforms/directfb/qdirectfbblitter.cpp b/src/plugins/platforms/directfb/qdirectfbblitter.cpp index 0a110da798..c93e48ead7 100644 --- a/src/plugins/platforms/directfb/qdirectfbblitter.cpp +++ b/src/plugins/platforms/directfb/qdirectfbblitter.cpp @@ -216,7 +216,7 @@ bool QDirectFbBlitter::drawCachedGlyphs(const QPaintEngineState *state, QFontEng for (int i=0; i<numGlyphs; ++i) { QFixed subPixelPosition = fontEngine->subPixelPositionForX(positions[i].x); - QTextureGlyphCache::GlyphAndSubPixelPosition glyph(glyphs[i], subPixelPosition); + QTextureGlyphCache::GlyphAndSubPixelPosition glyph(glyphs[i], QFixedPoint(subPixelPosition, 0)); const QTextureGlyphCache::Coord &c = cache->coords[glyph]; if (c.isNull()) continue; @@ -424,7 +424,7 @@ void QDirectFbBlitter::drawDebugRect(const QRect &rect, const QColor &color) void QDirectFbTextureGlyphCache::resizeTextureData(int width, int height) { - m_surface.reset();; + m_surface.reset(); QImageTextureGlyphCache::resizeTextureData(width, height); } diff --git a/src/plugins/platforms/directfb/qdirectfbblitter.h b/src/plugins/platforms/directfb/qdirectfbblitter.h index 591021061c..6183859613 100644 --- a/src/plugins/platforms/directfb/qdirectfbblitter.h +++ b/src/plugins/platforms/directfb/qdirectfbblitter.h @@ -19,11 +19,20 @@ public: QDirectFbBlitter(const QSize &size, bool alpha); virtual ~QDirectFbBlitter(); - virtual void fillRect(const QRectF &rect, const QColor &color); - virtual void drawPixmap(const QRectF &rect, const QPixmap &pixmap, const QRectF &subrect); - void alphaFillRect(const QRectF &rect, const QColor &color, QPainter::CompositionMode cmode); - void drawPixmapOpacity(const QRectF &rect, const QPixmap &pixmap, const QRectF &subrect, QPainter::CompositionMode cmode, qreal opacity); - virtual bool drawCachedGlyphs(const QPaintEngineState *state, QFontEngine::GlyphFormat glyphFormat, int numGlyphs, const glyph_t *glyphs, const QFixedPoint *positions, QFontEngine *fontEngine); + void fillRect(const QRectF &rect, const QColor &color) override; + void drawPixmap(const QRectF &rect, const QPixmap &pixmap, const QRectF &subrect) override; + void alphaFillRect(const QRectF &rect, const QColor &color, QPainter::CompositionMode cmode) override; + void drawPixmapOpacity(const QRectF &rect, + const QPixmap &pixmap, + const QRectF &subrect, + QPainter::CompositionMode cmode, + qreal opacity) override; + bool drawCachedGlyphs(const QPaintEngineState *state, + QFontEngine::GlyphFormat glyphFormat, + int numGlyphs, + const glyph_t *glyphs, + const QFixedPoint *positions, + QFontEngine *fontEngine) override; IDirectFBSurface *dfbSurface() const; @@ -32,8 +41,8 @@ public: static DFBSurfacePixelFormat selectPixmapFormat(bool withAlpha); protected: - virtual QImage *doLock(); - virtual void doUnlock(); + QImage *doLock() override; + void doUnlock() override; QDirectFBPointer<IDirectFBSurface> m_surface; QImage m_image; @@ -50,12 +59,12 @@ private: class QDirectFbBlitterPlatformPixmap : public QBlittablePlatformPixmap { public: - QBlittable *createBlittable(const QSize &size, bool alpha) const; + QBlittable *createBlittable(const QSize &size, bool alpha) const override; QDirectFbBlitter *dfbBlitter() const; - virtual bool fromFile(const QString &filename, const char *format, - Qt::ImageConversionFlags flags); + bool fromFile(const QString &filename, const char *format, + Qt::ImageConversionFlags flags) override; private: bool fromDataBufferDescription(const DFBDataBufferDescription &); @@ -83,7 +92,7 @@ public: : QImageTextureGlyphCache(format, matrix) {} - virtual void resizeTextureData(int width, int height); + void resizeTextureData(int width, int height) override; IDirectFBSurface *sourceSurface(); diff --git a/src/plugins/platforms/directfb/qdirectfbconvenience.h b/src/plugins/platforms/directfb/qdirectfbconvenience.h index c013988fe2..dc657f384e 100644 --- a/src/plugins/platforms/directfb/qdirectfbconvenience.h +++ b/src/plugins/platforms/directfb/qdirectfbconvenience.h @@ -62,7 +62,7 @@ template <typename T> class QDirectFBPointer : public QScopedPointer<T, QDirectFBInterfaceCleanupHandler<T> > { public: - QDirectFBPointer(T *t = nullptr) + Q_NODISCARD_CTOR QDirectFBPointer(T *t = nullptr) : QScopedPointer<T, QDirectFBInterfaceCleanupHandler<T> >(t) {} diff --git a/src/plugins/platforms/directfb/qdirectfbcursor.h b/src/plugins/platforms/directfb/qdirectfbcursor.h index 9a480fc8f8..21a8f4353d 100644 --- a/src/plugins/platforms/directfb/qdirectfbcursor.h +++ b/src/plugins/platforms/directfb/qdirectfbcursor.h @@ -19,7 +19,7 @@ class QDirectFBCursor : public QPlatformCursor public: QDirectFBCursor(QPlatformScreen *screen); #ifndef QT_NO_CURSOR - void changeCursor(QCursor *cursor, QWindow *window); + void changeCursor(QCursor *cursor, QWindow *window) override; #endif private: diff --git a/src/plugins/platforms/directfb/qdirectfbinput.cpp b/src/plugins/platforms/directfb/qdirectfbinput.cpp index a62ff9882c..c5aacad6cc 100644 --- a/src/plugins/platforms/directfb/qdirectfbinput.cpp +++ b/src/plugins/platforms/directfb/qdirectfbinput.cpp @@ -126,7 +126,7 @@ void QDirectFbInput::handleMouseEvents(const DFBEvent &event) long timestamp = (event.window.timestamp.tv_sec*1000) + (event.window.timestamp.tv_usec/1000); QWindow *tlw = m_tlwMap.value(event.window.window_id); - QWindowSystemInterface::handleMouseEvent(tlw, timestamp, p, globalPos, buttons); + QWindowSystemInterface::handleMouseEvent(tlw, timestamp, p, globalPos, buttons, Qt::NoButton, QEvent::None); } void QDirectFbInput::handleWheelEvent(const DFBEvent &event) @@ -135,9 +135,12 @@ void QDirectFbInput::handleWheelEvent(const DFBEvent &event) QPoint globalPos(event.window.cx, event.window.cy); long timestamp = (event.window.timestamp.tv_sec*1000) + (event.window.timestamp.tv_usec/1000); QWindow *tlw = m_tlwMap.value(event.window.window_id); - QWindowSystemInterface::handleWheelEvent(tlw, timestamp, p, globalPos, - event.window.step*120, - Qt::Vertical); + QWindowSystemInterface::handleWheelEvent(tlw, + timestamp, + p, + globalPos, + QPoint(), + QPoint(0, event.window.step*120)); } void QDirectFbInput::handleKeyEvents(const DFBEvent &event) @@ -173,7 +176,7 @@ void QDirectFbInput::handleEnterLeaveEvents(const DFBEvent &event) void QDirectFbInput::handleGotFocusEvent(const DFBEvent &event) { QWindow *tlw = m_tlwMap.value(event.window.window_id); - QWindowSystemInterface::handleWindowActivated(tlw, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(tlw, Qt::ActiveWindowFocusReason); } void QDirectFbInput::handleCloseEvent(const DFBEvent &event) diff --git a/src/plugins/platforms/directfb/qdirectfbinput.h b/src/plugins/platforms/directfb/qdirectfbinput.h index 5496f537a2..02175abc7b 100644 --- a/src/plugins/platforms/directfb/qdirectfbinput.h +++ b/src/plugins/platforms/directfb/qdirectfbinput.h @@ -26,7 +26,7 @@ public: void stopInputEventLoop(); protected: - void run(); + void run() override; private: void handleEvents(); diff --git a/src/plugins/platforms/directfb/qdirectfbintegration.h b/src/plugins/platforms/directfb/qdirectfbintegration.h index aa369d7c05..8dd2a4516a 100644 --- a/src/plugins/platforms/directfb/qdirectfbintegration.h +++ b/src/plugins/platforms/directfb/qdirectfbintegration.h @@ -25,16 +25,16 @@ public: void connectToDirectFb(); - bool hasCapability(Capability cap) const; - QPlatformPixmap *createPlatformPixmap(QPlatformPixmap::PixelType type) const; - QPlatformWindow *createPlatformWindow(QWindow *window) const; - QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const; - QAbstractEventDispatcher *createEventDispatcher() const; - - QPlatformFontDatabase *fontDatabase() const; - QPlatformServices *services() const; - QPlatformInputContext *inputContext() const { return m_inputContext; } - QPlatformNativeInterface *nativeInterface() const; + bool hasCapability(Capability cap) const override; + QPlatformPixmap *createPlatformPixmap(QPlatformPixmap::PixelType type) const override; + QPlatformWindow *createPlatformWindow(QWindow *window) const override; + QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override; + QAbstractEventDispatcher *createEventDispatcher() const override; + + QPlatformFontDatabase *fontDatabase() const override; + QPlatformServices *services() const override; + QPlatformInputContext *inputContext() const override { return m_inputContext; } + QPlatformNativeInterface *nativeInterface() const override; protected: virtual void initializeDirectFB(); diff --git a/src/plugins/platforms/directfb/qdirectfbscreen.h b/src/plugins/platforms/directfb/qdirectfbscreen.h index a4f2e9adb1..cbcb6a55da 100644 --- a/src/plugins/platforms/directfb/qdirectfbscreen.h +++ b/src/plugins/platforms/directfb/qdirectfbscreen.h @@ -19,11 +19,11 @@ class QDirectFbScreen : public QPlatformScreen public: QDirectFbScreen(int display); - QRect geometry() const { return m_geometry; } - int depth() const { return m_depth; } - QImage::Format format() const { return m_format; } - QSizeF physicalSize() const { return m_physicalSize; } - QPlatformCursor *cursor() const { return m_cursor.data(); } + QRect geometry() const override { return m_geometry; } + int depth() const override { return m_depth; } + QImage::Format format() const override { return m_format; } + QSizeF physicalSize() const override { return m_physicalSize; } + QPlatformCursor *cursor() const override { return m_cursor.data(); } // DirectFb helpers IDirectFBDisplayLayer *dfbLayer() const; diff --git a/src/plugins/platforms/directfb/qdirectfbwindow.h b/src/plugins/platforms/directfb/qdirectfbwindow.h index 6413d91860..f05038d8ca 100644 --- a/src/plugins/platforms/directfb/qdirectfbwindow.h +++ b/src/plugins/platforms/directfb/qdirectfbwindow.h @@ -15,19 +15,19 @@ class QDirectFbWindow : public QPlatformWindow { public: QDirectFbWindow(QWindow *tlw, QDirectFbInput *inputhandler); - ~QDirectFbWindow(); + ~QDirectFbWindow() override; - void setGeometry(const QRect &rect); - void setOpacity(qreal level); + void setGeometry(const QRect &rect) override; + void setOpacity(qreal level) override; - void setVisible(bool visible); + void setVisible(bool visible) override; - void setWindowFlags(Qt::WindowFlags flags); - bool setKeyboardGrabEnabled(bool grab); - bool setMouseGrabEnabled(bool grab); - void raise(); - void lower(); - WId winId() const; + void setWindowFlags(Qt::WindowFlags flags) override; + bool setKeyboardGrabEnabled(bool grab) override; + bool setMouseGrabEnabled(bool grab) override; + void raise() override; + void lower() override; + WId winId() const override; virtual void createDirectFBWindow(); IDirectFBWindow *dfbWindow() const; diff --git a/src/plugins/platforms/eglfs/CMakeLists.txt b/src/plugins/platforms/eglfs/CMakeLists.txt index c8b398e7a2..cb4b5d1eb9 100644 --- a/src/plugins/platforms/eglfs/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/CMakeLists.txt @@ -1,5 +1,6 @@ -# Generated from eglfs.pro. -# special case begin +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_find_package(EGL) if(QT_FEATURE_eglfs_gbm) @@ -15,7 +16,6 @@ elseif(QT_FEATURE_eglfs_openwfd) endif() set(QT_QPA_DEFAULT_EGLFS_INTEGRATION "${_device_integration}" CACHE STRING "Default EGLFS device integration plugin") -# special case end ##################################################################### ## EglFSDeviceIntegrationPrivate Module: @@ -34,7 +34,8 @@ qt_internal_add_module(EglFSDeviceIntegrationPrivate DEFINES QT_BUILD_EGL_DEVICE_LIB QT_EGL_NO_X11 - EGLFS_PREFERRED_PLUGIN=${QT_QPA_DEFAULT_EGLFS_INTEGRATION} # special case + QT_NO_FOREACH + EGLFS_PREFERRED_PLUGIN=${QT_QPA_DEFAULT_EGLFS_INTEGRATION} INCLUDE_DIRECTORIES api PUBLIC_LIBRARIES @@ -42,12 +43,12 @@ qt_internal_add_module(EglFSDeviceIntegrationPrivate Qt::DeviceDiscoverySupportPrivate Qt::FbSupportPrivate Qt::GuiPrivate - EGL::EGL # special case + EGL::EGL + HEADER_SYNC_SOURCE_DIRECTORY + "${CMAKE_CURRENT_SOURCE_DIR}/api" + NO_GENERATE_CPP_EXPORTS ) -#### Keys ignored in scope 2:.:.:eglfsdeviceintegration.pro:<TRUE>: -# MODULE = "eglfsdeviceintegration" - ## Scopes: ##################################################################### @@ -69,19 +70,6 @@ qt_internal_extend_target(EglFSDeviceIntegrationPrivate CONDITION QT_FEATURE_ope Qt::OpenGLPrivate ) -# special case begin -# comment out -#qt_internal_extend_target(EglFSDeviceIntegrationPrivate CONDITION NOT EGLFS_PLATFORM_HOOKS_SOURCES_ISEMPTY - #DEFINES - #EGLFS_PLATFORM_HOOKS -#) - -#qt_internal_extend_target(EglFSDeviceIntegrationPrivate CONDITION NOT EGLFS_DEVICE_INTEGRATION_ISEMPTY - #DEFINES - #EGLFS_PREFERRED_PLUGIN= -#) -# special case end - if(QT_FEATURE_cursor) # Resources: set(cursor_resource_files @@ -104,23 +92,15 @@ endif() qt_internal_add_plugin(QEglFSIntegrationPlugin OUTPUT_NAME qeglfs PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES eglfs # special case + DEFAULT_IF "eglfs" IN_LIST QT_QPA_PLATFORMS SOURCES qeglfsmain.cpp DEFINES QT_EGL_NO_X11 LIBRARIES - Qt::CorePrivate # special case + Qt::CorePrivate Qt::EglFSDeviceIntegrationPrivate - EGL::EGL # special case + EGL::EGL ) -#### Keys ignored in scope 12:.:.:eglfs-plugin.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs.json" - -## Scopes: -##################################################################### - -#### Keys ignored in scope 13:.:.:eglfs-plugin.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" add_subdirectory(deviceintegration) diff --git a/src/plugins/platforms/eglfs/api/qeglfscontext.cpp b/src/plugins/platforms/eglfs/api/qeglfscontext.cpp index a17b567b24..9c10c1a998 100644 --- a/src/plugins/platforms/eglfs/api/qeglfscontext.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfscontext.cpp @@ -22,10 +22,19 @@ QEglFSContext::QEglFSContext(const QSurfaceFormat &format, QPlatformOpenGLContex EGLSurface QEglFSContext::eglSurfaceForPlatformSurface(QPlatformSurface *surface) { - if (surface->surface()->surfaceClass() == QSurface::Window) - return static_cast<QEglFSWindow *>(surface)->surface(); - else + if (surface->surface()->surfaceClass() == QSurface::Window) { + + QEglFSWindow *w = static_cast<QEglFSWindow *>(surface); + EGLSurface s = w->surface(); + if (s == EGL_NO_SURFACE) { + w->resetSurface(); + s = w->surface(); + } + return s; + + } else { return static_cast<QEGLPbuffer *>(surface)->pbuffer(); + } } EGLSurface QEglFSContext::createTemporaryOffscreenSurface() diff --git a/src/plugins/platforms/eglfs/api/qeglfscursor.cpp b/src/plugins/platforms/eglfs/api/qeglfscursor.cpp index 05a86cee64..1e1a7d8269 100644 --- a/src/plugins/platforms/eglfs/api/qeglfscursor.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfscursor.cpp @@ -8,6 +8,7 @@ #include <qpa/qwindowsysteminterface.h> #include <QtGui/QOpenGLContext> +#include <QtGui/QOpenGLFunctions> #include <QtCore/QFile> #include <QtCore/QJsonDocument> #include <QtCore/QJsonArray> @@ -117,16 +118,18 @@ void QEglFSCursor::createShaderPrograms() void QEglFSCursor::createCursorTexture(uint *texture, const QImage &image) { + Q_ASSERT(QOpenGLContext::currentContext()); + QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions(); if (!*texture) - glGenTextures(1, texture); - glBindTexture(GL_TEXTURE_2D, *texture); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - glTexImage2D(GL_TEXTURE_2D, 0 /* level */, GL_RGBA, image.width(), image.height(), 0 /* border */, - GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); + f->glGenTextures(1, texture); + f->glBindTexture(GL_TEXTURE_2D, *texture); + f->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + f->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + f->glTexImage2D(GL_TEXTURE_2D, 0 /* level */, GL_RGBA, image.width(), image.height(), 0 /* border */, + GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); } void QEglFSCursor::initCursorAtlas() @@ -309,8 +312,7 @@ void QEglFSCursor::paintOnScreen() // screens are siblings of each other. When not enabled, the sibling list // only contains m_screen itself. for (QPlatformScreen *screen : m_screen->virtualSiblings()) { - if (screen->geometry().contains(cr.topLeft().toPoint() + m_cursor.hotSpot) - && QOpenGLContext::currentContext()->screen() == screen->screen()) + if (screen->geometry().contains(cr.topLeft().toPoint() + m_cursor.hotSpot)) { cr.translate(-screen->geometry().topLeft()); const QSize screenSize = screen->geometry().size(); @@ -339,8 +341,8 @@ void QEglFSCursor::paintOnScreen() // to deal with the changes we make. struct StateSaver { - StateSaver() { - f = QOpenGLContext::currentContext()->functions(); + StateSaver(QOpenGLFunctions* func) { + f = func; vaoHelper = QOpenGLVertexArrayObjectHelper::vertexArrayObjectHelperForContext(QOpenGLContext::currentContext()); static bool windowsChecked = false; @@ -366,6 +368,8 @@ struct StateSaver f->glGetIntegerv(GL_BLEND_SRC_ALPHA, blendFunc + 1); f->glGetIntegerv(GL_BLEND_DST_RGB, blendFunc + 2); f->glGetIntegerv(GL_BLEND_DST_ALPHA, blendFunc + 3); + scissor = f->glIsEnabled(GL_SCISSOR_TEST); + stencil = f->glIsEnabled(GL_STENCIL_TEST); f->glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &arrayBuf); if (vaoHelper->isValid()) f->glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &vao); @@ -389,17 +393,15 @@ struct StateSaver f->glFrontFace(frontFace); if (cull) f->glEnable(GL_CULL_FACE); - else - f->glDisable(GL_CULL_FACE); if (depthTest) f->glEnable(GL_DEPTH_TEST); - else - f->glDisable(GL_DEPTH_TEST); - if (blend) - f->glEnable(GL_BLEND); - else + if (!blend) f->glDisable(GL_BLEND); f->glBlendFuncSeparate(blendFunc[0], blendFunc[1], blendFunc[2], blendFunc[3]); + if (scissor) + f->glEnable(GL_SCISSOR_TEST); + if (stencil) + f->glEnable(GL_STENCIL_TEST); f->glBindBuffer(GL_ARRAY_BUFFER, arrayBuf); if (vaoHelper->isValid()) vaoHelper->glBindVertexArray(vao); @@ -424,6 +426,8 @@ struct StateSaver bool depthTest; bool blend; GLint blendFunc[4]; + bool scissor; + bool stencil; GLint vao; GLint arrayBuf; struct { GLint enabled, type, size, normalized, stride, buffer; GLvoid *pointer; } va[2]; @@ -431,13 +435,12 @@ struct StateSaver void QEglFSCursor::draw(const QRectF &r) { - StateSaver stateSaver; + Q_ASSERT(QOpenGLContext::currentContext()); + QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions(); + StateSaver stateSaver(f); QEglFSCursorData &gfx = static_cast<QEglFSContext*>(QOpenGLContext::currentContext()->handle())->cursorData; if (!gfx.program) { - // one time initialization - initializeOpenGLFunctions(); - createShaderPrograms(); if (!gfx.atlasTexture) { @@ -483,13 +486,13 @@ void QEglFSCursor::draw(const QRectF &r) s2, t1 }; - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, cursorTexture); + f->glActiveTexture(GL_TEXTURE0); + f->glBindTexture(GL_TEXTURE_2D, cursorTexture); if (stateSaver.vaoHelper->isValid()) stateSaver.vaoHelper->glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); + f->glBindBuffer(GL_ARRAY_BUFFER, 0); gfx.program->enableAttributeArray(0); gfx.program->enableAttributeArray(1); @@ -499,13 +502,15 @@ void QEglFSCursor::draw(const QRectF &r) gfx.program->setUniformValue(gfx.textureEntry, 0); gfx.program->setUniformValue(gfx.matEntry, m_rotationMatrix); - glDisable(GL_CULL_FACE); - glFrontFace(GL_CCW); - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - glDisable(GL_DEPTH_TEST); // disable depth testing to make sure cursor is always on top + f->glDisable(GL_CULL_FACE); + f->glFrontFace(GL_CCW); + f->glEnable(GL_BLEND); + f->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + f->glDisable(GL_DEPTH_TEST); // disable depth testing to make sure cursor is always on top + f->glDisable(GL_SCISSOR_TEST); + f->glDisable(GL_STENCIL_TEST); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); gfx.program->disableAttributeArray(0); gfx.program->disableAttributeArray(1); diff --git a/src/plugins/platforms/eglfs/api/qeglfscursor_p.h b/src/plugins/platforms/eglfs/api/qeglfscursor_p.h index 294426067c..88d4ce695a 100644 --- a/src/plugins/platforms/eglfs/api/qeglfscursor_p.h +++ b/src/plugins/platforms/eglfs/api/qeglfscursor_p.h @@ -20,7 +20,6 @@ #include <qpa/qplatformscreen.h> #include <QtOpenGL/QOpenGLShaderProgram> #include <QtGui/QMatrix4x4> -#include <QtGui/QOpenGLFunctions> #include <QtGui/private/qinputdevicemanager_p.h> #include <QtCore/qlist.h> @@ -58,7 +57,6 @@ struct QEglFSCursorData { }; class Q_EGLFS_EXPORT QEglFSCursor : public QPlatformCursor - , protected QOpenGLFunctions { Q_OBJECT public: diff --git a/src/plugins/platforms/eglfs/api/qeglfsdeviceintegration.cpp b/src/plugins/platforms/eglfs/api/qeglfsdeviceintegration.cpp index 268b9aa093..56fda45e90 100644 --- a/src/plugins/platforms/eglfs/api/qeglfsdeviceintegration.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfsdeviceintegration.cpp @@ -65,7 +65,7 @@ static int framebuffer = -1; QByteArray QEglFSDeviceIntegration::fbDeviceName() const { -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_VXWORKS) QByteArray fbDev = qgetenv("QT_QPA_EGLFS_FB"); if (fbDev.isEmpty()) fbDev = QByteArrayLiteral("/dev/fb0"); @@ -95,7 +95,7 @@ int QEglFSDeviceIntegration::framebufferIndex() const void QEglFSDeviceIntegration::platformInit() { -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_VXWORKS) QByteArray fbDev = fbDeviceName(); framebuffer = qt_safe_open(fbDev, O_RDONLY); @@ -113,7 +113,7 @@ void QEglFSDeviceIntegration::platformInit() void QEglFSDeviceIntegration::platformDestroy() { -#ifdef Q_OS_LINUX +#if defined(Q_OS_LINUX) || defined(Q_OS_VXWORKS) if (framebuffer != -1) close(framebuffer); #endif diff --git a/src/plugins/platforms/eglfs/api/qeglfsintegration.cpp b/src/plugins/platforms/eglfs/api/qeglfsintegration.cpp index 42cbdf127c..f0b64c475c 100644 --- a/src/plugins/platforms/eglfs/api/qeglfsintegration.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfsintegration.cpp @@ -110,7 +110,8 @@ void QEglFSIntegration::initialize() void QEglFSIntegration::destroy() { - foreach (QWindow *w, qGuiApp->topLevelWindows()) + const auto toplevels = qGuiApp->topLevelWindows(); + for (QWindow *w : toplevels) w->destroy(); qt_egl_device_integration()->screenDestroy(); @@ -386,6 +387,14 @@ QFunctionPointer QEglFSIntegration::platformFunction(const QByteArray &function) return qt_egl_device_integration()->platformFunction(function); } +QVariant QEglFSIntegration::styleHint(QPlatformIntegration::StyleHint hint) const +{ + if (hint == QPlatformIntegration::ShowIsFullScreen) + return true; + + return QPlatformIntegration::styleHint(hint); +} + #if QT_CONFIG(evdev) void QEglFSIntegration::loadKeymap(const QString &filename) { diff --git a/src/plugins/platforms/eglfs/api/qeglfsintegration_p.h b/src/plugins/platforms/eglfs/api/qeglfsintegration_p.h index 3e4c57197e..8007167ec7 100644 --- a/src/plugins/platforms/eglfs/api/qeglfsintegration_p.h +++ b/src/plugins/platforms/eglfs/api/qeglfsintegration_p.h @@ -76,6 +76,8 @@ public: QFunctionPointer platformFunction(const QByteArray &function) const override; + QVariant styleHint(QPlatformIntegration::StyleHint hint) const override; + QFbVtHandler *vtHandler() { return m_vtHandler.data(); } QPointer<QWindow> pointerWindow() { return m_pointerWindow; } diff --git a/src/plugins/platforms/eglfs/api/qeglfsscreen.cpp b/src/plugins/platforms/eglfs/api/qeglfsscreen.cpp index b081406258..c5108be04a 100644 --- a/src/plugins/platforms/eglfs/api/qeglfsscreen.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfsscreen.cpp @@ -121,7 +121,7 @@ void QEglFSScreen::handleCursorMove(const QPoint &pos) return; // First window is always fullscreen. - if (windows.count() == 1) { + if (windows.size() == 1) { QWindow *window = windows[0]->sourceWindow(); if (platformIntegration->pointerWindow() != window) { platformIntegration->setPointerWindow(window); @@ -131,7 +131,7 @@ void QEglFSScreen::handleCursorMove(const QPoint &pos) } QWindow *enter = nullptr, *leave = nullptr; - for (int i = windows.count() - 1; i >= 0; --i) { + for (int i = windows.size() - 1; i >= 0; --i) { QWindow *window = windows[i]->sourceWindow(); const QRect geom = window->geometry(); if (geom.contains(pos)) { @@ -189,7 +189,7 @@ QPixmap QEglFSScreen::grabWindow(WId wid, int x, int y, int width, int height) c return QPixmap::fromImage(img).copy(x, y, width, height); } - foreach (QOpenGLCompositorWindow *w, windows) { + for (QOpenGLCompositorWindow *w : windows) { const QWindow *window = w->sourceWindow(); if (window->winId() == wid) { const QRect geom = window->geometry(); @@ -212,4 +212,22 @@ QPixmap QEglFSScreen::grabWindow(WId wid, int x, int y, int width, int height) c return QPixmap(); } +QWindow *QEglFSScreen::topLevelAt(const QPoint &point) const +{ +#ifndef QT_NO_OPENGL + QOpenGLCompositor *compositor = QOpenGLCompositor::instance(); + const QList<QOpenGLCompositorWindow *> windows = compositor->windows(); + const int windowCount = windows.size(); + + // Higher z-order is at the end of the list + for (int i = windowCount - 1; i >= 0; i--) { + QWindow *window = windows[i]->sourceWindow(); + if (window->isVisible() && window->geometry().contains(point)) + return window; + } +#endif + + return QPlatformScreen::topLevelAt(point); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/eglfs/api/qeglfsscreen_p.h b/src/plugins/platforms/eglfs/api/qeglfsscreen_p.h index 8f1eb91206..bbfc9a9259 100644 --- a/src/plugins/platforms/eglfs/api/qeglfsscreen_p.h +++ b/src/plugins/platforms/eglfs/api/qeglfsscreen_p.h @@ -54,6 +54,8 @@ public: void handleCursorMove(const QPoint &pos); + QWindow *topLevelAt(const QPoint &point) const override; + private: void setPrimarySurface(EGLSurface surface); diff --git a/src/plugins/platforms/eglfs/api/qeglfswindow.cpp b/src/plugins/platforms/eglfs/api/qeglfswindow.cpp index 39c5b3d4bf..306d121cfb 100644 --- a/src/plugins/platforms/eglfs/api/qeglfswindow.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfswindow.cpp @@ -22,6 +22,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(qLcEglDevDebug) + QEglFSWindow::QEglFSWindow(QWindow *w) : QPlatformWindow(w), #ifndef QT_NO_OPENGL @@ -162,8 +164,29 @@ void QEglFSWindow::destroy() void QEglFSWindow::invalidateSurface() { if (m_surface != EGL_NO_SURFACE) { - eglDestroySurface(screen()->display(), m_surface); + qCDebug(qLcEglDevDebug) << Q_FUNC_INFO << " about to destroy EGLSurface: " << m_surface; + + bool ok = eglDestroySurface(screen()->display(), m_surface); + + if (!ok) { + qCWarning(qLcEglDevDebug, "QEglFSWindow::invalidateSurface() eglDestroySurface failed!" + " Follow-up errors or memory leaks are possible." + " eglGetError(): %x", eglGetError()); + } + + if (eglGetCurrentSurface(EGL_READ) == m_surface || + eglGetCurrentSurface(EGL_DRAW) == m_surface) { + bool ok = eglMakeCurrent(eglGetCurrentDisplay(), EGL_NO_DISPLAY, EGL_NO_DISPLAY, EGL_NO_CONTEXT); + qCDebug(qLcEglDevDebug) << Q_FUNC_INFO << " due to eglDestroySurface on *currently* bound surface" + << "we just called eglMakeCurrent(..,0,0,0)! It returned: " << ok; + } + + if (screen()->primarySurface() == m_surface) + screen()->setPrimarySurface(EGL_NO_SURFACE); + + m_surface = EGL_NO_SURFACE; + m_flags = m_flags & ~Created; } qt_egl_device_integration()->destroyNativeWindow(m_window); m_window = 0; @@ -240,7 +263,7 @@ void QEglFSWindow::requestActivateWindow() QOpenGLCompositor::instance()->moveToTop(this); #endif QWindow *wnd = window(); - QWindowSystemInterface::handleWindowActivated(wnd, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(wnd, Qt::ActiveWindowFocusReason); QWindowSystemInterface::handleExposeEvent(wnd, QRect(QPoint(0, 0), wnd->geometry().size())); } @@ -260,7 +283,7 @@ void QEglFSWindow::lower() #ifndef QT_NO_OPENGL QOpenGLCompositor *compositor = QOpenGLCompositor::instance(); QList<QOpenGLCompositorWindow *> windows = compositor->windows(); - if (window()->type() != Qt::Desktop && windows.count() > 1) { + if (window()->type() != Qt::Desktop && windows.size() > 1) { int idx = windows.indexOf(this); if (idx > 0) { compositor->changeWindowIndex(this, idx - 1); diff --git a/src/plugins/platforms/eglfs/api/qeglfswindow_p.h b/src/plugins/platforms/eglfs/api/qeglfswindow_p.h index d111042040..e51cd69f3d 100644 --- a/src/plugins/platforms/eglfs/api/qeglfswindow_p.h +++ b/src/plugins/platforms/eglfs/api/qeglfswindow_p.h @@ -70,8 +70,8 @@ public: bool isRaster() const; #ifndef QT_NO_OPENGL - QOpenGLCompositorBackingStore *backingStore() { return m_backingStore; } - void setBackingStore(QOpenGLCompositorBackingStore *backingStore); + QOpenGLCompositorBackingStore *backingStore() const override { return m_backingStore; } + void setBackingStore(QOpenGLCompositorBackingStore *backingStore) override; QWindow *sourceWindow() const override; const QPlatformTextureList *textures() const override; void endCompositing() override; diff --git a/src/plugins/platforms/eglfs/deviceintegration/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/CMakeLists.txt index 9e1c31527e..bb50216f23 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from deviceintegration.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_eglfs_x11) add_subdirectory(eglfs_x11) @@ -13,10 +14,10 @@ if(QT_FEATURE_eglfs_egldevice) add_subdirectory(eglfs_kms_egldevice) endif() if(QT_FEATURE_eglfs_vsp2) - # add_subdirectory(eglfs_kms_vsp2) # special case TODO + # add_subdirectory(eglfs_kms_vsp2) # TODO: QTBUG-112769 endif() if(QT_FEATURE_eglfs_brcm) - # add_subdirectory(eglfs_brcm) # special case TODO + # add_subdirectory(eglfs_brcm) # TODO: QTBUG-112769 endif() if(QT_FEATURE_eglfs_mali) add_subdirectory(eglfs_mali) @@ -25,7 +26,7 @@ if(QT_FEATURE_eglfs_viv) add_subdirectory(eglfs_viv) endif() if(QT_FEATURE_eglfs_rcar) - # add_subdirectory(eglfs_rcar) # special case TODO + # add_subdirectory(eglfs_rcar) # TODO: QTBUG-112769 endif() if(QT_FEATURE_eglfs_viv_wl) add_subdirectory(eglfs_viv_wl) diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/CMakeLists.txt index e1de63ea42..fa744be033 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_emu.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSEmulatorIntegrationPlugin Plugin: @@ -22,7 +23,3 @@ qt_internal_add_plugin(QEglFSEmulatorIntegrationPlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:eglfs_emu.pro:<TRUE>: -# DISTFILES = "eglfs_emu.json" -# OTHER_FILES = "$$PWD/eglfs_emu.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/qeglfsemulatorintegration.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/qeglfsemulatorintegration.cpp index ed55420c57..a63aafa242 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/qeglfsemulatorintegration.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_emu/qeglfsemulatorintegration.cpp @@ -93,7 +93,7 @@ EGLNativeWindowType QEglFSEmulatorIntegration::createNativeWindow(QPlatformWindo // Let the emulator know which screen the window surface is attached to setDisplay(screen->id()); } - static QBasicAtomicInt uniqueWindowId = Q_BASIC_ATOMIC_INITIALIZER(0); + Q_CONSTINIT static QBasicAtomicInt uniqueWindowId = Q_BASIC_ATOMIC_INITIALIZER(0); return EGLNativeWindowType(qintptr(1 + uniqueWindowId.fetchAndAddRelaxed(1))); } diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt index a20a4a084d..9f9d315202 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_kms.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## EglFsKmsGbmSupportPrivate Module: @@ -23,6 +24,7 @@ qt_internal_add_module(EglFsKmsGbmSupportPrivate Qt::GuiPrivate Qt::KmsSupportPrivate gbm::gbm + NO_GENERATE_CPP_EXPORTS ) ##################################################################### ## QEglFSKmsGbmIntegrationPlugin Plugin: @@ -45,6 +47,3 @@ qt_internal_add_plugin(QEglFSKmsGbmIntegrationPlugin Qt::KmsSupportPrivate gbm::gbm ) - -#### Keys ignored in scope 3:.:.:eglfs_kms-plugin.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_kms.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice.cpp index 6d903a3f1e..89479fc250 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice.cpp @@ -47,7 +47,13 @@ bool QEglFSKmsGbmDevice::open() setFd(fd); - m_eventReader.create(this); + if (usesEventReader()) { + qCDebug(qLcEglfsKmsDebug, "Using dedicated drm event reading thread"); + m_eventReader.create(this); + } else { + qCDebug(qLcEglfsKmsDebug, "Not using dedicated drm event reading thread; " + "threaded multi-screen setups may experience problems"); + } return true; } @@ -56,7 +62,8 @@ void QEglFSKmsGbmDevice::close() { // Note: screens are gone at this stage. - m_eventReader.destroy(); + if (usesEventReader()) + m_eventReader.destroy(); if (m_gbm_device) { gbm_device_destroy(m_gbm_device); @@ -138,4 +145,10 @@ void QEglFSKmsGbmDevice::registerScreen(QPlatformScreen *screen, m_globalCursor->reevaluateVisibilityForScreens(); } +bool QEglFSKmsGbmDevice::usesEventReader() const +{ + static const bool eventReaderThreadDisabled = qEnvironmentVariableIntValue("QT_QPA_EGLFS_KMS_NO_EVENT_READER_THREAD"); + return !eventReaderThreadDisabled; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice_p.h b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice_p.h index 832b3a2873..e00992ed29 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice_p.h +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmdevice_p.h @@ -19,6 +19,7 @@ #include "qeglfskmsgbmcursor_p.h" #include <private/qeglfskmsdevice_p.h> +#include <private/qeglfskmseventreader_p.h> #include <gbm.h> @@ -51,11 +52,14 @@ public: const QPoint &virtualPos, const QList<QPlatformScreen *> &virtualSiblings) override; + bool usesEventReader() const; + QEglFSKmsEventReader *eventReader() { return &m_eventReader; } + private: Q_DISABLE_COPY(QEglFSKmsGbmDevice) gbm_device *m_gbm_device; - + QEglFSKmsEventReader m_eventReader; QEglFSKmsGbmCursor *m_globalCursor; }; diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp index 9aa815ba90..8dcfed04db 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp @@ -20,6 +20,8 @@ QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(qLcEglfsKmsDebug) +QMutex QEglFSKmsGbmScreen::m_nonThreadedFlipMutex; + static inline uint32_t drmFormatToGbmFormat(uint32_t drmFormat) { Q_ASSERT(DRM_FORMAT_XRGB8888 == GBM_FORMAT_XRGB8888); @@ -71,8 +73,9 @@ QEglFSKmsGbmScreen::FrameBuffer *QEglFSKmsGbmScreen::framebufferForBufferObject( return nullptr; } - gbm_bo_set_user_data(bo, fb.get(), bufferDestroyedHandler); - return fb.release(); + auto res = fb.get(); + gbm_bo_set_user_data(bo, fb.release(), bufferDestroyedHandler); + return res; } QEglFSKmsGbmScreen::QEglFSKmsGbmScreen(QEglFSKmsDevice *device, const QKmsOutput &output, bool headless) @@ -140,11 +143,12 @@ gbm_surface *QEglFSKmsGbmScreen::createSurface(EGLConfig eglConfig) } } + const uint32_t gbmFormat = drmFormatToGbmFormat(m_output.drm_format); + // Fallback for older drivers, and when "format" is explicitly specified // in the output config. (not guaranteed that the requested format works // of course, but do what we are told to) if (!m_gbm_surface) { - uint32_t gbmFormat = drmFormatToGbmFormat(m_output.drm_format); if (queryFromEgl) qCDebug(qLcEglfsKmsDebug, "Could not create surface with EGL_NATIVE_VISUAL_ID, falling back to format %x", gbmFormat); m_gbm_surface = gbm_surface_create(gbmDevice, @@ -153,13 +157,39 @@ gbm_surface *QEglFSKmsGbmScreen::createSurface(EGLConfig eglConfig) gbmFormat, gbmFlags()); } + + // Fallback for some drivers, its required to request with modifiers + if (!m_gbm_surface) { + uint64_t modifier = DRM_FORMAT_MOD_LINEAR; + + m_gbm_surface = gbm_surface_create_with_modifiers(gbmDevice, + rawGeometry().width(), + rawGeometry().height(), + gbmFormat, + &modifier, 1); + } + // Fail here, as it would fail with the next usage of the GBM surface, which is very unexpected + if (!m_gbm_surface) + qFatal("Could not create GBM surface!"); } return m_gbm_surface; // not owned, gets destroyed in QEglFSKmsGbmIntegration::destroyNativeWindow() via QEglFSKmsGbmWindow::invalidateSurface() } void QEglFSKmsGbmScreen::resetSurface() { + m_flipPending = false; // not necessarily true but enough to keep bo_next + m_gbm_bo_current = nullptr; m_gbm_surface = nullptr; + + // Leave m_gbm_bo_next untouched. waitForFlip() should + // still do its work, when called. Otherwise we end up + // in device-is-busy errors if there is a new QWindow + // created afterwards. (QTBUG-122663) + + // If not using atomic, will need a new drmModeSetCrtc if a new window + // gets created later on (and so there's a new fb). + if (!device()->hasAtomicSupport()) + needsNewModeSetForNextFb = true; } void QEglFSKmsGbmScreen::initCloning(QPlatformScreen *screenThisScreenClones, @@ -171,8 +201,10 @@ void QEglFSKmsGbmScreen::initCloning(QPlatformScreen *screenThisScreenClones, qWarning("QEglFSKmsGbmScreen %s cannot be clone source and destination at the same time", qPrintable(name())); return; } - if (clonesAnother) + if (clonesAnother) { m_cloneSource = static_cast<QEglFSKmsGbmScreen *>(screenThisScreenClones); + qCDebug(qLcEglfsKmsDebug, "Screen %s clones %s", qPrintable(name()), qPrintable(m_cloneSource->name())); + } // clone sources need to know their additional destinations for (QPlatformScreen *s : screensCloningThisScreen) { @@ -187,8 +219,9 @@ void QEglFSKmsGbmScreen::ensureModeSet(uint32_t fb) QKmsOutput &op(output()); const int fd = device()->fd(); - if (!op.mode_set) { + if (!op.mode_set || needsNewModeSetForNextFb) { op.mode_set = true; + needsNewModeSetForNextFb = false; bool doModeSet = true; drmModeCrtcPtr currentMode = drmModeGetCrtc(fd, op.crtc_id); @@ -227,6 +260,29 @@ void QEglFSKmsGbmScreen::ensureModeSet(uint32_t fb) } } +void QEglFSKmsGbmScreen::nonThreadedPageFlipHandler(int fd, + unsigned int sequence, + unsigned int tv_sec, + unsigned int tv_usec, + void *user_data) +{ + // note that with cloning involved this callback is called also for screens that clone another one + Q_UNUSED(fd); + QEglFSKmsGbmScreen *screen = static_cast<QEglFSKmsGbmScreen *>(user_data); + screen->flipFinished(); + screen->pageFlipped(sequence, tv_sec, tv_usec); +} + +void QEglFSKmsGbmScreen::waitForFlipWithEventReader(QEglFSKmsGbmScreen *screen) +{ + m_flipMutex.lock(); + QEglFSKmsGbmDevice *dev = static_cast<QEglFSKmsGbmDevice *>(device()); + dev->eventReader()->startWaitFlip(screen, &m_flipMutex, &m_flipCond); + m_flipCond.wait(&m_flipMutex); + m_flipMutex.unlock(); + screen->flipFinished(); +} + void QEglFSKmsGbmScreen::waitForFlip() { if (m_headless || m_cloneSource) @@ -236,18 +292,69 @@ void QEglFSKmsGbmScreen::waitForFlip() if (!m_gbm_bo_next) return; - m_flipMutex.lock(); - device()->eventReader()->startWaitFlip(this, &m_flipMutex, &m_flipCond); - m_flipCond.wait(&m_flipMutex); - m_flipMutex.unlock(); - - flipFinished(); + QEglFSKmsGbmDevice *dev = static_cast<QEglFSKmsGbmDevice *>(device()); + if (dev->usesEventReader()) { + waitForFlipWithEventReader(this); + // Now, unlike on the other code path, we need to ensure the + // flips have completed for the screens that just scan out + // this one's content, because the eventReader's wait is + // per-output. + for (CloneDestination &d : m_cloneDests) { + if (d.screen != this) + waitForFlipWithEventReader(d.screen); + } + } else { + QMutexLocker lock(&m_nonThreadedFlipMutex); + while (m_gbm_bo_next) { + drmEventContext drmEvent; + memset(&drmEvent, 0, sizeof(drmEvent)); + drmEvent.version = 2; + drmEvent.vblank_handler = nullptr; + drmEvent.page_flip_handler = nonThreadedPageFlipHandler; + drmHandleEvent(device()->fd(), &drmEvent); + } + } #if QT_CONFIG(drm_atomic) device()->threadLocalAtomicReset(); #endif } +#if QT_CONFIG(drm_atomic) +static void addAtomicFlip(drmModeAtomicReq *request, const QKmsOutput &output, uint32_t fb) +{ + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->framebufferPropertyId, fb); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->crtcPropertyId, output.crtc_id); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->srcwidthPropertyId, output.size.width() << 16); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->srcXPropertyId, 0); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->srcYPropertyId, 0); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->srcheightPropertyId, output.size.height() << 16); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->crtcXPropertyId, 0); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->crtcYPropertyId, 0); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->crtcwidthPropertyId, output.modes[output.mode].hdisplay); + + drmModeAtomicAddProperty(request, output.eglfs_plane->id, + output.eglfs_plane->crtcheightPropertyId, output.modes[output.mode].vdisplay); +} +#endif + void QEglFSKmsGbmScreen::flip() { // For headless screen just return silently. It is not necessarily an error @@ -267,14 +374,24 @@ void QEglFSKmsGbmScreen::flip() m_gbm_bo_next = gbm_surface_lock_front_buffer(m_gbm_surface); if (!m_gbm_bo_next) { - qWarning("Could not lock GBM surface front buffer!"); + qWarning("Could not lock GBM surface front buffer for screen %s", qPrintable(name())); return; } + auto gbmRelease = qScopeGuard([this]{ + m_flipPending = false; + gbm_surface_release_buffer(m_gbm_surface, m_gbm_bo_next); + m_gbm_bo_next = nullptr; + }); + FrameBuffer *fb = framebufferForBufferObject(m_gbm_bo_next); + if (!fb) { + qWarning("FrameBuffer not available. Cannot flip"); + return; + } ensureModeSet(fb->fb); - QKmsOutput &op(output()); + const QKmsOutput &thisOutput(output()); const int fd = device()->fd(); m_flipPending = true; @@ -282,40 +399,27 @@ void QEglFSKmsGbmScreen::flip() #if QT_CONFIG(drm_atomic) drmModeAtomicReq *request = device()->threadLocalAtomicRequest(); if (request) { - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->framebufferPropertyId, fb->fb); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->crtcPropertyId, op.crtc_id); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->srcwidthPropertyId, - op.size.width() << 16); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->srcXPropertyId, 0); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->srcYPropertyId, 0); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->srcheightPropertyId, - op.size.height() << 16); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->crtcXPropertyId, 0); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->crtcYPropertyId, 0); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->crtcwidthPropertyId, - m_output.modes[m_output.mode].hdisplay); - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->crtcheightPropertyId, - m_output.modes[m_output.mode].vdisplay); - + addAtomicFlip(request, thisOutput, fb->fb); static int zpos = qEnvironmentVariableIntValue("QT_QPA_EGLFS_KMS_ZPOS"); - if (zpos) - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->zposPropertyId, zpos); + if (zpos) { + drmModeAtomicAddProperty(request, thisOutput.eglfs_plane->id, + thisOutput.eglfs_plane->zposPropertyId, zpos); + } static uint blendOp = uint(qEnvironmentVariableIntValue("QT_QPA_EGLFS_KMS_BLEND_OP")); - if (blendOp) - drmModeAtomicAddProperty(request, op.eglfs_plane->id, op.eglfs_plane->blendOpPropertyId, blendOp); + if (blendOp) { + drmModeAtomicAddProperty(request, thisOutput.eglfs_plane->id, + thisOutput.eglfs_plane->blendOpPropertyId, blendOp); + } } #endif } else { int ret = drmModePageFlip(fd, - op.crtc_id, - fb->fb, - DRM_MODE_PAGE_FLIP_EVENT, - this); + thisOutput.crtc_id, + fb->fb, + DRM_MODE_PAGE_FLIP_EVENT, + this); if (ret) { qErrnoWarning("Could not queue DRM page flip on screen %s", qPrintable(name())); - m_flipPending = false; - gbm_surface_release_buffer(m_gbm_surface, m_gbm_bo_next); - m_gbm_bo_next = nullptr; return; } } @@ -324,34 +428,46 @@ void QEglFSKmsGbmScreen::flip() if (d.screen != this) { d.screen->ensureModeSet(fb->fb); d.cloneFlipPending = true; + const QKmsOutput &destOutput(d.screen->output()); if (device()->hasAtomicSupport()) { #if QT_CONFIG(drm_atomic) drmModeAtomicReq *request = device()->threadLocalAtomicRequest(); - if (request) { - drmModeAtomicAddProperty(request, d.screen->output().eglfs_plane->id, - d.screen->output().eglfs_plane->framebufferPropertyId, fb->fb); - drmModeAtomicAddProperty(request, d.screen->output().eglfs_plane->id, - d.screen->output().eglfs_plane->crtcPropertyId, op.crtc_id); - } + if (request) + addAtomicFlip(request, destOutput, fb->fb); + + // ### This path is broken. On the other branch we can easily + // pass in d.screen as the user_data for drmModePageFlip, but + // using one atomic request breaks down here since we get events + // with the same user_data passed to drmModeAtomicCommit. Until + // this gets reworked (multiple requests?) screen cloning is not + // compatible with atomic. #endif } else { int ret = drmModePageFlip(fd, - d.screen->output().crtc_id, + destOutput.crtc_id, fb->fb, DRM_MODE_PAGE_FLIP_EVENT, d.screen); if (ret) { - qErrnoWarning("Could not queue DRM page flip for clone screen %s", qPrintable(name())); + qErrnoWarning("Could not queue DRM page flip for screen %s (clones screen %s)", + qPrintable(d.screen->name()), + qPrintable(name())); d.cloneFlipPending = false; } } } } + if (device()->hasAtomicSupport()) { #if QT_CONFIG(drm_atomic) - device()->threadLocalAtomicCommit(this); + if (!device()->threadLocalAtomicCommit(this)) { + return; + } #endif + } + + gbmRelease.dismiss(); } void QEglFSKmsGbmScreen::flipFinished() @@ -378,19 +494,23 @@ void QEglFSKmsGbmScreen::cloneDestFlipFinished(QEglFSKmsGbmScreen *cloneDestScre void QEglFSKmsGbmScreen::updateFlipStatus() { - Q_ASSERT(!m_cloneSource); + // only for 'real' outputs that own the color buffer, i.e. that are not cloning another one + if (m_cloneSource) + return; + // proceed only if flips for both this and all others that clone this have finished if (m_flipPending) return; - for (const CloneDestination &d : qAsConst(m_cloneDests)) { + for (const CloneDestination &d : std::as_const(m_cloneDests)) { if (d.cloneFlipPending) return; } - if (m_gbm_bo_current) + if (m_gbm_bo_current) { gbm_surface_release_buffer(m_gbm_surface, m_gbm_bo_current); + } m_gbm_bo_current = m_gbm_bo_next; m_gbm_bo_next = nullptr; diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen_p.h b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen_p.h index d2882038bd..aca34fcae2 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen_p.h +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen_p.h @@ -52,6 +52,12 @@ protected: void flipFinished(); void ensureModeSet(uint32_t fb); void cloneDestFlipFinished(QEglFSKmsGbmScreen *cloneDestScreen); + void waitForFlipWithEventReader(QEglFSKmsGbmScreen *screen); + static void nonThreadedPageFlipHandler(int fd, + unsigned int sequence, + unsigned int tv_sec, + unsigned int tv_usec, + void *user_data); gbm_surface *m_gbm_surface; @@ -61,6 +67,7 @@ protected: QMutex m_flipMutex; QWaitCondition m_flipCond; + static QMutex m_nonThreadedFlipMutex; QScopedPointer<QEglFSKmsGbmCursor> m_cursor; @@ -76,6 +83,8 @@ protected: bool cloneFlipPending = false; }; QList<CloneDestination> m_cloneDests; + + bool needsNewModeSetForNextFb = false; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/CMakeLists.txt index a9be917555..3c3e5a8a6c 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_kms_egldevice.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSKmsEglDeviceIntegrationPlugin Plugin: @@ -24,6 +25,3 @@ qt_internal_add_plugin(QEglFSKmsEglDeviceIntegrationPlugin Qt::GuiPrivate Qt::KmsSupportPrivate ) - -#### Keys ignored in scope 1:.:.:eglfs_kms_egldevice.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_kms_egldevice.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldeviceintegration.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldeviceintegration.cpp index 3a79ebb7b4..a213bc9bba 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldeviceintegration.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldeviceintegration.cpp @@ -38,8 +38,11 @@ EGLDisplay QEglFSKmsEglDeviceIntegration::createDisplay(EGLNativeDisplayType nat EGLDisplay display; + EGLint egldevice_fd = device()->fd(); + + const EGLint attribs[] = { EGL_DRM_MASTER_FD_EXT, egldevice_fd, EGL_NONE }; if (m_funcs->has_egl_platform_device) { - display = m_funcs->get_platform_display(EGL_PLATFORM_DEVICE_EXT, nativeDisplay, nullptr); + display = m_funcs->get_platform_display(EGL_PLATFORM_DEVICE_EXT, nativeDisplay, attribs); } else { qWarning("EGL_EXT_platform_device not available, falling back to legacy path!"); display = eglGetDisplay(nativeDisplay); @@ -168,7 +171,7 @@ void QEglFSKmsEglDeviceWindow::resetSurface() QByteArray reqLayerIndex = qgetenv("QT_QPA_EGLFS_LAYER_INDEX"); if (!reqLayerIndex.isEmpty()) { int idx = reqLayerIndex.toInt(); - if (idx >= 0 && idx < layers.count()) { + if (idx >= 0 && idx < layers.size()) { qCDebug(qLcEglfsKmsDebug, "EGLOutput layer index override = %d", idx); layer = layers[idx]; } diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.cpp index eadb079f70..3fc46cd224 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.cpp @@ -13,12 +13,63 @@ Q_DECLARE_LOGGING_CATEGORY(qLcEglfsKmsDebug) QEglFSKmsEglDeviceScreen::QEglFSKmsEglDeviceScreen(QEglFSKmsDevice *device, const QKmsOutput &output) : QEglFSKmsScreen(device, output) + , m_default_fb_handle(uint32_t(-1)) + , m_default_fb_id(uint32_t(-1)) { + const int fd = device->fd(); + + struct drm_mode_create_dumb createRequest; + createRequest.width = output.size.width(); + createRequest.height = output.size.height(); + createRequest.bpp = 32; + createRequest.flags = 0; + + qCDebug(qLcEglfsKmsDebug, "Creating dumb fb %dx%d", createRequest.width, createRequest.height); + + int ret = drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &createRequest); + if (ret < 0) + qFatal("Unable to create dumb buffer.\n"); + + m_default_fb_handle = createRequest.handle; + + uint32_t handles[4] = { 0, 0, 0, 0 }; + uint32_t pitches[4] = { 0, 0, 0, 0 }; + uint32_t offsets[4] = { 0, 0, 0, 0 }; + + handles[0] = createRequest.handle; + pitches[0] = createRequest.pitch; + offsets[0] = 0; + + ret = drmModeAddFB2(fd, createRequest.width, createRequest.height, DRM_FORMAT_ARGB8888, handles, + pitches, offsets, &m_default_fb_id, 0); + if (ret) + qFatal("Unable to add fb\n"); + + qCDebug(qLcEglfsKmsDebug, "Added dumb fb %dx%d handle:%u pitch:%d id:%u", createRequest.width, createRequest.height, + createRequest.handle, createRequest.pitch, m_default_fb_id); } QEglFSKmsEglDeviceScreen::~QEglFSKmsEglDeviceScreen() { - const int remainingScreenCount = qGuiApp->screens().count(); + int ret; + const int fd = device()->fd(); + + if (m_default_fb_id != uint32_t(-1)) { + ret = drmModeRmFB(fd, m_default_fb_id); + if (ret) + qErrnoWarning("drmModeRmFB failed"); + } + + if (m_default_fb_handle != uint32_t(-1)) { + struct drm_mode_destroy_dumb destroyRequest; + destroyRequest.handle = m_default_fb_handle; + + ret = drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroyRequest); + if (ret) + qErrnoWarning("DRM_IOCTL_MODE_DESTROY_DUMB failed"); + } + + const int remainingScreenCount = qGuiApp->screens().size(); qCDebug(qLcEglfsKmsDebug, "Screen dtor. Remaining screens: %d", remainingScreenCount); if (!remainingScreenCount && !device()->screenConfig()->separateScreens()) static_cast<QEglFSKmsEglDevice *>(device())->destroyGlobalCursor(); @@ -53,8 +104,11 @@ void QEglFSKmsEglDeviceScreen::waitForFlip() if (alreadySet) { // Maybe detecting the DPMS mode could help here, but there are no properties // exposed on the connector apparently. So rely on an env var for now. - static bool alwaysDoSet = qEnvironmentVariableIntValue("QT_QPA_EGLFS_ALWAYS_SET_MODE"); - if (!alwaysDoSet) { + // Note that typically, we need to set crtc with the default fb even if the + // mode did not change, unless QT_QPA_EGLFS_ALWAYS_SET_MODE is explicitly specified. + static bool envVarSet = false; + static bool alwaysDoSet = qEnvironmentVariableIntValue("QT_QPA_EGLFS_ALWAYS_SET_MODE", &envVarSet); + if (envVarSet && !alwaysDoSet) { qCDebug(qLcEglfsKmsDebug, "Mode already set"); return; } @@ -62,7 +116,7 @@ void QEglFSKmsEglDeviceScreen::waitForFlip() qCDebug(qLcEglfsKmsDebug, "Setting mode"); int ret = drmModeSetCrtc(fd, op.crtc_id, - uint32_t(-1), 0, 0, + m_default_fb_id, 0, 0, &op.connector_id, 1, &op.modes[op.mode]); if (ret) @@ -74,7 +128,7 @@ void QEglFSKmsEglDeviceScreen::waitForFlip() if (op.wants_forced_plane) { qCDebug(qLcEglfsKmsDebug, "Setting plane %u", op.forced_plane_id); - int ret = drmModeSetPlane(fd, op.forced_plane_id, op.crtc_id, uint32_t(-1), 0, + int ret = drmModeSetPlane(fd, op.forced_plane_id, op.crtc_id, m_default_fb_id, 0, 0, 0, w, h, 0 << 16, 0 << 16, op.size.width() << 16, op.size.height() << 16); if (ret == -1) diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.h b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.h index 884a70093a..8779499bef 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.h +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldevicescreen.h @@ -17,6 +17,9 @@ public: QPlatformCursor *cursor() const override; void waitForFlip() override; +private: + uint32_t m_default_fb_handle; + uint32_t m_default_fb_id; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/CMakeLists.txt index f9458ae5c6..5fcc0e1b18 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_kms_support.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## EglFsKmsSupportPrivate Module: @@ -23,4 +24,5 @@ qt_internal_add_module(EglFsKmsSupportPrivate Qt::Gui Qt::GuiPrivate Qt::KmsSupportPrivate + NO_GENERATE_CPP_EXPORTS ) diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsdevice_p.h b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsdevice_p.h index 7dc5235d33..6e11953a69 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsdevice_p.h +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsdevice_p.h @@ -17,7 +17,6 @@ // #include "private/qeglfsglobal_p.h" -#include "qeglfskmseventreader_p.h" #include <QtKmsSupport/private/qkmsdevice_p.h> QT_BEGIN_NAMESPACE @@ -31,11 +30,6 @@ public: bool isPrimary, const QPoint &virtualPos, const QList<QPlatformScreen *> &virtualSiblings) override; - - QEglFSKmsEventReader *eventReader() { return &m_eventReader; } - -protected: - QEglFSKmsEventReader m_eventReader; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsscreen.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsscreen.cpp index cc52d371e3..369356b761 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsscreen.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/qeglfskmsscreen.cpp @@ -185,7 +185,7 @@ QList<QPlatformScreen::Mode> QEglFSKmsScreen::modes() const QList<QPlatformScreen::Mode> list; list.reserve(m_output.modes.size()); - for (const drmModeModeInfo &info : qAsConst(m_output.modes)) + for (const drmModeModeInfo &info : std::as_const(m_output.modes)) list.append({QSize(info.hdisplay, info.vdisplay), qreal(info.vrefresh > 0 ? info.vrefresh : 60)}); diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qlinuxmediadevice.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qlinuxmediadevice.cpp index 44555b85b5..5d2900097e 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qlinuxmediadevice.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qlinuxmediadevice.cpp @@ -200,7 +200,7 @@ bool QLinuxMediaDevice::resetLinks() struct media_link *QLinuxMediaDevice::parseLink(const QString &link) { - char *endp = nullptr;; + char *endp = nullptr; struct media_link *mediaLink = media_parse_link(m_mediaDevice, link.toStdString().c_str(), &endp); if (!mediaLink) diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qvsp2blendingdevice.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qvsp2blendingdevice.cpp index 40cdd78e56..e7e3c76190 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qvsp2blendingdevice.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_vsp2/qvsp2blendingdevice.cpp @@ -205,7 +205,7 @@ bool QVsp2BlendingDevice::blend(int outputDmabufFd) if (queueingFailed) { qWarning() << "Vsp2: Trying to clean up queued buffers"; - for (auto &input : qAsConst(m_inputs)) { + for (auto &input : std::as_const(m_inputs)) { if (input.enabled) { if (!input.rpfInput->clearBuffers()) qWarning() << "Vsp2: Failed to remove buffers after an aborted blend"; @@ -248,7 +248,7 @@ int QVsp2BlendingDevice::numInputs() const bool QVsp2BlendingDevice::hasContinuousLayers() const { bool seenDisabled = false; - for (auto &input : qAsConst(m_inputs)) { + for (auto &input : std::as_const(m_inputs)) { if (seenDisabled && input.enabled) return false; seenDisabled |= !input.enabled; diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_mali/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_mali/CMakeLists.txt index 7550529f79..38981f87b9 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_mali/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_mali/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_mali.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSMaliIntegrationPlugin Plugin: @@ -21,6 +22,3 @@ qt_internal_add_plugin(QEglFSMaliIntegrationPlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:eglfs_mali.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_mali.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_openwfd/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_openwfd/CMakeLists.txt index fb8b79454d..7e2362e627 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_openwfd/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_openwfd/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from openwfd.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSOpenWFDIntegrationPlugin Plugin: @@ -19,6 +20,3 @@ qt_internal_add_plugin(QEglFSOpenWFDIntegrationPlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:openwfd.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_openwfd.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv/CMakeLists.txt index 988ab76d79..01defc9242 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_viv.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSVivIntegrationPlugin Plugin: @@ -22,6 +23,3 @@ qt_internal_add_plugin(QEglFSVivIntegrationPlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:eglfs_viv.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_viv.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv_wl/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv_wl/CMakeLists.txt index cb07c9aecc..6052e98ab8 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv_wl/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_viv_wl/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_viv_wl.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSVivWaylandIntegrationPlugin Plugin: @@ -23,6 +24,3 @@ qt_internal_add_plugin(QEglFSVivWaylandIntegrationPlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:eglfs_viv_wl.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_viv_wl.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/CMakeLists.txt index 18bb02efe0..9dde3090c8 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from eglfs_x11.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QEglFSX11IntegrationPlugin Plugin: @@ -23,7 +24,5 @@ qt_internal_add_plugin(QEglFSX11IntegrationPlugin X11::X11 X11::XCB XCB::XCB + NO_UNITY_BUILD # X11 define clashes ) - -#### Keys ignored in scope 1:.:.:eglfs_x11.pro:<TRUE>: -# OTHER_FILES = "$$PWD/eglfs_x11.json" diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/qeglfsx11integration.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/qeglfsx11integration.cpp index 39ec35df9e..8f491274d5 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/qeglfsx11integration.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_x11/qeglfsx11integration.cpp @@ -25,7 +25,7 @@ private: QEglFSX11Integration *m_integration; }; -static QBasicAtomicInt running; +Q_CONSTINIT static QBasicAtomicInt running = Q_BASIC_ATOMIC_INITIALIZER(0); void EventReader::run() { @@ -115,7 +115,7 @@ QSize QEglFSX11Integration::screenSize() const { if (m_screenSize.isEmpty()) { QList<QByteArray> env = qgetenv("EGLFS_X11_SIZE").split('x'); - if (env.length() == 2) { + if (env.size() == 2) { m_screenSize = QSize(env.at(0).toInt(), env.at(1).toInt()); } else { XWindowAttributes a; diff --git a/src/plugins/platforms/haiku/qhaikuintegration.cpp b/src/plugins/platforms/haiku/qhaikuintegration.cpp index 7a4ecbfcbf..2b0672e363 100644 --- a/src/plugins/platforms/haiku/qhaikuintegration.cpp +++ b/src/plugins/platforms/haiku/qhaikuintegration.cpp @@ -20,6 +20,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + static long int startApplicationThread(void *data) { QHaikuApplication *app = static_cast<QHaikuApplication*>(data); @@ -32,7 +34,7 @@ QHaikuIntegration::QHaikuIntegration(const QStringList ¶meters) { Q_UNUSED(parameters); - const QString signature = QStringLiteral("application/x-vnd.Qt.%1").arg(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); + const QString signature = "application/x-vnd.Qt.%1"_L1.arg(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); QHaikuApplication *app = new QHaikuApplication(signature.toLocal8Bit()); be_app = app; diff --git a/src/plugins/platforms/haiku/qhaikuwindow.cpp b/src/plugins/platforms/haiku/qhaikuwindow.cpp index 21bcad1749..3f2c6a889a 100644 --- a/src/plugins/platforms/haiku/qhaikuwindow.cpp +++ b/src/plugins/platforms/haiku/qhaikuwindow.cpp @@ -294,7 +294,7 @@ void QHaikuWindow::haikuWindowResized(const QSize &size, bool zoomInProgress) void QHaikuWindow::haikuWindowActivated(bool activated) { - QWindowSystemInterface::handleWindowActivated(activated ? window() : nullptr); + QWindowSystemInterface::handleFocusWindowChanged(activated ? window() : nullptr); } void QHaikuWindow::haikuWindowMinimized(bool minimize) diff --git a/src/plugins/platforms/ios/CMakeLists.txt b/src/plugins/platforms/ios/CMakeLists.txt index a55b30440a..51c1b52cf3 100644 --- a/src/plugins/platforms/ios/CMakeLists.txt +++ b/src/plugins/platforms/ios/CMakeLists.txt @@ -1,18 +1,32 @@ -# Generated from ios.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QIOSIntegrationPlugin Plugin: ##################################################################### +if(VISIONOS) + include(SwiftIntegration.cmake) + + qt_install(TARGETS QIOSIntegrationPluginSwift + EXPORT "${INSTALL_CMAKE_NAMESPACE}QIOSIntegrationPluginTargets" + DESTINATION "${INSTALL_LIBDIR}" + ) + qt_internal_add_targets_to_additional_targets_export_file( + TARGETS QIOSIntegrationPluginSwift + EXPORT_NAME_PREFIX "${INSTALL_CMAKE_NAMESPACE}QIOSIntegrationPlugin" + ) +endif() + qt_internal_add_plugin(QIOSIntegrationPlugin OUTPUT_NAME qios - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES ios # special case + STATIC # Force static, even in shared builds + DEFAULT_IF "ios" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms SOURCES plugin.mm qiosapplicationdelegate.h qiosapplicationdelegate.mm qiosapplicationstate.h qiosapplicationstate.mm - qioscontext.h qioscontext.mm qioseventdispatcher.h qioseventdispatcher.mm qiosglobal.h qiosglobal.mm qiosinputcontext.h qiosinputcontext.mm @@ -26,43 +40,66 @@ qt_internal_add_plugin(QIOSIntegrationPlugin qioswindow.h qioswindow.mm quiaccessibilityelement.h quiaccessibilityelement.mm quiview.h quiview.mm + quiwindow.mm quiwindow.h uistrings_p.h uistrings.cpp + NO_PCH_SOURCES + qioscontext.mm # undef QT_NO_FOREACH + qiosintegration.mm # undef QT_NO_FOREACH + qiosplatformaccessibility.mm # undef QT_NO_FOREACH + qiosscreen.mm # undef QT_NO_FOREACH LIBRARIES ${FWAudioToolbox} ${FWFoundation} ${FWMetal} ${FWQuartzCore} ${FWUIKit} + ${FWCoreGraphics} Qt::CorePrivate Qt::GuiPrivate ) -# special case begin qt_disable_apple_app_extension_api_only(QIOSIntegrationPlugin) -# special case end -#### Keys ignored in scope 2:.:.:kernel.pro:<TRUE>: -# OTHER_FILES = "quiview_textinput.mm" "quiview_accessibility.mm" ## Scopes: ##################################################################### +qt_internal_find_apple_system_framework(FWUniformTypeIdentifiers UniformTypeIdentifiers) qt_internal_extend_target(QIOSIntegrationPlugin CONDITION QT_FEATURE_opengl + SOURCES + qioscontext.h qioscontext.mm LIBRARIES Qt::OpenGLPrivate ) -qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION QT_FEATURE_clipboard SOURCES qiosclipboard.h qiosclipboard.mm - qiosdocumentpickercontroller.h qiosdocumentpickercontroller.mm +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS + SOURCES qiosfiledialog.h qiosfiledialog.mm - qiosmenu.h qiosmenu.mm + qiosdocumentpickercontroller.h qiosdocumentpickercontroller.mm + LIBRARIES + ${FWUniformTypeIdentifiers} + ${FWPhotos} +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS + SOURCES + qioscolordialog.h qioscolordialog.mm + qiosfontdialog.h qiosfontdialog.mm qiosmessagedialog.h qiosmessagedialog.mm +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT (TVOS OR VISIONOS) + SOURCES + qiosmenu.h qiosmenu.mm qiostextinputoverlay.h qiostextinputoverlay.mm - LIBRARIES - ${FWAssetsLibrary} ) -#### Keys ignored in scope 6:.:.:kernel.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" add_subdirectory(optional) + +if(VISIONOS) + target_link_libraries(QIOSIntegrationPlugin PRIVATE QIOSIntegrationPluginSwift) +endif() diff --git a/src/plugins/platforms/ios/SwiftIntegration.cmake b/src/plugins/platforms/ios/SwiftIntegration.cmake new file mode 100644 index 0000000000..d52edb3ad2 --- /dev/null +++ b/src/plugins/platforms/ios/SwiftIntegration.cmake @@ -0,0 +1,78 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +set(CMAKE_Swift_COMPILER_TARGET arm64-apple-xros) +if($CACHE{CMAKE_OSX_SYSROOT} MATCHES "^[a-z]+simulator$") + set(CMAKE_Swift_COMPILER_TARGET "${CMAKE_Swift_COMPILER_TARGET}-simulator") +endif() + +cmake_policy(SET CMP0157 NEW) +enable_language(Swift) + +# Verify that we have a new enough compiler +if("${CMAKE_Swift_COMPILER_VERSION}" VERSION_LESS 5.9) + message(FATAL_ERROR "Swift 5.9 required for C++ interoperability") +endif() + +get_target_property(QT_CORE_INCLUDES Qt6::Core INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_GUI_INCLUDES Qt6::Gui INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_CORE_PRIVATE_INCLUDES Qt6::CorePrivate INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_GUI_PRIVATE_INCLUDES Qt6::GuiPrivate INTERFACE_INCLUDE_DIRECTORIES) + +set(target QIOSIntegrationPluginSwift) +# Swift library +set(SWIFT_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/qiosapplication.swift" +) +add_library(${target} STATIC ${SWIFT_SOURCES}) +set_target_properties(${target} PROPERTIES + Swift_MODULE_NAME ${target}) +target_include_directories(${target} PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + "${QT_CORE_INCLUDES}" + "${QT_GUI_INCLUDES}" + "${QT_CORE_PRIVATE_INCLUDES}" + "${QT_GUI_PRIVATE_INCLUDES}" + +) +target_compile_options(${target} PUBLIC + $<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default> + $<$<COMPILE_LANGUAGE:Swift>:-Xcc -std=c++17>) + +# Swift to C++ bridging header +set(SWIFT_BRIDGING_HEADER "${CMAKE_CURRENT_BINARY_DIR}/qiosswiftintegration.h") +list(TRANSFORM QT_CORE_INCLUDES PREPEND "-I") +list(TRANSFORM QT_GUI_INCLUDES PREPEND "-I") +list(TRANSFORM QT_CORE_PRIVATE_INCLUDES PREPEND "-I") +list(TRANSFORM QT_GUI_PRIVATE_INCLUDES PREPEND "-I") +add_custom_command( + COMMAND + ${CMAKE_Swift_COMPILER} -frontend -typecheck + ${SWIFT_SOURCES} + -I ${CMAKE_CURRENT_SOURCE_DIR} + ${QT_CORE_INCLUDES} + ${QT_GUI_INCLUDES} + ${QT_CORE_PRIVATE_INCLUDES} + ${QT_GUI_PRIVATE_INCLUDES} + -sdk ${CMAKE_OSX_SYSROOT} + -module-name ${target} + -cxx-interoperability-mode=default + -Xcc -std=c++17 + -emit-clang-header-path "${SWIFT_BRIDGING_HEADER}" + -target ${CMAKE_Swift_COMPILER_TARGET} + OUTPUT + "${SWIFT_BRIDGING_HEADER}" + DEPENDS + ${SWIFT_SOURCES} + ) + +set(header_target "${target}Header") +add_custom_target(${header_target} + DEPENDS "${SWIFT_BRIDGING_HEADER}" +) +# Make sure the "'__bridge_transfer' casts have no effect when not using ARC" +# warning doesn't break warnings-are-error builds. +target_compile_options(${target} INTERFACE + -Wno-error=arc-bridge-casts-disallowed-in-nonarc) + +add_dependencies(${target} ${header_target}) diff --git a/src/plugins/platforms/ios/module.modulemap b/src/plugins/platforms/ios/module.modulemap new file mode 100644 index 0000000000..af42b3e1f5 --- /dev/null +++ b/src/plugins/platforms/ios/module.modulemap @@ -0,0 +1,4 @@ +module QIOSIntegrationPlugin { + header "qiosapplicationdelegate.h" + header "qiosintegration.h" +} diff --git a/src/plugins/platforms/ios/optional/CMakeLists.txt b/src/plugins/platforms/ios/optional/CMakeLists.txt index 6f5d754d4a..a01d7a6441 100644 --- a/src/plugins/platforms/ios/optional/CMakeLists.txt +++ b/src/plugins/platforms/ios/optional/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from optional.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(IOS) add_subdirectory(nsphotolibrarysupport) diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt index 2fceac77ab..663878bde7 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from nsphotolibrarysupport.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QIosOptionalPlugin_NSPhotoLibrary Plugin: @@ -6,6 +7,7 @@ qt_internal_add_plugin(QIosOptionalPlugin_NSPhotoLibraryPlugin OUTPUT_NAME qiosnsphotolibrarysupport + STATIC # Force static, even in shared builds PLUGIN_TYPE platforms/darwin CLASS_NAME QIosOptionalPlugin_NSPhotoLibrary DEFAULT_IF FALSE @@ -23,17 +25,7 @@ qt_internal_add_plugin(QIosOptionalPlugin_NSPhotoLibraryPlugin Qt::GuiPrivate ) -# special case begin set_target_properties(QIosOptionalPlugin_NSPhotoLibraryPlugin PROPERTIES DISABLE_PRECOMPILE_HEADERS ON ) -# special case end - - -#### Keys ignored in scope 1:.:.:nsphotolibrarysupport.pro:<TRUE>: -# OTHER_FILES = "plugin.json" -# PLUGIN_EXTENDS = "-" - -## Scopes: -##################################################################### diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h index 90b62af56d..0ad54a9e11 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h @@ -29,8 +29,8 @@ public: void setFileName(const QString &file) override; #ifndef QT_NO_FILESYSTEMITERATOR - Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override; - Iterator *endEntryList() override; + IteratorUniquePtr beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) override; #endif void setError(QFile::FileError error, const QString &str) { QAbstractFileEngine::setError(error, str); } diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm index 0ca911f68b..f7e112ab81 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm @@ -11,6 +11,8 @@ #include <QtCore/qurl.h> #include <QtCore/qset.h> #include <QtCore/qthreadstorage.h> +#include <QtCore/qfileselector.h> +#include <QtCore/qpointer.h> QT_BEGIN_NAMESPACE @@ -256,8 +258,8 @@ public: QIOSAssetEnumerator *m_enumerator; QIOSFileEngineIteratorAssetsLibrary( - QDir::Filters filters, const QStringList &nameFilters) - : QAbstractFileEngineIterator(filters, nameFilters) + const QString &path, QDir::Filters filters, const QStringList &nameFilters) + : QAbstractFileEngineIterator(path, filters, nameFilters) , m_enumerator(new QIOSAssetEnumerator([[[ALAssetsLibrary alloc] init] autorelease], ALAssetsGroupAll)) { } @@ -268,8 +270,11 @@ public: g_iteratorCurrentUrl.setLocalData(QString()); } - QString next() override + bool advance() override { + if (!m_enumerator->hasNext()) + return false; + // Cache the URL that we are about to return, since QDir will immediately create a // new file engine on the file and ask if it exists. Unless we do this, we end up // creating a new ALAsset just to verify its existence, which will be especially @@ -277,12 +282,7 @@ public: ALAsset *asset = m_enumerator->next(); QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString(); g_iteratorCurrentUrl.setLocalData(url); - return url; - } - - bool hasNext() const override - { - return m_enumerator->hasNext(); + return true; } QString currentFileName() const override @@ -344,6 +344,17 @@ QAbstractFileEngine::FileFlags QIOSFileEngineAssetsLibrary::fileFlags(QAbstractF { QAbstractFileEngine::FileFlags flags; const bool isDir = (m_assetUrl == "assets-library://"_L1); + if (!isDir) { + static const QFileSelector fileSelector; + static const auto selectors = fileSelector.allSelectors(); + if (m_assetUrl.startsWith("assets-library://"_L1)) { + for (const auto &selector : selectors) { + if (m_assetUrl.endsWith(selector)) + return flags; + } + } + } + const bool exists = isDir || m_assetUrl == g_iteratorCurrentUrl.localData() || loadAsset(); if (!exists) @@ -427,15 +438,11 @@ void QIOSFileEngineAssetsLibrary::setFileName(const QString &file) #ifndef QT_NO_FILESYSTEMITERATOR -QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::beginEntryList( - QDir::Filters filters, const QStringList &filterNames) -{ - return new QIOSFileEngineIteratorAssetsLibrary(filters, filterNames); -} - -QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::endEntryList() +QAbstractFileEngine::IteratorUniquePtr +QIOSFileEngineAssetsLibrary::beginEntryList( + const QString &path, QDir::Filters filters, const QStringList &filterNames) { - return 0; + return std::make_unique<QIOSFileEngineIteratorAssetsLibrary>(path, filters, filterNames); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h index caa852df9f..dfffbb8990 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h @@ -12,19 +12,22 @@ QT_BEGIN_NAMESPACE class QIOSFileEngineFactory : public QAbstractFileEngineHandler { + Q_DISABLE_COPY_MOVE(QIOSFileEngineFactory) public: - QAbstractFileEngine* create(const QString &fileName) const + QIOSFileEngineFactory() = default; + + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const { - static QLatin1StringView assetsScheme("assets-library:"); + Q_CONSTINIT static QLatin1StringView assetsScheme("assets-library:"); #ifndef Q_OS_TVOS if (fileName.toLower().startsWith(assetsScheme)) - return new QIOSFileEngineAssetsLibrary(fileName); + return std::make_unique<QIOSFileEngineAssetsLibrary>(fileName); #else Q_UNUSED(fileName); #endif - return 0; + return {}; } }; diff --git a/src/plugins/platforms/ios/qiosapplication.swift b/src/plugins/platforms/ios/qiosapplication.swift new file mode 100644 index 0000000000..6f75ebd0b5 --- /dev/null +++ b/src/plugins/platforms/ios/qiosapplication.swift @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import SwiftUI +import CompositorServices +import QIOSIntegrationPlugin +import RealityKit + +struct QIOSSwiftApplication: App { + @UIApplicationDelegateAdaptor private var appDelegate: QIOSApplicationDelegate + + var body: some SwiftUI.Scene { + WindowGroup() { + ImmersiveSpaceControlView() + } + + ImmersiveSpace(id: "QIOSImmersiveSpace") { + CompositorLayer(configuration: QIOSLayerConfiguration()) { layerRenderer in + QIOSIntegration.instance().renderCompositorLayer(layerRenderer) + } + } + // CompositorLayer immersive spaces are always full, and should not need + // to set the immersion style, but lacking this we get a warning in the + // console about not being able to "configure an immersive space with + // selected style 'AutomaticImmersionStyle' since it is not in the list + // of supported styles for this type of content: 'FullImmersionStyle'." + .immersionStyle(selection: .constant(.full), in: .full) + } +} + +public struct QIOSLayerConfiguration: CompositorLayerConfiguration { + public func makeConfiguration(capabilities: LayerRenderer.Capabilities, + configuration: inout LayerRenderer.Configuration) { + // Use reflection to pull out underlying C handles + // FIXME: Use proper bridging APIs when available + let capabilitiesMirror = Mirror(reflecting: capabilities) + let configurationMirror = Mirror(reflecting: configuration) + QIOSIntegration.instance().configureCompositorLayer( + capabilitiesMirror.descendant("c_capabilities") as? cp_layer_renderer_capabilities_t, + configurationMirror.descendant("box", "value") as? cp_layer_renderer_configuration_t + ) + } +} + +public func runSwiftAppMain() { + QIOSSwiftApplication.main() +} + +public class ImmersiveState: ObservableObject { + static let shared = ImmersiveState() + @Published var showImmersiveSpace: Bool = false +} + +struct ImmersiveSpaceControlView: View { + @ObservedObject private var immersiveState = ImmersiveState.shared + + @Environment(\.openImmersiveSpace) var openImmersiveSpace + @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace + + var body: some View { + VStack {} + .onChange(of: immersiveState.showImmersiveSpace) { _, newValue in + Task { + if newValue { + await openImmersiveSpace(id: "QIOSImmersiveSpace") + } else { + await dismissImmersiveSpace() + } + } + } + } +} + +public class ImmersiveSpaceManager : NSObject { + @objc public static func openImmersiveSpace() { + ImmersiveState.shared.showImmersiveSpace = true + } + + @objc public static func dismissImmersiveSpace() { + ImmersiveState.shared.showImmersiveSpace = false + } +} diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.h b/src/plugins/platforms/ios/qiosapplicationdelegate.h index 39bb9fdedb..7e12d64cbf 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.h +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.h @@ -1,6 +1,9 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QIOSAPPLICATIONDELEGATE_H +#define QIOSAPPLICATIONDELEGATE_H + #import <UIKit/UIKit.h> #import <QtGui/QtGui> @@ -8,3 +11,5 @@ @interface QIOSApplicationDelegate : UIResponder <UIApplicationDelegate> @end + +#endif // QIOSAPPLICATIONDELEGATE_H diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm index a017fef457..c6e5a83874 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm @@ -3,15 +3,21 @@ #include "qiosapplicationdelegate.h" +#include "qiosglobal.h" #include "qiosintegration.h" #include "qiosservices.h" #include "qiosviewcontroller.h" #include "qioswindow.h" +#include "qiosscreen.h" +#include "quiwindow.h" #include <qpa/qplatformintegration.h> #include <QtCore/QtCore> +@interface QIOSWindowSceneDelegate : NSObject<UIWindowSceneDelegate> +@end + @implementation QIOSApplicationDelegate - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *restorableObjects))restorationHandler @@ -50,5 +56,44 @@ return iosServices->handleUrl(QUrl::fromNSURL(url)); } +- (UISceneConfiguration *)application:(UIApplication *)application + configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession + options:(UISceneConnectionOptions *)options +{ + qCDebug(lcQpaWindowScene) << "Configuring scene for" << connectingSceneSession + << "with options" << options; + + auto *sceneConfig = connectingSceneSession.configuration; + sceneConfig.delegateClass = QIOSWindowSceneDelegate.class; + return sceneConfig; +} + @end +@implementation QIOSWindowSceneDelegate + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions +{ + qCDebug(lcQpaWindowScene) << "Connecting" << scene << "to" << session; + + Q_ASSERT([scene isKindOfClass:UIWindowScene.class]); + UIWindowScene *windowScene = static_cast<UIWindowScene*>(scene); + + QUIWindow *window = [[QUIWindow alloc] initWithWindowScene:windowScene]; + + QIOSScreen *screen = [&]{ + for (auto *screen : qGuiApp->screens()) { + auto *platformScreen = static_cast<QIOSScreen*>(screen->handle()); +#if !defined(Q_OS_VISIONOS) + if (platformScreen->uiScreen() == windowScene.screen) +#endif + return platformScreen; + } + Q_UNREACHABLE(); + }(); + + window.rootViewController = [[[QIOSViewController alloc] + initWithWindow:window andScreen:screen] autorelease]; +} + +@end diff --git a/src/plugins/platforms/ios/qiosclipboard.mm b/src/plugins/platforms/ios/qiosclipboard.mm index 554c3ca118..de8ab69dff 100644 --- a/src/plugins/platforms/ios/qiosclipboard.mm +++ b/src/plugins/platforms/ios/qiosclipboard.mm @@ -6,22 +6,11 @@ #ifndef QT_NO_CLIPBOARD #include <QtCore/qurl.h> -#include <QtGui/private/qmacmime_p.h> +#include <QtGui/private/qmacmimeregistry_p.h> +#include <QtGui/qutimimeconverter.h> #include <QtCore/QMimeData> #include <QtGui/QGuiApplication> -@interface UIPasteboard (QUIPasteboard) -+ (instancetype)pasteboardWithQClipboardMode:(QClipboard::Mode)mode; -@end - -@implementation UIPasteboard (QUIPasteboard) -+ (instancetype)pasteboardWithQClipboardMode:(QClipboard::Mode)mode -{ - NSString *name = (mode == QClipboard::Clipboard) ? UIPasteboardNameGeneral : UIPasteboardNameFind; - return [UIPasteboard pasteboardWithName:name create:NO]; -} -@end - // -------------------------------------------------------------------- @interface QUIClipboard : NSObject @@ -30,7 +19,6 @@ @implementation QUIClipboard { QIOSClipboard *m_qiosClipboard; NSInteger m_changeCountClipboard; - NSInteger m_changeCountFindBuffer; } - (instancetype)initWithQIOSClipboard:(QIOSClipboard *)qiosClipboard @@ -38,8 +26,7 @@ self = [super init]; if (self) { m_qiosClipboard = qiosClipboard; - m_changeCountClipboard = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::Clipboard].changeCount; - m_changeCountFindBuffer = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::FindBuffer].changeCount; + m_changeCountClipboard = UIPasteboard.generalPasteboard.changeCount; [[NSNotificationCenter defaultCenter] addObserver:self @@ -76,18 +63,12 @@ - (void)updatePasteboardChanged:(NSNotification *)notification { Q_UNUSED(notification); - NSInteger changeCountClipboard = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::Clipboard].changeCount; - NSInteger changeCountFindBuffer = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::FindBuffer].changeCount; + NSInteger changeCountClipboard = UIPasteboard.generalPasteboard.changeCount; if (m_changeCountClipboard != changeCountClipboard) { m_changeCountClipboard = changeCountClipboard; m_qiosClipboard->emitChanged(QClipboard::Clipboard); } - - if (m_changeCountFindBuffer != changeCountFindBuffer) { - m_changeCountFindBuffer = changeCountFindBuffer; - m_qiosClipboard->emitChanged(QClipboard::FindBuffer); - } } @end @@ -98,25 +79,22 @@ QT_BEGIN_NAMESPACE class QIOSMimeData : public QMimeData { public: - QIOSMimeData(QClipboard::Mode mode) : QMimeData(), m_mode(mode) { } + QIOSMimeData() : QMimeData() { } ~QIOSMimeData() { } QStringList formats() const override; QVariant retrieveData(const QString &mimeType, QMetaType type) const override; - -private: - const QClipboard::Mode m_mode; }; QStringList QIOSMimeData::formats() const { QStringList foundMimeTypes; - UIPasteboard *pb = [UIPasteboard pasteboardWithQClipboardMode:m_mode]; + UIPasteboard *pb = UIPasteboard.generalPasteboard; NSArray<NSString *> *pasteboardTypes = [pb pasteboardTypes]; for (NSUInteger i = 0; i < [pasteboardTypes count]; ++i) { - QString uti = QString::fromNSString([pasteboardTypes objectAtIndex:i]); - QString mimeType = QMacInternalPasteboardMime::flavorToMime(QMacInternalPasteboardMime::MIME_ALL, uti); + const QString uti = QString::fromNSString([pasteboardTypes objectAtIndex:i]); + const QString mimeType = QMacMimeRegistry::flavorToMime(QUtiMimeConverter::HandlerScopeFlag::All, uti); if (!mimeType.isEmpty() && !foundMimeTypes.contains(mimeType)) foundMimeTypes << mimeType; } @@ -126,17 +104,14 @@ QStringList QIOSMimeData::formats() const QVariant QIOSMimeData::retrieveData(const QString &mimeType, QMetaType) const { - UIPasteboard *pb = [UIPasteboard pasteboardWithQClipboardMode:m_mode]; + UIPasteboard *pb = UIPasteboard.generalPasteboard; NSArray<NSString *> *pasteboardTypes = [pb pasteboardTypes]; - foreach (QMacInternalPasteboardMime *converter, - QMacInternalPasteboardMime::all(QMacInternalPasteboardMime::MIME_ALL)) { - if (!converter->canConvert(mimeType, converter->flavorFor(mimeType))) - continue; - + const auto converters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All); + for (QUtiMimeConverter *converter : converters) { for (NSUInteger i = 0; i < [pasteboardTypes count]; ++i) { NSString *availableUtiNSString = [pasteboardTypes objectAtIndex:i]; - QString availableUti = QString::fromNSString(availableUtiNSString); + const QString availableUti = QString::fromNSString(availableUtiNSString); if (!converter->canConvert(mimeType, availableUti)) continue; @@ -166,7 +141,7 @@ QMimeData *QIOSClipboard::mimeData(QClipboard::Mode mode) { Q_ASSERT(supportsMode(mode)); if (!m_mimeData.contains(mode)) - return *m_mimeData.insert(mode, new QIOSMimeData(mode)); + return *m_mimeData.insert(mode, new QIOSMimeData); return m_mimeData[mode]; } @@ -174,7 +149,7 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) { Q_ASSERT(supportsMode(mode)); - UIPasteboard *pb = [UIPasteboard pasteboardWithQClipboardMode:mode]; + UIPasteboard *pb = UIPasteboard.generalPasteboard; if (!mimeData) { pb.items = [NSArray<NSDictionary<NSString *, id> *> array]; return; @@ -183,26 +158,29 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) mimeData->deleteLater(); NSMutableDictionary<NSString *, id> *pbItem = [NSMutableDictionary<NSString *, id> dictionaryWithCapacity:mimeData->formats().size()]; - foreach (const QString &mimeType, mimeData->formats()) { - foreach (QMacInternalPasteboardMime *converter, - QMacInternalPasteboardMime::all(QMacInternalPasteboardMime::MIME_ALL)) { - QString uti = converter->flavorFor(mimeType); - if (uti.isEmpty() || !converter->canConvert(mimeType, uti)) + const auto formats = mimeData->formats(); + for (const QString &mimeType : formats) { + const auto converters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All); + for (const QUtiMimeConverter *converter : converters) { + const QString uti = converter->utiForMime(mimeType); + if (uti.isEmpty()) continue; QVariant mimeDataAsVariant; if (mimeData->hasImage()) { mimeDataAsVariant = mimeData->imageData(); } else if (mimeData->hasUrls()) { + const auto urls = mimeData->urls(); QVariantList urlList; - for (QUrl url : mimeData->urls()) + urlList.reserve(urls.size()); + for (const QUrl& url : urls) urlList << url; mimeDataAsVariant = QVariant(urlList); } else { mimeDataAsVariant = QVariant(mimeData->data(mimeType)); } - QByteArray byteArray = converter->convertFromMime(mimeType, mimeDataAsVariant, uti).first(); + QByteArray byteArray = converter->convertFromMime(mimeType, mimeDataAsVariant, uti).constFirst(); NSData *nsData = [NSData dataWithBytes:byteArray.constData() length:byteArray.size()]; [pbItem setValue:nsData forKey:uti.toNSString()]; break; @@ -214,7 +192,7 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) bool QIOSClipboard::supportsMode(QClipboard::Mode mode) const { - return (mode == QClipboard::Clipboard || mode == QClipboard::FindBuffer); + return mode == QClipboard::Clipboard; } bool QIOSClipboard::ownsMode(QClipboard::Mode mode) const diff --git a/src/plugins/platforms/ios/qioscolordialog.h b/src/plugins/platforms/ios/qioscolordialog.h new file mode 100644 index 0000000000..1af718949b --- /dev/null +++ b/src/plugins/platforms/ios/qioscolordialog.h @@ -0,0 +1,38 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QIOSCOLORDIALOG_H +#define QIOSCOLORDIALOG_H + +#include <QtCore/qeventloop.h> +#include <qpa/qplatformdialoghelper.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(QIOSColorDialogController); + +QT_BEGIN_NAMESPACE + +class QIOSColorDialog : public QPlatformColorDialogHelper +{ +public: + QIOSColorDialog(); + ~QIOSColorDialog(); + + void exec() override; + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + + void setCurrentColor(const QColor&) override; + QColor currentColor() const override; + + void updateColor(const QColor&); + +private: + QEventLoop m_eventLoop; + QIOSColorDialogController *m_viewController; + QColor m_currentColor; +}; + +QT_END_NAMESPACE + +#endif // QIOSCOLORDIALOG_H + diff --git a/src/plugins/platforms/ios/qioscolordialog.mm b/src/plugins/platforms/ios/qioscolordialog.mm new file mode 100644 index 0000000000..6651b1791d --- /dev/null +++ b/src/plugins/platforms/ios/qioscolordialog.mm @@ -0,0 +1,159 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#import <UIKit/UIKit.h> + +#include <QtGui/qwindow.h> +#include <QDebug> + +#include <QtCore/private/qcore_mac_p.h> + +#include "qiosglobal.h" +#include "qioscolordialog.h" +#include "qiosintegration.h" + +@interface QIOSColorDialogController : UIColorPickerViewController <UIColorPickerViewControllerDelegate, + UIAdaptivePresentationControllerDelegate> +@end + +@implementation QIOSColorDialogController { + QIOSColorDialog *m_colorDialog; +} + +- (instancetype)initWithQIOSColorDialog:(QIOSColorDialog *)dialog +{ + if (self = [super init]) { + m_colorDialog = dialog; + self.delegate = self; + self.presentationController.delegate = self; + self.supportsAlpha = dialog->options()->testOption(QColorDialogOptions::ShowAlphaChannel); + } + return self; +} + +- (void)setQColor:(const QColor &)qColor +{ + UIColor *uiColor; + const QColor::Spec spec = qColor.spec(); + if (spec == QColor::Hsv) { + uiColor = [UIColor colorWithHue:qColor.hsvHueF() + saturation:qColor.hsvSaturationF() + brightness:qColor.valueF() + alpha:qColor.alphaF()]; + } else { + uiColor = [UIColor colorWithRed:qColor.redF() + green:qColor.greenF() + blue:qColor.blueF() + alpha:qColor.alphaF()]; + } + self.selectedColor = uiColor; +} + +- (void)updateQColor +{ + UIColor *color = self.selectedColor; + CGFloat red = 0, green = 0, blue = 0, alpha = 0; + + QColor newColor; + if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) + newColor.setRgbF(red, green, blue, alpha); + else + qWarning() << "Incompatible color space"; + + + if (m_colorDialog) { + m_colorDialog->updateColor(newColor); + emit m_colorDialog->currentColorChanged(newColor); + } +} + +// ----------------------UIColorPickerViewControllerDelegate-------------------------- +- (void)colorPickerViewControllerDidSelectColor:(UIColorPickerViewController *)viewController +{ + Q_UNUSED(viewController); + [self updateQColor]; +} + +- (void)colorPickerViewControllerDidFinish:(UIColorPickerViewController *)viewController +{ + Q_UNUSED(viewController); + [self updateQColor]; + emit m_colorDialog->accept(); +} + +// ----------------------UIAdaptivePresentationControllerDelegate-------------------------- +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController +{ + Q_UNUSED(presentationController); + emit m_colorDialog->reject(); +} + +@end + +QIOSColorDialog::QIOSColorDialog() + : m_viewController(nullptr) +{ +} + +QIOSColorDialog::~QIOSColorDialog() +{ + hide(); +} + +void QIOSColorDialog::exec() +{ + m_eventLoop.exec(QEventLoop::DialogExec); +} + +bool QIOSColorDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) +{ + Q_UNUSED(windowFlags); + + if (!m_viewController) { + m_viewController = [[QIOSColorDialogController alloc] initWithQIOSColorDialog:this]; + if (m_currentColor.isValid()) + [m_viewController setQColor:m_currentColor]; + } + + if (windowModality == Qt::ApplicationModal || windowModality == Qt::WindowModal) + m_viewController.modalInPresentation = YES; + + UIWindow *window = presentationWindow(parent); + if (!window) + return false; + + // We can't present from view controller if already presenting + if (window.rootViewController.presentedViewController) + return false; + + [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; + + return true; +} + +void QIOSColorDialog::hide() +{ + [m_viewController dismissViewControllerAnimated:YES completion:nil]; + [m_viewController release]; + m_viewController = nullptr; + m_eventLoop.exit(); +} + +void QIOSColorDialog::setCurrentColor(const QColor &color) +{ + updateColor(color); + if (m_viewController) + [m_viewController setQColor:color]; +} + +QColor QIOSColorDialog::currentColor() const +{ + return m_currentColor; +} + +void QIOSColorDialog::updateColor(const QColor &color) +{ + m_currentColor = color; +} + + diff --git a/src/plugins/platforms/ios/qioscontext.mm b/src/plugins/platforms/ios/qioscontext.mm index 9defb01bfa..499adea0fe 100644 --- a/src/plugins/platforms/ios/qioscontext.mm +++ b/src/plugins/platforms/ios/qioscontext.mm @@ -1,6 +1,8 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qioscontext.h" #include "qiosintegration.h" @@ -332,6 +334,6 @@ bool QIOSContext::isSharing() const return m_sharedContext; } -#include "moc_qioscontext.cpp" - QT_END_NAMESPACE + +#include "moc_qioscontext.cpp" diff --git a/src/plugins/platforms/ios/qiosdocumentpickercontroller.h b/src/plugins/platforms/ios/qiosdocumentpickercontroller.h index 9182a699b3..f0b7472539 100644 --- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.h +++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #import <UIKit/UIKit.h> +#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> #include "qiosfiledialog.h" diff --git a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm index 6421895624..09e2f2f4c3 100644 --- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm +++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm @@ -12,27 +12,37 @@ - (instancetype)initWithQIOSFileDialog:(QIOSFileDialog *)fileDialog { - NSMutableArray <NSString *> *docTypes = [[[NSMutableArray alloc] init] autorelease]; - UIDocumentPickerMode importMode; - switch (fileDialog->options()->fileMode()) { - case QFileDialogOptions::AnyFile: - case QFileDialogOptions::ExistingFile: - case QFileDialogOptions::ExistingFiles: - [docTypes addObject:(__bridge NSString *)kUTTypeContent]; - [docTypes addObject:(__bridge NSString *)kUTTypeItem]; - [docTypes addObject:(__bridge NSString *)kUTTypeData]; - importMode = UIDocumentPickerModeImport; - break; - case QFileDialogOptions::Directory: - case QFileDialogOptions::DirectoryOnly: - // Directory picking is not supported because it requires - // special handling not possible with the current QFilePicker - // implementation. - - Q_UNREACHABLE(); + NSMutableArray <UTType *> *docTypes = [[[NSMutableArray alloc] init] autorelease]; + + QStringList nameFilters = fileDialog->options()->nameFilters(); + if (!nameFilters.isEmpty() && (fileDialog->options()->fileMode() != QFileDialogOptions::Directory + || fileDialog->options()->fileMode() != QFileDialogOptions::DirectoryOnly)) + { + QStringList results; + for (const QString &filter : nameFilters) + results.append(QPlatformFileDialogHelper::cleanFilterList(filter)); + + docTypes = [self computeAllowedFileTypes:results]; + } + + if (!docTypes.count) { + switch (fileDialog->options()->fileMode()) { + case QFileDialogOptions::AnyFile: + case QFileDialogOptions::ExistingFile: + case QFileDialogOptions::ExistingFiles: + [docTypes addObject:UTTypeContent]; + [docTypes addObject:UTTypeItem]; + [docTypes addObject:UTTypeData]; + break; + // Showing files is not supported in Directory mode in iOS + case QFileDialogOptions::Directory: + case QFileDialogOptions::DirectoryOnly: + [docTypes addObject:UTTypeFolder]; + break; + } } - if (self = [super initWithDocumentTypes:docTypes inMode:importMode]) { + if (self = [super initForOpeningContentTypes:docTypes]) { m_fileDialog = fileDialog; self.modalPresentationStyle = UIModalPresentationFormSheet; self.delegate = self; @@ -41,8 +51,7 @@ if (m_fileDialog->options()->fileMode() == QFileDialogOptions::ExistingFiles) self.allowsMultipleSelection = YES; - if (@available(ios 13.0, *)) - self.directoryURL = m_fileDialog->options()->initialDirectory().toNSURL(); + self.directoryURL = m_fileDialog->options()->initialDirectory().toNSURL(); } return self; } @@ -79,4 +88,28 @@ emit m_fileDialog->reject(); } +- (NSMutableArray<UTType*>*)computeAllowedFileTypes:(QStringList)filters +{ + QStringList fileTypes; + for (const QString &filter : filters) { + if (filter == (QLatin1String("*"))) + continue; + + if (filter.contains(u'?')) + continue; + + if (filter.count(u'*') != 1) + continue; + + auto extensions = filter.split('.', Qt::SkipEmptyParts); + fileTypes += extensions.last(); + } + + NSMutableArray<UTType *> *result = [NSMutableArray<UTType *> arrayWithCapacity:fileTypes.size()]; + for (const QString &string : fileTypes) + [result addObject:[UTType typeWithFilenameExtension:string.toNSString()]]; + + return result; +} + @end diff --git a/src/plugins/platforms/ios/qioseventdispatcher.h b/src/plugins/platforms/ios/qioseventdispatcher.h index b40024ec19..5eee0556f5 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.h +++ b/src/plugins/platforms/ios/qioseventdispatcher.h @@ -16,6 +16,8 @@ public: static QIOSEventDispatcher* create(); bool processPostedEvents() override; + static bool isQtApplication(); + protected: explicit QIOSEventDispatcher(QObject *parent = nullptr); }; diff --git a/src/plugins/platforms/ios/qioseventdispatcher.mm b/src/plugins/platforms/ios/qioseventdispatcher.mm index 24d9d88294..710a834bfd 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.mm +++ b/src/plugins/platforms/ios/qioseventdispatcher.mm @@ -5,6 +5,10 @@ #include "qiosapplicationdelegate.h" #include "qiosglobal.h" +#if defined(Q_OS_VISIONOS) +#include "qiosswiftintegration.h" +#endif + #include <QtCore/qprocessordetection.h> #include <QtCore/private/qcoreapplication_p.h> #include <QtCore/private/qthread_p.h> @@ -173,12 +177,16 @@ namespace QAppleLogActivity UIApplicationMain; QAppleLogActivity applicationDidFinishLaunching; } logActivity; + + static bool s_isQtApplication = false; } using namespace QT_PREPEND_NAMESPACE(QtPrivate); extern "C" int qt_main_wrapper(int argc, char *argv[]) { + s_isQtApplication = true; + @autoreleasepool { size_t defaultStackSize = 512 * kBytesPerKiloByte; // Same as secondary threads @@ -202,8 +210,16 @@ extern "C" int qt_main_wrapper(int argc, char *argv[]) logActivity.UIApplicationMain = QT_APPLE_LOG_ACTIVITY( lcEventDispatcher().isDebugEnabled(), "UIApplicationMain").enter(); +#if defined(Q_OS_VISIONOS) + Q_UNUSED(argc); + Q_UNUSED(argv); + qCDebug(lcEventDispatcher) << "Starting Swift app"; + QIOSIntegrationPluginSwift::runSwiftAppMain(); + Q_UNREACHABLE(); +#else qCDebug(lcEventDispatcher) << "Running UIApplicationMain"; return UIApplicationMain(argc, argv, nil, NSStringFromClass([QIOSApplicationDelegate class])); +#endif } } @@ -424,6 +440,11 @@ QIOSEventDispatcher::QIOSEventDispatcher(QObject *parent) QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } +bool QIOSEventDispatcher::isQtApplication() +{ + return s_isQtApplication; +} + /*! Override of the CoreFoundation posted events runloop source callback so that we can send window system (QPA) events in addition to sending diff --git a/src/plugins/platforms/ios/qiosfiledialog.h b/src/plugins/platforms/ios/qiosfiledialog.h index 12a3af7181..f00c154c03 100644 --- a/src/plugins/platforms/ios/qiosfiledialog.h +++ b/src/plugins/platforms/ios/qiosfiledialog.h @@ -39,6 +39,7 @@ private: bool showImagePickerDialog(QWindow *parent); bool showNativeDocumentPickerDialog(QWindow *parent); + void showImagePickerDialog_helper(QWindow *parent); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosfiledialog.mm b/src/plugins/platforms/ios/qiosfiledialog.mm index 64e9445c43..cf9580c17e 100644 --- a/src/plugins/platforms/ios/qiosfiledialog.mm +++ b/src/plugins/platforms/ios/qiosfiledialog.mm @@ -3,17 +3,22 @@ #import <UIKit/UIKit.h> +#import <Photos/Photos.h> + #include <QtCore/qstandardpaths.h> #include <QtGui/qwindow.h> #include <QDebug> #include <QtCore/private/qcore_mac_p.h> +#include "qiosglobal.h" #include "qiosfiledialog.h" #include "qiosintegration.h" #include "qiosoptionalplugininterface.h" #include "qiosdocumentpickercontroller.h" +#include <QtCore/qpointer.h> + using namespace Qt::StringLiterals; QIOSFileDialog::QIOSFileDialog() @@ -36,11 +41,15 @@ bool QIOSFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window Q_UNUSED(windowFlags); Q_UNUSED(windowModality); - bool acceptOpen = options()->acceptMode() == QFileDialogOptions::AcceptOpen; - QString directory = options()->initialDirectory().toLocalFile(); + const bool acceptOpen = options()->acceptMode() == QFileDialogOptions::AcceptOpen; + const auto initialDir = options()->initialDirectory(); + const QString directory = initialDir.toLocalFile(); + // We manually add assets-library:// to the list of paths, + // when converted to QUrl, it becames a scheme. + const QString scheme = initialDir.scheme(); if (acceptOpen) { - if (directory.startsWith("assets-library:"_L1)) + if (directory.startsWith("assets-library:"_L1) || scheme == "assets-library"_L1) return showImagePickerDialog(parent); else return showNativeDocumentPickerDialog(parent); @@ -49,6 +58,12 @@ bool QIOSFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window return false; } +void QIOSFileDialog::showImagePickerDialog_helper(QWindow *parent) +{ + UIWindow *window = presentationWindow(parent); + [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; +} + bool QIOSFileDialog::showImagePickerDialog(QWindow *parent) { if (!m_viewController) { @@ -67,9 +82,38 @@ bool QIOSFileDialog::showImagePickerDialog(QWindow *parent) return false; } - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window - : qt_apple_sharedApplication().keyWindow; - [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; + // "Old style" authorization (deprecated, but we have to work with AssetsLibrary anyway). + // + // From the documentation: + // "The authorizationStatus and requestAuthorization: methods aren’t compatible with the + // limited library and return PHAuthorizationStatusAuthorized when the user authorizes your + // app for limited access only." + // + // This is good enough for us. + + const auto authStatus = [PHPhotoLibrary authorizationStatus]; + if (authStatus == PHAuthorizationStatusAuthorized) { + showImagePickerDialog_helper(parent); + } else if (authStatus == PHAuthorizationStatusNotDetermined) { + QPointer<QWindow> winGuard(parent); + QPointer<QIOSFileDialog> thisGuard(this); + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + if (thisGuard && winGuard) + thisGuard->showImagePickerDialog_helper(winGuard); + + } else if (thisGuard) { + emit thisGuard->reject(); + } + }); + }]; + } else { + // Treat 'Limited' (we don't know how to deal with anyway) and 'Denied' as errors. + // FIXME: logging category? + qWarning() << "QIOSFileDialog: insufficient permission, cannot pick images"; + return false; + } return true; } @@ -77,14 +121,9 @@ bool QIOSFileDialog::showImagePickerDialog(QWindow *parent) bool QIOSFileDialog::showNativeDocumentPickerDialog(QWindow *parent) { #ifndef Q_OS_TVOS - if (options()->fileMode() == QFileDialogOptions::Directory || - options()->fileMode() == QFileDialogOptions::DirectoryOnly) - return false; - m_viewController = [[QIOSDocumentPickerController alloc] initWithQIOSFileDialog:this]; - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window - : qt_apple_sharedApplication().keyWindow; + UIWindow *window = presentationWindow(parent); [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; return true; diff --git a/src/plugins/platforms/ios/qiosfontdialog.h b/src/plugins/platforms/ios/qiosfontdialog.h new file mode 100644 index 0000000000..f0a92d0d6f --- /dev/null +++ b/src/plugins/platforms/ios/qiosfontdialog.h @@ -0,0 +1,41 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QIOSFONTDIALOG_H +#define QIOSFONTDIALOG_H + +#include <QtCore/qeventloop.h> +#include <qpa/qplatformdialoghelper.h> + +@interface QIOSFontDialogController : UIFontPickerViewController <UIFontPickerViewControllerDelegate, + UIAdaptivePresentationControllerDelegate> +@end + +QT_BEGIN_NAMESPACE + +class QIOSFontDialog : public QPlatformFontDialogHelper +{ +public: + QIOSFontDialog(); + ~QIOSFontDialog(); + + void exec() override; + + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + + void setCurrentFont(const QFont &) override; + QFont currentFont() const override; + + void updateCurrentFont(const QFont &); + +private: + QEventLoop m_eventLoop; + QIOSFontDialogController *m_viewController; + QFont m_currentFont; + +}; + +QT_END_NAMESPACE + +#endif // QIOSFONTDIALOG_H diff --git a/src/plugins/platforms/ios/qiosfontdialog.mm b/src/plugins/platforms/ios/qiosfontdialog.mm new file mode 100644 index 0000000000..25d0197195 --- /dev/null +++ b/src/plugins/platforms/ios/qiosfontdialog.mm @@ -0,0 +1,190 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#import <UIKit/UIKit.h> + +#include <QtGui/qwindow.h> +#include <QtGui/qfontdatabase.h> +#include <QDebug> + +#include <QtCore/private/qcore_mac_p.h> +#include <QtGui/private/qfont_p.h> +#include <QtGui/private/qfontengine_p.h> + +#include "qiosglobal.h" +#include "qiosfontdialog.h" +#include "qiosintegration.h" + +@implementation QIOSFontDialogController { + QIOSFontDialog *m_fontDialog; +} + +- (instancetype)initWithQIOSFontDialog:(QIOSFontDialog *)dialog +{ + UIFontPickerViewControllerConfiguration *configuration = [[UIFontPickerViewControllerConfiguration alloc] init]; + if (dialog->options()->testOption(QFontDialogOptions::MonospacedFonts)) { + UIFontDescriptorSymbolicTraits traits = {}; + traits |= UIFontDescriptorTraitMonoSpace; + configuration.filteredTraits = traits; + } + configuration.includeFaces = YES; + if (self = [super initWithConfiguration:configuration]) { + m_fontDialog = dialog; + self.delegate = self; + self.presentationController.delegate = self; + } + [configuration release]; + return self; +} + +- (void)setQFont:(const QFont &)font +{ + QFontInfo fontInfo(font); + auto family = fontInfo.family().toNSString(); + auto size = fontInfo.pointSize(); + + NSDictionary *dictionary = @{ + static_cast<NSString *>(UIFontDescriptorFamilyAttribute): family, + static_cast<NSString *>(UIFontDescriptorSizeAttribute): [NSNumber numberWithInt:size] + }; + UIFontDescriptor *fd = [UIFontDescriptor fontDescriptorWithFontAttributes:dictionary]; + + UIFontDescriptorSymbolicTraits traits = 0; + if (font.style() == QFont::StyleItalic) + traits |= UIFontDescriptorTraitItalic; + if (font.weight() == QFont::Bold) + traits |= UIFontDescriptorTraitBold; + fd = [fd fontDescriptorWithSymbolicTraits:traits]; + + self.selectedFontDescriptor = fd; +} + +- (void)updateQFont +{ + UIFontDescriptor *font = self.selectedFontDescriptor; + if (!font) + return; + + NSDictionary *attributes = font.fontAttributes; + UIFontDescriptorSymbolicTraits traits = font.symbolicTraits; + + QFont newFont; + int size = qRound(font.pointSize); + QString family = QString::fromNSString([attributes objectForKey:UIFontDescriptorFamilyAttribute]); + if (family.isEmpty()) { + // If includeFaces is true, then the font descriptor won't + // have the UIFontDescriptorFamilyAttribute key set so we + // need to create a UIFont to get the font family + UIFont *f = [UIFont fontWithDescriptor:font size:size]; + family = QString::fromNSString(f.familyName); + } + + QString style; + if ((traits & (UIFontDescriptorTraitItalic | UIFontDescriptorTraitBold)) == (UIFontDescriptorTraitItalic | UIFontDescriptorTraitBold)) + style = "Bold Italic"; + else if (traits & UIFontDescriptorTraitItalic) + style = "Italic"; + else if (traits & UIFontDescriptorTraitBold) + style = "Bold"; + + newFont = QFontDatabase::font(family, style, size); + + if (m_fontDialog) { + m_fontDialog->updateCurrentFont(newFont); + emit m_fontDialog->currentFontChanged(newFont); + } +} + +// ----------------------UIFontPickerViewControllerDelegate-------------------------- +- (void)fontPickerViewControllerDidPickFont:(UIFontPickerViewController *)viewController +{ + [self updateQFont]; + emit m_fontDialog->accept(); +} + +- (void)fontPickerViewControllerDidCancel:(UIFontPickerViewController *)viewController +{ + Q_UNUSED(viewController); + emit m_fontDialog->reject(); +} + +// ----------------------UIAdaptivePresentationControllerDelegate-------------------------- +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController +{ + Q_UNUSED(presentationController); + emit m_fontDialog->reject(); +} + +@end + +QIOSFontDialog::QIOSFontDialog() + : m_viewController(nullptr) +{ +} + +QIOSFontDialog::~QIOSFontDialog() +{ + hide(); +} + +void QIOSFontDialog::exec() +{ + m_eventLoop.exec(QEventLoop::DialogExec); +} + +bool QIOSFontDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) +{ + Q_UNUSED(windowFlags); + Q_UNUSED(windowModality); + + if (!m_viewController) { + m_viewController = [[QIOSFontDialogController alloc] initWithQIOSFontDialog:this]; + [m_viewController setQFont:m_currentFont]; + } + + if (windowModality == Qt::ApplicationModal || windowModality == Qt::WindowModal) + m_viewController.modalInPresentation = YES; + + UIWindow *window = presentationWindow(parent); + if (!window) + return false; + + // We can't present from view controller if already presenting + if (window.rootViewController.presentedViewController) + return false; + + [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; + + return true; +} + +void QIOSFontDialog::hide() +{ + [m_viewController dismissViewControllerAnimated:YES completion:nil]; + [m_viewController release]; + m_viewController = nullptr; + if (m_eventLoop.isRunning()) + m_eventLoop.exit(); +} + +void QIOSFontDialog::setCurrentFont(const QFont &font) +{ + if (m_currentFont == font) + return; + + m_currentFont = font; + if (m_viewController) + [m_viewController setQFont:font]; +} + +QFont QIOSFontDialog::currentFont() const +{ + return m_currentFont; +} + +void QIOSFontDialog::updateCurrentFont(const QFont &font) +{ + m_currentFont = font; +} + + diff --git a/src/plugins/platforms/ios/qiosglobal.h b/src/plugins/platforms/ios/qiosglobal.h index 022fa88c24..9428487a00 100644 --- a/src/plugins/platforms/ios/qiosglobal.h +++ b/src/plugins/platforms/ios/qiosglobal.h @@ -14,6 +14,7 @@ QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(lcQpaApplication); Q_DECLARE_LOGGING_CATEGORY(lcQpaInputMethods); Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow); +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindowScene); #if !defined(QT_NO_DEBUG) #define qImDebug \ @@ -26,6 +27,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow); class QPlatformScreen; bool isQtApplication(); +bool isRunningOnVisionOS(); #ifndef Q_OS_TVOS Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientation); @@ -34,10 +36,15 @@ UIDeviceOrientation fromQtScreenOrientation(Qt::ScreenOrientation qtOrientation) int infoPlistValue(NSString* key, int defaultValue); +class QWindow; +class QScreen; +UIWindow *presentationWindow(QWindow *); +UIView *rootViewForScreen(QScreen *); + QT_END_NAMESPACE @interface UIResponder (QtFirstResponder) -+ (id)currentFirstResponder; ++ (id)qt_currentFirstResponder; @end QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosglobal.mm b/src/plugins/platforms/ios/qiosglobal.mm index 0a6bc5bb62..1722e09aaa 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -5,6 +5,8 @@ #include "qiosapplicationdelegate.h" #include "qiosviewcontroller.h" #include "qiosscreen.h" +#include "quiwindow.h" +#include "qioseventdispatcher.h" #include <QtCore/private/qcore_mac_p.h> @@ -13,20 +15,26 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcQpaApplication, "qt.qpa.application"); Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window"); +Q_LOGGING_CATEGORY(lcQpaWindowScene, "qt.qpa.window.scene"); bool isQtApplication() { - if (qt_apple_isApplicationExtension()) - return false; - // Returns \c true if the plugin is in full control of the whole application. This means // that we control the application delegate and the top view controller, and can take // actions that impacts all parts of the application. The opposite means that we are // embedded inside a native iOS application, and should be more focused on playing along // with native UIControls, and less inclined to change structures that lies outside the // scope of our QWindows/UIViews. - static bool isQt = ([qt_apple_sharedApplication().delegate isKindOfClass:[QIOSApplicationDelegate class]]); - return isQt; + return QIOSEventDispatcher::isQtApplication(); +} + +bool isRunningOnVisionOS() +{ + static bool result = []{ + // This class is documented to only be available on visionOS + return NSClassFromString(@"UIWindowSceneGeometryPreferencesVision"); + }(); + return result; } #ifndef Q_OS_TVOS @@ -85,6 +93,53 @@ int infoPlistValue(NSString* key, int defaultValue) return value ? [value intValue] : defaultValue; } +UIWindow *presentationWindow(QWindow *window) +{ + UIWindow *uiWindow = window ? reinterpret_cast<UIView *>(window->winId()).window : nullptr; + if (!uiWindow) { + auto *scenes = [qt_apple_sharedApplication().connectedScenes allObjects]; + if (scenes.count > 0) { + auto *windowScene = static_cast<UIWindowScene*>(scenes[0]); + uiWindow = windowScene.keyWindow; + if (!uiWindow && windowScene.windows.count) + uiWindow = windowScene.windows[0]; + } + } + return uiWindow; +} + +UIView *rootViewForScreen(QScreen *screen) +{ + const auto *iosScreen = static_cast<QIOSScreen *>(screen->handle()); + for (UIScene *scene in [qt_apple_sharedApplication().connectedScenes allObjects]) { + if (![scene isKindOfClass:UIWindowScene.class]) + continue; + + auto *windowScene = static_cast<UIWindowScene*>(scene); + +#if !defined(Q_OS_VISIONOS) + if (windowScene.screen != iosScreen->uiScreen()) + continue; +#else + Q_UNUSED(iosScreen); +#endif + + UIWindow *uiWindow = qt_objc_cast<QUIWindow*>(windowScene.keyWindow); + if (!uiWindow) { + for (UIWindow *win in windowScene.windows) { + if (qt_objc_cast<QUIWindow*>(win)) { + uiWindow = win; + break; + } + } + } + + return uiWindow.rootViewController.view; + } + + return nullptr; +} + QT_END_NAMESPACE // ------------------------------------------------------------------------- @@ -119,7 +174,7 @@ QT_END_NAMESPACE @implementation UIResponder (QtFirstResponder) -+ (id)currentFirstResponder ++ (id)qt_currentFirstResponder { if (qt_apple_isApplicationExtension()) { qWarning() << "can't get first responder in application extensions!"; diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index e30ccc2c84..5716ad041e 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.mm +++ b/src/plugins/platforms/ios/qiosinputcontext.mm @@ -18,6 +18,8 @@ #include <QGuiApplication> #include <QtGui/private/qwindow_p.h> +#include <QtCore/qpointer.h> + // ------------------------------------------------------------------------- static QUIView *focusView() @@ -119,12 +121,12 @@ static QUIView *focusView() { [self keyboardWillOrDidChange:notification]; - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; if (![firstResponder isKindOfClass:[QIOSTextInputResponder class]]) return; // Enable hide-keyboard gesture - self.enabled = YES; + self.enabled = m_context->isInputPanelVisible(); m_context->scrollToCursor(); } @@ -174,7 +176,11 @@ static QUIView *focusView() { [super touchesBegan:touches withEvent:event]; - Q_ASSERT(m_context->isInputPanelVisible()); + if (!m_context->isInputPanelVisible()) { + qImDebug("keyboard was hidden by sliding it down, disabling hide-keyboard gesture"); + self.enabled = NO; + return; + } if ([touches count] != 1) self.state = UIGestureRecognizerStateFailed; @@ -228,7 +234,7 @@ static QUIView *focusView() if (self.state == UIGestureRecognizerStateBegan) { qImDebug("hide keyboard gesture was triggered"); - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; Q_ASSERT([firstResponder isKindOfClass:[QIOSTextInputResponder class]]); [firstResponder resignFirstResponder]; } @@ -297,11 +303,7 @@ QIOSInputContext::QIOSInputContext() , m_keyboardHideGesture([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this]) , m_textResponder(0) { - if (isQtApplication()) { - QIOSScreen *iosScreen = static_cast<QIOSScreen*>(QGuiApplication::primaryScreen()->handle()); - [iosScreen->uiWindow() addGestureRecognizer:m_keyboardHideGesture]; - } - + Q_ASSERT(!qGuiApp->focusWindow()); connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged); } @@ -346,7 +348,7 @@ void QIOSInputContext::clearCurrentFocusObject() void QIOSInputContext::updateKeyboardState(NSNotification *notification) { -#ifdef Q_OS_TVOS +#if defined(Q_OS_TVOS) || defined(Q_OS_VISIONOS) Q_UNUSED(notification); #else static CGRect currentKeyboardRect = CGRectZero; @@ -366,7 +368,7 @@ void QIOSInputContext::updateKeyboardState(NSNotification *notification) // The isInputPanelVisible() property is based on whether or not the virtual keyboard // is visible on screen, and does not follow the logic of the iOS WillShow and WillHide // notifications which are not emitted for undocked keyboards, and are buggy when dealing - // with input-accesosory-views. The reason for using frameEnd here (the future state), + // with input-accessory-views. The reason for using frameEnd here (the future state), // instead of the current state reflected in frameBegin, is that QInputMethod::isVisible() // is documented to reflect the future state in the case of animated transitions. m_keyboardState.keyboardVisible = CGRectIntersectsRect(frameEnd, [UIScreen mainScreen].bounds); @@ -436,6 +438,7 @@ UIView *QIOSInputContext::scrollableRootView() void QIOSInputContext::scrollToCursor() { +#if !defined(Q_OS_VISIONOS) if (!isQtApplication()) return; @@ -492,6 +495,7 @@ void QIOSInputContext::scrollToCursor() } else { scroll(0); } +#endif } void QIOSInputContext::scroll(int y) @@ -603,12 +607,15 @@ void QIOSInputContext::setFocusObject(QObject *focusObject) void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) { - Q_UNUSED(focusWindow); - qImDebug() << "new focus window =" << focusWindow; reset(); + if (isQtApplication()) { + [m_keyboardHideGesture.view removeGestureRecognizer:m_keyboardHideGesture]; + [focusView().window addGestureRecognizer:m_keyboardHideGesture]; + } + // The keyboard rectangle depend on the focus window, so // we need to re-evaluate the keyboard state. updateKeyboardState(); @@ -712,11 +719,20 @@ void QIOSInputContext::reset() // Instead, we choose to recreate the text responder as a brute-force solution // until we have better knowledge of what is going on (or implement the new // UITextInteraction protocol). + const auto oldResponder = m_textResponder; [m_textResponder reset]; [m_textResponder autorelease]; m_textResponder = nullptr; update(Qt::ImQueryAll); + + // If update() didn't end up creating a new text responder, oldResponder will still be + // the first responder. In that case we need to resign it, so that the input panel hides. + // (the input panel will apparently not hide if the first responder is only released). + if ([oldResponder isFirstResponder]) { + qImDebug("IM not enabled, resigning autoreleased text responder as first responder"); + [oldResponder resignFirstResponder]; + } } /*! diff --git a/src/plugins/platforms/ios/qiosintegration.h b/src/plugins/platforms/ios/qiosintegration.h index 6830a5b455..53f64c1748 100644 --- a/src/plugins/platforms/ios/qiosintegration.h +++ b/src/plugins/platforms/ios/qiosintegration.h @@ -11,15 +11,29 @@ #include <QtCore/private/qfactoryloader_p.h> #include "qiosapplicationstate.h" -#ifndef Q_OS_TVOS + +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) #include "qiostextinputoverlay.h" #endif +#if defined(Q_OS_VISIONOS) +#include <swift/bridging> +#endif + QT_BEGIN_NAMESPACE +using namespace QNativeInterface; + class QIOSServices; -class QIOSIntegration : public QPlatformNativeInterface, public QPlatformIntegration +class +#if defined(Q_OS_VISIONOS) + SWIFT_IMMORTAL_REFERENCE +#endif +QIOSIntegration : public QPlatformNativeInterface, public QPlatformIntegration +#if defined(Q_OS_VISIONOS) + , public QVisionOSApplication +#endif { Q_OBJECT public: @@ -31,15 +45,21 @@ public: bool hasCapability(Capability cap) const override; QPlatformWindow *createPlatformWindow(QWindow *window) const override; + QPlatformWindow *createForeignWindow(QWindow *window, WId nativeHandle) const override; QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override; +#if QT_CONFIG(opengl) QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const override; +#endif + QPlatformOffscreenSurface *createPlatformOffscreenSurface(QOffscreenSurface *surface) const override; QPlatformFontDatabase *fontDatabase() const override; -#ifndef QT_NO_CLIPBOARD + +#if QT_CONFIG(clipboard) QPlatformClipboard *clipboard() const override; #endif + QPlatformInputContext *inputContext() const override; QPlatformServices *services() const override; @@ -58,6 +78,8 @@ public: void beep() const override; + void setApplicationBadge(qint64 number) override; + static QIOSIntegration *instance(); // -- QPlatformNativeInterface -- @@ -68,9 +90,20 @@ public: QIOSApplicationState applicationState; +#if defined(Q_OS_VISIONOS) + void openImmersiveSpace() override; + void dismissImmersiveSpace() override; + + using CompositorLayer = QVisionOSApplication::ImmersiveSpaceCompositorLayer; + void setImmersiveSpaceCompositorLayer(CompositorLayer *layer) override; + + void configureCompositorLayer(cp_layer_renderer_capabilities_t, cp_layer_renderer_configuration_t); + void renderCompositorLayer(cp_layer_renderer_t); +#endif + private: QPlatformFontDatabase *m_fontDatabase; -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) QPlatformClipboard *m_clipboard; #endif QPlatformInputContext *m_inputContext; @@ -78,9 +111,13 @@ private: QIOSServices *m_platformServices; mutable QPlatformAccessibility *m_accessibility; QFactoryLoader *m_optionalPlugins; -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QIOSTextInputOverlay m_textInputOverlay; #endif + +#if defined(Q_OS_VISIONOS) + CompositorLayer *m_immersiveSpaceCompositorLayer = nullptr; +#endif }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index 7d04a84d86..2c32957c03 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -1,14 +1,15 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qiosintegration.h" #include "qioseventdispatcher.h" #include "qiosglobal.h" #include "qioswindow.h" #include "qiosscreen.h" #include "qiosplatformaccessibility.h" -#include "qioscontext.h" -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) #include "qiosclipboard.h" #endif #include "qiosinputcontext.h" @@ -16,6 +17,10 @@ #include "qiosservices.h" #include "qiosoptionalplugininterface.h" +#if defined(Q_OS_VISIONOS) +#include "qiosswiftintegration.h" +#endif + #include <QtGui/qpointingdevice.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qrhibackingstore_p.h> @@ -24,10 +29,15 @@ #include <qpa/qplatformoffscreensurface.h> #include <QtGui/private/qcoretextfontdatabase_p.h> -#include <QtGui/private/qmacmime_p.h> +#include <QtGui/private/qmacmimeregistry_p.h> +#include <QtGui/qutimimeconverter.h> #include <QDir> #include <QOperatingSystemVersion> +#if QT_CONFIG(opengl) +#include "qioscontext.h" +#endif + #import <AudioToolbox/AudioServices.h> #include <QtDebug> @@ -45,7 +55,7 @@ QIOSIntegration *QIOSIntegration::instance() QIOSIntegration::QIOSIntegration() : m_fontDatabase(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>) -#if !defined(Q_OS_TVOS) && !defined(QT_NO_CLIPBOARD) +#if QT_CONFIG(clipboard) , m_clipboard(new QIOSClipboard) #endif , m_inputContext(0) @@ -66,6 +76,10 @@ QIOSIntegration::QIOSIntegration() void QIOSIntegration::initialize() { +#if defined(Q_OS_VISIONOS) + // Qt requires a screen, so let's give it a dummy one + QWindowSystemInterface::handleScreenAdded(new QIOSScreen); +#else UIScreen *mainScreen = [UIScreen mainScreen]; NSMutableArray<UIScreen *> *screens = [[[UIScreen screens] mutableCopy] autorelease]; if (![screens containsObject:mainScreen]) { @@ -75,6 +89,7 @@ void QIOSIntegration::initialize() for (UIScreen *screen in screens) QWindowSystemInterface::handleScreenAdded(new QIOSScreen(screen)); +#endif // Depends on a primary screen being present m_inputContext = new QIOSInputContext; @@ -82,14 +97,16 @@ void QIOSIntegration::initialize() m_touchDevice = new QPointingDevice; m_touchDevice->setType(QInputDevice::DeviceType::TouchScreen); QPointingDevice::Capabilities touchCapabilities = QPointingDevice::Capability::Position | QPointingDevice::Capability::NormalizedPosition; +#if !defined(Q_OS_VISIONOS) if (mainScreen.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) touchCapabilities |= QPointingDevice::Capability::Pressure; +#endif m_touchDevice->setCapabilities(touchCapabilities); QWindowSystemInterface::registerInputDevice(m_touchDevice); #if QT_CONFIG(tabletevent) QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); #endif - QMacInternalPasteboardMime::initializeMimeTypes(); + QMacMimeRegistry::initializeMimeTypes(); qsizetype size = QList<QPluginParsedMetaData>(m_optionalPlugins->metaData()).size(); for (qsizetype i = 0; i < size; ++i) @@ -101,11 +118,12 @@ QIOSIntegration::~QIOSIntegration() delete m_fontDatabase; m_fontDatabase = 0; -#if !defined(Q_OS_TVOS) && !defined(QT_NO_CLIPBOARD) +#if QT_CONFIG(clipboard) delete m_clipboard; m_clipboard = 0; #endif - QMacInternalPasteboardMime::destroyMimeTypes(); + + QMacMimeRegistry::destroyMimeTypes(); delete m_inputContext; m_inputContext = 0; @@ -126,11 +144,15 @@ QIOSIntegration::~QIOSIntegration() bool QIOSIntegration::hasCapability(Capability cap) const { switch (cap) { +#if QT_CONFIG(opengl) case BufferQueueingOpenGL: return true; case OpenGL: case ThreadedOpenGL: return true; + case RasterGLSurface: + return true; +#endif case ThreadedPixmaps: return true; case MultipleWindows: @@ -139,7 +161,7 @@ bool QIOSIntegration::hasCapability(Capability cap) const return false; case ApplicationState: return true; - case RasterGLSurface: + case ForeignWindows: return true; default: return QPlatformIntegration::hasCapability(cap); @@ -151,16 +173,23 @@ QPlatformWindow *QIOSIntegration::createPlatformWindow(QWindow *window) const return new QIOSWindow(window); } +QPlatformWindow *QIOSIntegration::createForeignWindow(QWindow *window, WId nativeHandle) const +{ + return new QIOSWindow(window, nativeHandle); +} + QPlatformBackingStore *QIOSIntegration::createPlatformBackingStore(QWindow *window) const { return new QRhiBackingStore(window); } +#if QT_CONFIG(opengl) // Used when the QWindow's surface type is set by the client to QSurface::OpenGLSurface QPlatformOpenGLContext *QIOSIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { return new QIOSContext(context); } +#endif class QIOSOffscreenSurface : public QPlatformOffscreenSurface { @@ -190,14 +219,10 @@ QPlatformFontDatabase * QIOSIntegration::fontDatabase() const return m_fontDatabase; } -#ifndef QT_NO_CLIPBOARD +#if QT_CONFIG(clipboard) QPlatformClipboard *QIOSIntegration::clipboard() const { -#ifndef Q_OS_TVOS return m_clipboard; -#else - return QPlatformIntegration::clipboard(); -#endif } #endif @@ -268,6 +293,44 @@ void QIOSIntegration::beep() const #endif } +void QIOSIntegration::setApplicationBadge(qint64 number) +{ + UIApplication.sharedApplication.applicationIconBadgeNumber = number; +} + +// --------------------------------------------------------- + +#if defined(Q_OS_VISIONOS) +void QIOSIntegration::openImmersiveSpace() +{ + [ImmersiveSpaceManager openImmersiveSpace]; +} + +void QIOSIntegration::dismissImmersiveSpace() +{ + [ImmersiveSpaceManager dismissImmersiveSpace]; +} + +void QIOSIntegration::setImmersiveSpaceCompositorLayer(CompositorLayer *layer) +{ + m_immersiveSpaceCompositorLayer = layer; +} + +void QIOSIntegration::configureCompositorLayer(cp_layer_renderer_capabilities_t capabilities, + cp_layer_renderer_configuration_t configuration) +{ + if (m_immersiveSpaceCompositorLayer) + m_immersiveSpaceCompositorLayer->configure(capabilities, configuration); +} + +void QIOSIntegration::renderCompositorLayer(cp_layer_renderer_t renderer) +{ + if (m_immersiveSpaceCompositorLayer) + m_immersiveSpaceCompositorLayer->render(renderer); +} + +#endif + // --------------------------------------------------------- void *QIOSIntegration::nativeResourceForWindow(const QByteArray &resource, QWindow *window) @@ -287,6 +350,6 @@ void *QIOSIntegration::nativeResourceForWindow(const QByteArray &resource, QWind // --------------------------------------------------------- -#include "moc_qiosintegration.cpp" - QT_END_NAMESPACE + +#include "moc_qiosintegration.cpp" diff --git a/src/plugins/platforms/ios/qiosmenu.h b/src/plugins/platforms/ios/qiosmenu.h index 1822c08b1a..b0c8e7e10c 100644 --- a/src/plugins/platforms/ios/qiosmenu.h +++ b/src/plugins/platforms/ios/qiosmenu.h @@ -11,6 +11,8 @@ #import "quiview.h" +#include <QtCore/qpointer.h> + class QIOSMenu; @class QUIMenuController; @class QUIPickerView; diff --git a/src/plugins/platforms/ios/qiosmenu.mm b/src/plugins/platforms/ios/qiosmenu.mm index e547cb0641..227ad2c7f5 100644 --- a/src/plugins/platforms/ios/qiosmenu.mm +++ b/src/plugins/platforms/ios/qiosmenu.mm @@ -491,7 +491,7 @@ QIOSMenuItemList QIOSMenu::filterFirstResponderActions(const QIOSMenuItemList &m // In case of QIOSTextResponder, edit actions will be converted to key events that ends up // triggering the shortcuts of the filtered menu items. QIOSMenuItemList filteredMenuItems; - UIResponder *responder = [UIResponder currentFirstResponder]; + UIResponder *responder = [UIResponder qt_currentFirstResponder]; for (int i = 0; i < menuItems.count(); ++i) { QIOSMenuItem *menuItem = menuItems.at(i); @@ -502,8 +502,8 @@ QIOSMenuItemList QIOSMenu::filterFirstResponderActions(const QIOSMenuItemList &m || (shortcut == QKeySequence::Paste && [responder canPerformAction:@selector(paste:) withSender:nil]) || (shortcut == QKeySequence::Delete && [responder canPerformAction:@selector(delete:) withSender:nil]) || (shortcut == QKeySequence::SelectAll && [responder canPerformAction:@selector(selectAll:) withSender:nil]) - || (shortcut == QKeySequence::Undo && [responder canPerformAction:@selector(undo:) withSender:nil]) - || (shortcut == QKeySequence::Redo && [responder canPerformAction:@selector(redo:) withSender:nil]) + || (shortcut == QKeySequence::Undo && [responder canPerformAction:@selector(undo) withSender:nil]) + || (shortcut == QKeySequence::Redo && [responder canPerformAction:@selector(redo) withSender:nil]) || (shortcut == QKeySequence::Bold && [responder canPerformAction:@selector(toggleBoldface:) withSender:nil]) || (shortcut == QKeySequence::Italic && [responder canPerformAction:@selector(toggleItalics:) withSender:nil]) || (shortcut == QKeySequence::Underline && [responder canPerformAction:@selector(toggleUnderline:) withSender:nil])) { diff --git a/src/plugins/platforms/ios/qiosmessagedialog.mm b/src/plugins/platforms/ios/qiosmessagedialog.mm index fd14e699e0..7fbd5d8729 100644 --- a/src/plugins/platforms/ios/qiosmessagedialog.mm +++ b/src/plugins/platforms/ios/qiosmessagedialog.mm @@ -11,6 +11,7 @@ #include "qiosglobal.h" #include "quiview.h" +#include "qiosscreen.h" #include "qiosmessagedialog.h" using namespace Qt::StringLiterals; @@ -29,7 +30,7 @@ inline QString QIOSMessageDialog::messageTextPlain() { // Concatenate text fragments, and remove HTML tags const QSharedPointer<QMessageDialogOptions> &opt = options(); - const QString &lineShift = QStringLiteral("\n\n"); + constexpr auto lineShift = "\n\n"_L1; const QString &informativeText = opt->informativeText(); const QString &detailedText = opt->detailedText(); @@ -39,7 +40,7 @@ inline QString QIOSMessageDialog::messageTextPlain() if (!detailedText.isEmpty()) text += lineShift + detailedText; - text.replace("<p>"_L1, QStringLiteral("\n"), Qt::CaseInsensitive); + text.replace("<p>"_L1, "\n"_L1, Qt::CaseInsensitive); text.remove(QRegularExpression(QStringLiteral("<[^>]*>"))); return text; @@ -91,6 +92,9 @@ bool QIOSMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality win || windowModality == Qt::NonModal) // We can only do modal dialogs return false; + if (!options()->checkBoxLabel().isNull()) + return false; // Can't support + m_alertController = [[UIAlertController alertControllerWithTitle:options()->windowTitle().toNSString() message:messageTextPlain().toNSString() @@ -112,7 +116,18 @@ bool QIOSMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality win [m_alertController addAction:createAction(NoButton)]; } - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window : qt_apple_sharedApplication().keyWindow; + UIWindow *window = presentationWindow(parent); + if (!window) + return false; + + if (window.hidden) { + // With a window hidden, an attempt to present view controller + // below fails with a warning, that a view "is not a part of + // any view hierarchy". The UIWindow is initially hidden, + // as unhiding it is what hides the splash screen. + window.hidden = NO; + } + [window.rootViewController presentViewController:m_alertController animated:YES completion:nil]; return true; } diff --git a/src/plugins/platforms/ios/qiosplatformaccessibility.mm b/src/plugins/platforms/ios/qiosplatformaccessibility.mm index d54b7db57a..eb18ee637e 100644 --- a/src/plugins/platforms/ios/qiosplatformaccessibility.mm +++ b/src/plugins/platforms/ios/qiosplatformaccessibility.mm @@ -1,12 +1,15 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qiosplatformaccessibility.h" #if QT_CONFIG(accessibility) #include <QtGui/QtGui> #include "qioswindow.h" +#include "quiaccessibilityelement.h" QIOSPlatformAccessibility::QIOSPlatformAccessibility() {} @@ -25,8 +28,6 @@ void invalidateCache(QAccessibleInterface *iface) // This will invalidate everything regardless of what window the // interface belonged to. We might want to revisit this strategy later. // (Therefore this function still takes the interface as argument) - // It is also responsible for the bug that focus gets temporary lost - // when items get added or removed from the screen foreach (QWindow *win, QGuiApplication::topLevelWindows()) { if (win && win->handle()) { QT_PREPEND_NAMESPACE(QIOSWindow) *window = static_cast<QT_PREPEND_NAMESPACE(QIOSWindow) *>(win->handle()); @@ -38,14 +39,35 @@ void invalidateCache(QAccessibleInterface *iface) void QIOSPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) { - if (!isActive() || !event->accessibleInterface()) + auto *accessibleInterface = event->accessibleInterface(); + if (!isActive() || !accessibleInterface) return; switch (event->type()) { + case QAccessible::Focus: { + auto *element = [QMacAccessibilityElement elementWithId:event->uniqueId()]; + Q_ASSERT(element); + // There's no NSAccessibilityFocusedUIElementChangedNotification, like we have on + // macOS. Instead, the documentation for UIAccessibilityLayoutChangedNotification + // specifies that the optional argument to UIAccessibilityPostNotification is the + // accessibility element for VoiceOver to move to after processing the notification. + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, element); + break; + } case QAccessible::ObjectCreated: case QAccessible::ObjectShow: case QAccessible::ObjectHide: case QAccessible::ObjectDestroyed: - invalidateCache(event->accessibleInterface()); + invalidateCache(accessibleInterface); + switch (accessibleInterface->role()) { + case QAccessible::Window: + case QAccessible::Dialog: + // Bigger changes to the UI require a full reset of VoiceOver + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + break; + default: + // While smaller changes can be handled by re-reading the layout + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } break; default: break; diff --git a/src/plugins/platforms/ios/qiosscreen.h b/src/plugins/platforms/ios/qiosscreen.h index 21afb90c55..dd69428390 100644 --- a/src/plugins/platforms/ios/qiosscreen.h +++ b/src/plugins/platforms/ios/qiosscreen.h @@ -10,10 +10,6 @@ @class QIOSOrientationListener; -@interface QUIWindow : UIWindow -@property (nonatomic, readonly) BOOL sendingEvent; -@end - QT_BEGIN_NAMESPACE class QIOSScreen : public QObject, public QPlatformScreen @@ -21,7 +17,11 @@ class QIOSScreen : public QObject, public QPlatformScreen Q_OBJECT public: +#if !defined(Q_OS_VISIONOS) QIOSScreen(UIScreen *screen); +#else + QIOSScreen(); +#endif ~QIOSScreen(); QString name() const override; @@ -40,8 +40,9 @@ public: QPixmap grabWindow(WId window, int x, int y, int width, int height) const override; +#if !defined(Q_OS_VISIONOS) UIScreen *uiScreen() const; - UIWindow *uiWindow() const; +#endif void setUpdatesPaused(bool); @@ -50,15 +51,17 @@ public: private: void deliverUpdateRequests() const; - UIScreen *m_uiScreen; - UIWindow *m_uiWindow; +#if !defined(Q_OS_VISIONOS) + UIScreen *m_uiScreen = nullptr; +#endif QRect m_geometry; QRect m_availableGeometry; int m_depth; +#if !defined(Q_OS_VISIONOS) uint m_physicalDpi; +#endif QSizeF m_physicalSize; - QIOSOrientationListener *m_orientationListener; - CADisplayLink *m_displayLink; + CADisplayLink *m_displayLink = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosscreen.mm b/src/plugins/platforms/ios/qiosscreen.mm index 7ae28e1783..7559979f33 100644 --- a/src/plugins/platforms/ios/qiosscreen.mm +++ b/src/plugins/platforms/ios/qiosscreen.mm @@ -1,6 +1,8 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qiosglobal.h" #include "qiosintegration.h" #include "qiosscreen.h" @@ -10,11 +12,13 @@ #include "qiosviewcontroller.h" #include "quiview.h" #include "qiostheme.h" +#include "quiwindow.h" #include <QtCore/private/qcore_mac_p.h> #include <QtGui/qpointingdevice.h> #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qguiapplication_p.h> #include <private/qcoregraphics_p.h> #include <qpa/qwindowsysteminterface.h> @@ -43,6 +47,7 @@ typedef void (^DisplayLinkBlock)(CADisplayLink *displayLink); // ------------------------------------------------------------------------- +#if !defined(Q_OS_VISIONOS) static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) { foreach (QScreen *screen, QGuiApplication::screens()) { @@ -72,14 +77,17 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) + (void)screenConnected:(NSNotification*)notification { - Q_ASSERT_X(QIOSIntegration::instance(), Q_FUNC_INFO, - "Screen connected before QIOSIntegration creation"); + if (!QIOSIntegration::instance()) + return; // Will be added when QIOSIntegration is created QWindowSystemInterface::handleScreenAdded(new QIOSScreen([notification object])); } + (void)screenDisconnected:(NSNotification*)notification { + if (!QIOSIntegration::instance()) + return; + QIOSScreen *screen = qtPlatformScreenFor([notification object]); Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen disconnected that we didn't know about"); @@ -88,6 +96,9 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) + (void)screenModeChanged:(NSNotification*)notification { + if (!QIOSIntegration::instance()) + return; + QIOSScreen *screen = qtPlatformScreenFor([notification object]); Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen changed that we didn't know about"); @@ -96,98 +107,7 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) @end -// ------------------------------------------------------------------------- - -@interface QIOSOrientationListener : NSObject -@end - -@implementation QIOSOrientationListener { - QIOSScreen *m_screen; -} - -- (instancetype)initWithQIOSScreen:(QIOSScreen *)screen -{ - self = [super init]; - if (self) { - m_screen = screen; -#ifndef Q_OS_TVOS - [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(orientationChanged:) - name:@"UIDeviceOrientationDidChangeNotification" object:nil]; -#endif - } - return self; -} - -- (void)dealloc -{ -#ifndef Q_OS_TVOS - [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:@"UIDeviceOrientationDidChangeNotification" object:nil]; -#endif - [super dealloc]; -} - -- (void)orientationChanged:(NSNotification *)notification -{ - Q_UNUSED(notification); - m_screen->updateProperties(); -} - -@end - -@interface UIScreen (Compatibility) -@property (nonatomic, readonly) CGRect qt_applicationFrame; -@end - -@implementation UIScreen (Compatibility) -- (CGRect)qt_applicationFrame -{ -#ifdef Q_OS_IOS - return self.applicationFrame; -#else - return self.bounds; -#endif -} -@end - -// ------------------------------------------------------------------------- - -@implementation QUIWindow - -- (instancetype)initWithFrame:(CGRect)frame -{ - if ((self = [super initWithFrame:frame])) - self->_sendingEvent = NO; - - return self; -} - -- (void)sendEvent:(UIEvent *)event -{ - QScopedValueRollback<BOOL> sendingEvent(self->_sendingEvent, YES); - [super sendEvent:event]; -} - -- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection -{ - [super traitCollectionDidChange:previousTraitCollection]; - - if (@available(iOS 12, *)) { - if (self.screen == UIScreen.mainScreen) { - if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) { - QIOSTheme::initializeSystemPalette(); - QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); - } - } - } -} - -@end +#endif // !defined(Q_OS_VISIONOS) // ------------------------------------------------------------------------- @@ -195,6 +115,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; +#if !defined(Q_OS_VISIONOS) /*! Returns the model identifier of the device. */ @@ -214,12 +135,14 @@ static QString deviceModelIdentifier() return QString::fromLatin1(QByteArrayView(value, qsizetype(size))); #endif } +#endif // !defined(Q_OS_VISIONOS) +#if defined(Q_OS_VISIONOS) +QIOSScreen::QIOSScreen() +{ +#else QIOSScreen::QIOSScreen(UIScreen *screen) - : QPlatformScreen() - , m_uiScreen(screen) - , m_uiWindow(0) - , m_orientationListener(0) + : m_uiScreen(screen) { QString deviceIdentifier = deviceModelIdentifier(); @@ -253,44 +176,30 @@ QIOSScreen::QIOSScreen(UIScreen *screen) m_physicalDpi = 96; } - if (!qt_apple_isApplicationExtension()) { - for (UIWindow *existingWindow in qt_apple_sharedApplication().windows) { - if (existingWindow.screen == m_uiScreen) { - m_uiWindow = [m_uiWindow retain]; - break; - } - } - - if (!m_uiWindow) { - // Create a window and associated view-controller that we can use - m_uiWindow = [[QUIWindow alloc] initWithFrame:[m_uiScreen bounds]]; - m_uiWindow.rootViewController = [[[QIOSViewController alloc] initWithQIOSScreen:this] autorelease]; - } - } - - m_orientationListener = [[QIOSOrientationListener alloc] initWithQIOSScreen:this]; - - updateProperties(); - m_displayLink = [m_uiScreen displayLinkWithBlock:^(CADisplayLink *) { deliverUpdateRequests(); }]; m_displayLink.paused = YES; // Enabled when clients call QWindow::requestUpdate() [m_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; + +#endif // !defined(Q_OS_VISIONOS)) + + updateProperties(); } QIOSScreen::~QIOSScreen() { [m_displayLink invalidate]; - - [m_orientationListener release]; - [m_uiWindow release]; } QString QIOSScreen::name() const { +#if defined(Q_OS_VISIONOS) + return {}; +#else if (m_uiScreen == [UIScreen mainScreen]) return QString::fromNSString([UIDevice currentDevice].model) + " built-in display"_L1; else return "External display"_L1; +#endif } void QIOSScreen::updateProperties() @@ -298,42 +207,13 @@ void QIOSScreen::updateProperties() QRect previousGeometry = m_geometry; QRect previousAvailableGeometry = m_availableGeometry; +#if defined(Q_OS_VISIONOS) + // Based on what iPad app reports + m_geometry = QRect(0, 0, 1194, 834); + m_depth = 24; +#else m_geometry = QRectF::fromCGRect(m_uiScreen.bounds).toRect(); - // The application frame doesn't take safe area insets into account, and - // the safe area insets are not available before the UIWindow is shown, - // and do not take split-view constraints into account, so we have to - // combine the two to get the correct available geometry. - QRect applicationFrame = QRectF::fromCGRect(m_uiScreen.qt_applicationFrame).toRect(); - UIEdgeInsets safeAreaInsets = m_uiWindow.qt_safeAreaInsets; - m_availableGeometry = m_geometry.adjusted(safeAreaInsets.left, safeAreaInsets.top, - -safeAreaInsets.right, -safeAreaInsets.bottom).intersected(applicationFrame); - -#ifndef Q_OS_TVOS - if (m_uiScreen == [UIScreen mainScreen]) { - QIOSViewController *qtViewController = [m_uiWindow.rootViewController isKindOfClass:[QIOSViewController class]] ? - static_cast<QIOSViewController *>(m_uiWindow.rootViewController) : nil; - - if (qtViewController.lockedOrientation) { - Q_ASSERT(!qt_apple_isApplicationExtension()); - - // Setting the statusbar orientation (content orientation) on will affect the screen geometry, - // which is not what we want. We want to reflect the screen geometry based on the locked orientation, - // and adjust the available geometry based on the repositioned status bar for the current status - // bar orientation. - - Qt::ScreenOrientation statusBarOrientation = toQtScreenOrientation( - UIDeviceOrientation(qt_apple_sharedApplication().statusBarOrientation)); - - Qt::ScreenOrientation lockedOrientation = toQtScreenOrientation(UIDeviceOrientation(qtViewController.lockedOrientation)); - QTransform transform = transformBetween(lockedOrientation, statusBarOrientation, m_geometry).inverted(); - - m_geometry = transform.mapRect(m_geometry); - m_availableGeometry = transform.mapRect(m_availableGeometry); - } - } -#endif - if (m_geometry != previousGeometry) { // We can't use the primaryOrientation of screen(), as we haven't reported the new geometry yet Qt::ScreenOrientation primaryOrientation = m_geometry.width() >= m_geometry.height() ? @@ -349,6 +229,14 @@ void QIOSScreen::updateProperties() m_physicalSize = physicalGeometry.size() / m_physicalDpi * millimetersPerInch; } +#endif // defined(Q_OS_VISIONOS) + + // UIScreen does not provide a consistent accessor for the safe area margins + // of the screen, and on visionOS we won't even have a UIScreen, so we report + // the available geometry of the screen to be the same as the full geometry. + // Safe area margins and maximized state is handled in QIOSWindow::setWindowState. + m_availableGeometry = m_geometry; + // At construction time, we don't yet have an associated QScreen, but we still want // to compute the properties above so they are ready for when the QScreen attaches. // Also, at destruction time the QScreen has already been torn down, so notifying @@ -433,16 +321,28 @@ QDpi QIOSScreen::logicalBaseDpi() const qreal QIOSScreen::devicePixelRatio() const { +#if defined(Q_OS_VISIONOS) + return 2.0; // Based on what iPad app reports +#else return [m_uiScreen scale]; +#endif } qreal QIOSScreen::refreshRate() const { +#if defined(Q_OS_VISIONOS) + return 120.0; // Based on what iPad app reports +#else return m_uiScreen.maximumFramesPerSecond; +#endif } Qt::ScreenOrientation QIOSScreen::nativeOrientation() const { +#if defined(Q_OS_VISIONOS) + // Based on iPad app reporting native bounds 1668x2388 + return Qt::PortraitOrientation; +#else CGRect nativeBounds = #if defined(Q_OS_IOS) m_uiScreen.nativeBounds; @@ -454,38 +354,18 @@ Qt::ScreenOrientation QIOSScreen::nativeOrientation() const // be on the safe side we compare the width and height of the bounds. return nativeBounds.size.width >= nativeBounds.size.height ? Qt::LandscapeOrientation : Qt::PortraitOrientation; +#endif } Qt::ScreenOrientation QIOSScreen::orientation() const { -#ifdef Q_OS_TVOS - return Qt::PrimaryOrientation; -#else - // Auxiliary screens are always the same orientation as their primary orientation - if (m_uiScreen != [UIScreen mainScreen]) - return Qt::PrimaryOrientation; - - UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; - - // At startup, iOS will report an unknown orientation for the device, even - // if we've asked it to begin generating device orientation notifications. - // In this case we fall back to the status bar orientation, which reflects - // the orientation the application was started up in (which may not match - // the physical orientation of the device, but typically does unless the - // application has been locked to a subset of the available orientations). - if (deviceOrientation == UIDeviceOrientationUnknown && !qt_apple_isApplicationExtension()) - deviceOrientation = UIDeviceOrientation(qt_apple_sharedApplication().statusBarOrientation); - - // If the device reports face up or face down orientations, we can't map - // them to Qt orientations, so we pretend we're in the same orientation - // as before. - if (deviceOrientation == UIDeviceOrientationFaceUp || deviceOrientation == UIDeviceOrientationFaceDown) { - Q_ASSERT(screen()); - return screen()->orientation(); - } - - return toQtScreenOrientation(deviceOrientation); -#endif + // We don't report UIDevice.currentDevice.orientation here, + // as that would report the actual orientation of the device, + // even if the orientation of the UI was locked to a subset + // of the possible orientations via the app's Info.plist or + // via [UIViewController supportedInterfaceOrientations]. + return m_geometry.width() >= m_geometry.height() ? + Qt::LandscapeOrientation : Qt::PortraitOrientation; } QPixmap QIOSScreen::grabWindow(WId window, int x, int y, int width, int height) const @@ -493,26 +373,27 @@ QPixmap QIOSScreen::grabWindow(WId window, int x, int y, int width, int height) if (window && ![reinterpret_cast<id>(window) isKindOfClass:[UIView class]]) return QPixmap(); - UIView *view = window ? reinterpret_cast<UIView *>(window) : m_uiWindow; + UIView *view = window ? reinterpret_cast<UIView *>(window) + : rootViewForScreen(screen()); if (width < 0) width = qMax(view.bounds.size.width - x, CGFloat(0)); if (height < 0) height = qMax(view.bounds.size.height - y, CGFloat(0)); - CGRect captureRect = [m_uiWindow convertRect:CGRectMake(x, y, width, height) fromView:view]; - captureRect = CGRectIntersection(captureRect, m_uiWindow.bounds); + CGRect captureRect = [view.window convertRect:CGRectMake(x, y, width, height) fromView:view]; + captureRect = CGRectIntersection(captureRect, view.window.bounds); UIGraphicsBeginImageContextWithOptions(captureRect.size, NO, 0.0); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(context, -captureRect.origin.x, -captureRect.origin.y); - // Draws the complete view hierarchy of m_uiWindow into the given rect, which - // needs to be the same aspect ratio as the m_uiWindow's size. Since we've + // Draws the complete view hierarchy of view.window into the given rect, which + // needs to be the same aspect ratio as the view.window's size. Since we've // translated the graphics context, and are potentially drawing into a smaller // context than the full window, the resulting image will be a subsection of the // full screen. - [m_uiWindow drawViewHierarchyInRect:m_uiWindow.bounds afterScreenUpdates:NO]; + [view.window drawViewHierarchyInRect:view.window.bounds afterScreenUpdates:NO]; UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); @@ -520,16 +401,13 @@ QPixmap QIOSScreen::grabWindow(WId window, int x, int y, int width, int height) return QPixmap::fromImage(qt_mac_toQImage(screenshot.CGImage)); } +#if !defined(Q_OS_VISIONOS) UIScreen *QIOSScreen::uiScreen() const { return m_uiScreen; } +#endif -UIWindow *QIOSScreen::uiWindow() const -{ - return m_uiWindow; -} +QT_END_NAMESPACE #include "moc_qiosscreen.cpp" - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiostextinputoverlay.mm b/src/plugins/platforms/ios/qiostextinputoverlay.mm index 512ab77bd2..83170c1851 100644 --- a/src/plugins/platforms/ios/qiostextinputoverlay.mm +++ b/src/plugins/platforms/ios/qiostextinputoverlay.mm @@ -434,7 +434,7 @@ static void executeBlockWithoutAnimation(Block block) if (enabled) { _focusView = [reinterpret_cast<UIView *>(qApp->focusWindow()->winId()) retain]; - _desktopView = [qt_apple_sharedApplication().keyWindow.rootViewController.view retain]; + _desktopView = [presentationWindow(nullptr).rootViewController.view retain]; Q_ASSERT(_focusView && _desktopView && _desktopView.superview); [_desktopView addGestureRecognizer:self]; } else { @@ -947,7 +947,17 @@ static void executeBlockWithoutAnimation(Block block) int cursorPosOnRelease = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, touchPos).toInt(); if (cursorPosOnRelease == _cursorPosOnPress) { + // We've recognized a gesture to open the menu, but we don't know + // whether the user tapped a control that was overlaid our input + // area, since we don't do any granular hit-testing in touchesBegan. + // To ensure that the gesture doesn't eat touch events that should + // have reached another UI control we report the gesture as failed + // here, and then manually show the menu at the next runloop pass. _menuShouldBeVisible = true; + self.state = UIGestureRecognizerStateFailed; + dispatch_async(dispatch_get_main_queue(), ^{ + QIOSTextInputOverlay::s_editMenu.visible = _menuShouldBeVisible; + }); } else { // The menu is hidden, and the cursor will change position once // Qt receive the touch release. We therefore fail so that we @@ -996,19 +1006,26 @@ QIOSTextInputOverlay::~QIOSTextInputOverlay() void QIOSTextInputOverlay::updateFocusObject() { + // Destroy old recognizers since they were created with + // dependencies to the old focus object (focus view). if (m_cursorRecognizer) { - // Destroy old recognizers since they were created with - // dependencies to the old focus object (focus view). m_cursorRecognizer.enabled = NO; - m_selectionRecognizer.enabled = NO; - m_openMenuOnTapRecognizer.enabled = NO; [m_cursorRecognizer release]; - [m_selectionRecognizer release]; - [m_openMenuOnTapRecognizer release]; - [s_editMenu release]; m_cursorRecognizer = nullptr; + } + if (m_selectionRecognizer) { + m_selectionRecognizer.enabled = NO; + [m_selectionRecognizer release]; m_selectionRecognizer = nullptr; + } + if (m_openMenuOnTapRecognizer) { + m_openMenuOnTapRecognizer.enabled = NO; + [m_openMenuOnTapRecognizer release]; m_openMenuOnTapRecognizer = nullptr; + } + + if (s_editMenu) { + [s_editMenu release]; s_editMenu = nullptr; } diff --git a/src/plugins/platforms/ios/qiostextresponder.mm b/src/plugins/platforms/ios/qiostextresponder.mm index 8069ddb601..5231a3adde 100644 --- a/src/plugins/platforms/ios/qiostextresponder.mm +++ b/src/plugins/platforms/ios/qiostextresponder.mm @@ -164,7 +164,7 @@ { FirstResponderCandidate firstResponderCandidate(self); - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; if (![super becomeFirstResponder]) { qImDebug() << self << "was not allowed to become first responder"; @@ -178,7 +178,7 @@ - (BOOL)resignFirstResponder { - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; // Don't allow activation events of the window that we're doing text on behalf on // to steal responder. @@ -196,11 +196,11 @@ // a regular responder transfer to another window. In the former case, iOS // will set the new first-responder to our next-responder, and in the latter // case we'll have an active responder candidate. - if (![UIResponder currentFirstResponder] && !FirstResponderCandidate::currentCandidate()) { + if (![UIResponder qt_currentFirstResponder] && !FirstResponderCandidate::currentCandidate()) { // No first responder set anymore, sync this with Qt by clearing the // focus object. m_inputContext->clearCurrentFocusObject(); - } else if ([UIResponder currentFirstResponder] == [self nextResponder]) { + } else if ([UIResponder qt_currentFirstResponder] == [self nextResponder]) { // We have resigned the keyboard, and transferred first responder back to the parent view Q_ASSERT(!FirstResponderCandidate::currentCandidate()); if ([self currentImeState:Qt::ImEnabled].toBool()) { @@ -222,8 +222,12 @@ - (UIResponder*)nextResponder { - return qApp->focusWindow() ? - reinterpret_cast<QUIView *>(qApp->focusWindow()->handle()->winId()) : 0; + // Make sure we have a handle/platform window before getting the winId(). + // In the dtor of QIOSWindow the platform window is set to null before calling + // removeFromSuperview which will end up calling nextResponder. That means it's + // possible that we can get here while the window is being torn down. + return (qApp->focusWindow() && qApp->focusWindow()->handle()) ? + reinterpret_cast<QUIView *>(qApp->focusWindow()->handle()->winId()) : 0; } // ------------------------------------------------------------------------- @@ -397,7 +401,7 @@ if (UIView *accessoryView = static_cast<UIView *>(platformData.value(kImePlatformDataInputAccessoryView).value<void *>())) self.inputAccessoryView = [[[WrapperView alloc] initWithView:accessoryView] autorelease]; -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) if (platformData.value(kImePlatformDataHideShortcutsBar).toBool()) { // According to the docs, leadingBarButtonGroups/trailingBarButtonGroups should be set to nil to hide the shortcuts bar. // However, starting with iOS 10, the API has been surrounded with NS_ASSUME_NONNULL, which contradicts this and causes @@ -898,7 +902,7 @@ QInputMethodEvent e(m_markedText, attrs); [self sendEventToFocusObject:e]; } - QRectF startRect = QPlatformInputContext::cursorRectangle();; + QRectF startRect = QPlatformInputContext::cursorRectangle(); attrs = QList<QInputMethodEvent::Attribute>(); attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.location + r.length, 0, 0); @@ -906,7 +910,7 @@ QInputMethodEvent e(m_markedText, attrs); [self sendEventToFocusObject:e]; } - QRectF endRect = QPlatformInputContext::cursorRectangle();; + QRectF endRect = QPlatformInputContext::cursorRectangle(); if (cursorPos != int(r.location + r.length) || cursorPos != anchorPos) { attrs = QList<QInputMethodEvent::Attribute>(); @@ -944,20 +948,20 @@ [self sendEventToFocusObject:e]; } -- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(UITextRange *)range +- (void)setBaseWritingDirection:(NSWritingDirection)writingDirection forRange:(UITextRange *)range { Q_UNUSED(writingDirection); Q_UNUSED(range); // Writing direction is handled by QLocale } -- (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction +- (NSWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction { Q_UNUSED(position); Q_UNUSED(direction); if (QLocale::system().textDirection() == Qt::RightToLeft) - return UITextWritingDirectionRightToLeft; - return UITextWritingDirectionLeftToRight; + return NSWritingDirectionRightToLeft; + return NSWritingDirectionLeftToRight; } - (UITextRange *)characterRangeByExtendingPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction diff --git a/src/plugins/platforms/ios/qiostheme.h b/src/plugins/platforms/ios/qiostheme.h index ea5169800b..70e2c37ff1 100644 --- a/src/plugins/platforms/ios/qiostheme.h +++ b/src/plugins/platforms/ios/qiostheme.h @@ -4,6 +4,8 @@ #ifndef QIOSTHEME_H #define QIOSTHEME_H +#import <UIKit/UIKit.h> + #include <QtCore/QHash> #include <QtGui/QPalette> #include <qpa/qplatformtheme.h> @@ -21,22 +23,28 @@ public: const QPalette *palette(Palette type = SystemPalette) const override; QVariant themeHint(ThemeHint hint) const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QPlatformMenuItem* createPlatformMenuItem() const override; QPlatformMenu* createPlatformMenu() const override; +#endif bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; static const char *name; static void initializeSystemPalette(); + static void applyTheme(UIWindow *window); private: static QPalette s_systemPalette; + static inline Qt::ColorScheme s_colorSchemeOverride = Qt::ColorScheme::Unknown; QMacNotificationObserver m_contentSizeCategoryObserver; }; diff --git a/src/plugins/platforms/ios/qiostheme.mm b/src/plugins/platforms/ios/qiostheme.mm index a03a0b9515..0b420a875a 100644 --- a/src/plugins/platforms/ios/qiostheme.mm +++ b/src/plugins/platforms/ios/qiostheme.mm @@ -11,16 +11,24 @@ #include <QtGui/private/qcoregraphics_p.h> #include <QtGui/private/qcoretextfontdatabase_p.h> +#include <QtGui/private/qappleiconengine_p.h> #include <QtGui/private/qguiapplication_p.h> #include <qpa/qplatformintegration.h> #include <UIKit/UIFont.h> #include <UIKit/UIInterface.h> -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) #include "qiosmenu.h" +#endif + +#if !defined(Q_OS_TVOS) #include "qiosfiledialog.h" +#include "qioscolordialog.h" +#include "qiosfontdialog.h" #include "qiosmessagedialog.h" +#include "qiosscreen.h" +#include "quiwindow.h" #endif QT_BEGIN_NAMESPACE @@ -34,7 +42,7 @@ QIOSTheme::QIOSTheme() m_contentSizeCategoryObserver = QMacNotificationObserver(nil, UIContentSizeCategoryDidChangeNotification, [] { qCDebug(lcQpaFonts) << "Contents size category changed to" << UIApplication.sharedApplication.preferredContentSizeCategory; - QPlatformFontDatabase::handleAvailableFontsChanged(); + QPlatformFontDatabase::repopulateFontDatabase(); }); } @@ -49,28 +57,26 @@ void QIOSTheme::initializeSystemPalette() Q_DECL_IMPORT QPalette qt_fusionPalette(void); s_systemPalette = qt_fusionPalette(); - if (@available(ios 13.0, *)) { - s_systemPalette.setBrush(QPalette::Window, qt_mac_toQBrush(UIColor.systemGroupedBackgroundColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::WindowText, qt_mac_toQBrush(UIColor.labelColor.CGColor)); + s_systemPalette.setBrush(QPalette::Window, qt_mac_toQBrush(UIColor.systemGroupedBackgroundColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::WindowText, qt_mac_toQBrush(UIColor.labelColor.CGColor)); - s_systemPalette.setBrush(QPalette::Base, qt_mac_toQBrush(UIColor.secondarySystemGroupedBackgroundColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::Text, qt_mac_toQBrush(UIColor.labelColor.CGColor)); + s_systemPalette.setBrush(QPalette::Base, qt_mac_toQBrush(UIColor.secondarySystemGroupedBackgroundColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::Text, qt_mac_toQBrush(UIColor.labelColor.CGColor)); - s_systemPalette.setBrush(QPalette::Button, qt_mac_toQBrush(UIColor.secondarySystemBackgroundColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::ButtonText, qt_mac_toQBrush(UIColor.labelColor.CGColor)); + s_systemPalette.setBrush(QPalette::Button, qt_mac_toQBrush(UIColor.secondarySystemBackgroundColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::ButtonText, qt_mac_toQBrush(UIColor.labelColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::BrightText, qt_mac_toQBrush(UIColor.lightTextColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::PlaceholderText, qt_mac_toQBrush(UIColor.placeholderTextColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::BrightText, qt_mac_toQBrush(UIColor.lightTextColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::PlaceholderText, qt_mac_toQBrush(UIColor.placeholderTextColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::Link, qt_mac_toQBrush(UIColor.linkColor.CGColor)); - s_systemPalette.setBrush(QPalette::Active, QPalette::LinkVisited, qt_mac_toQBrush(UIColor.linkColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::Link, qt_mac_toQBrush(UIColor.linkColor.CGColor)); + s_systemPalette.setBrush(QPalette::Active, QPalette::LinkVisited, qt_mac_toQBrush(UIColor.linkColor.CGColor)); - s_systemPalette.setBrush(QPalette::Highlight, QColor(11, 70, 150, 60)); - s_systemPalette.setBrush(QPalette::HighlightedText, qt_mac_toQBrush(UIColor.labelColor.CGColor)); - } else { - s_systemPalette.setBrush(QPalette::Highlight, QColor(204, 221, 237)); - s_systemPalette.setBrush(QPalette::HighlightedText, Qt::black); - } + s_systemPalette.setBrush(QPalette::Highlight, QColor(11, 70, 150, 60)); + s_systemPalette.setBrush(QPalette::HighlightedText, qt_mac_toQBrush(UIColor.labelColor.CGColor)); + + if (@available(ios 15.0, *)) + s_systemPalette.setBrush(QPalette::Accent, qt_mac_toQBrush(UIColor.tintColor.CGColor)); } const QPalette *QIOSTheme::palette(QPlatformTheme::Palette type) const @@ -80,29 +86,25 @@ const QPalette *QIOSTheme::palette(QPlatformTheme::Palette type) const return 0; } +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QPlatformMenuItem* QIOSTheme::createPlatformMenuItem() const { -#ifdef Q_OS_TVOS - return 0; -#else - return new QIOSMenuItem(); -#endif + return new QIOSMenuItem; } QPlatformMenu* QIOSTheme::createPlatformMenu() const { -#ifdef Q_OS_TVOS - return 0; -#else - return new QIOSMenu(); -#endif + return new QIOSMenu; } +#endif bool QIOSTheme::usePlatformNativeDialog(QPlatformTheme::DialogType type) const { switch (type) { case FileDialog: case MessageDialog: + case ColorDialog: + case FontDialog: return !qt_apple_isApplicationExtension(); default: return false; @@ -119,6 +121,12 @@ QPlatformDialogHelper *QIOSTheme::createPlatformDialogHelper(QPlatformTheme::Dia case MessageDialog: return new QIOSMessageDialog(); break; + case ColorDialog: + return new QIOSColorDialog(); + break; + case FontDialog: + return new QIOSFontDialog(); + break; #endif default: return 0; @@ -137,17 +145,65 @@ QVariant QIOSTheme::themeHint(ThemeHint hint) const } } -QPlatformTheme::Appearance QIOSTheme::appearance() const +Qt::ColorScheme QIOSTheme::colorScheme() const { - if (@available(ios 12, *)) { - if (UIWindow *window = qt_apple_sharedApplication().windows.lastObject) { - return window.rootViewController.traitCollection.userInterfaceStyle - == UIUserInterfaceStyleDark - ? QPlatformTheme::Appearance::Dark - : QPlatformTheme::Appearance::Light; +#if defined(Q_OS_VISIONOS) + // On visionOS the concept of light or dark mode does not + // apply, as the UI is constantly changing based on what + // the lighting conditions are outside the headset, but + // the OS reports itself as always being in dark mode. + return Qt::ColorScheme::Dark; +#else + if (s_colorSchemeOverride != Qt::ColorScheme::Unknown) + return s_colorSchemeOverride; + + // Set the appearance based on the QUIWindow + // Fallback to the UIScreen if no window is created yet + UIUserInterfaceStyle appearance = UIScreen.mainScreen.traitCollection.userInterfaceStyle; + NSArray<UIWindow *> *windows = qt_apple_sharedApplication().windows; + for (UIWindow *window in windows) { + if ([window isKindOfClass:[QUIWindow class]]) { + appearance = static_cast<QUIWindow*>(window).traitCollection.userInterfaceStyle; + break; } } - return QPlatformTheme::Appearance::Unknown; + + return appearance == UIUserInterfaceStyleDark + ? Qt::ColorScheme::Dark + : Qt::ColorScheme::Light; +#endif +} + +void QIOSTheme::requestColorScheme(Qt::ColorScheme scheme) +{ +#if defined(Q_OS_VISIONOS) + Q_UNUSED(scheme); +#else + s_colorSchemeOverride = scheme; + + const NSArray<UIWindow *> *windows = qt_apple_sharedApplication().windows; + for (UIWindow *window in windows) { + // don't apply a theme to windows we don't own + if (qt_objc_cast<QUIWindow*>(window)) + applyTheme(window); + } +#endif +} + +void QIOSTheme::applyTheme(UIWindow *window) +{ + const UIUserInterfaceStyle style = []{ + switch (s_colorSchemeOverride) { + case Qt::ColorScheme::Dark: + return UIUserInterfaceStyleDark; + case Qt::ColorScheme::Light: + return UIUserInterfaceStyleLight; + case Qt::ColorScheme::Unknown: + return UIUserInterfaceStyleUnspecified; + } + }(); + + window.overrideUserInterfaceStyle = style; } const QFont *QIOSTheme::font(Font type) const @@ -157,4 +213,9 @@ const QFont *QIOSTheme::font(Font type) const return coreTextFontDatabase->themeFont(type); } +QIconEngine *QIOSTheme::createIconEngine(const QString &iconName) const +{ + return new QAppleIconEngine(iconName); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosviewcontroller.h b/src/plugins/platforms/ios/qiosviewcontroller.h index 237cd57edf..1f8da41ba4 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.h +++ b/src/plugins/platforms/ios/qiosviewcontroller.h @@ -12,14 +12,12 @@ QT_END_NAMESPACE @interface QIOSViewController : UIViewController -- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen; +- (instancetype)initWithWindow:(UIWindow*)window andScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen; - (void)updateProperties; - (NSArray*)keyCommands; - (void)handleShortcut:(UIKeyCommand*)keyCommand; #ifndef Q_OS_TVOS -@property (nonatomic, assign) UIInterfaceOrientation lockedOrientation; - // UIViewController @property (nonatomic, assign) BOOL prefersStatusBarHidden; @property (nonatomic, assign) UIStatusBarAnimation preferredStatusBarUpdateAnimation; diff --git a/src/plugins/platforms/ios/qiosviewcontroller.mm b/src/plugins/platforms/ios/qiosviewcontroller.mm index c4e8968232..436d1e7bed 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.mm +++ b/src/plugins/platforms/ios/qiosviewcontroller.mm @@ -21,9 +21,12 @@ #include "qioswindow.h" #include "quiview.h" +#include <QtCore/qpointer.h> + // ------------------------------------------------------------------------- @interface QIOSViewController () +@property (nonatomic, assign) UIWindow *window; @property (nonatomic, assign) QPointer<QT_PREPEND_NAMESPACE(QIOSScreen)> platformScreen; @property (nonatomic, assign) BOOL changingOrientation; @end @@ -88,27 +91,30 @@ { Q_UNUSED(subview); - QT_PREPEND_NAMESPACE(QIOSScreen) *screen = self.qtViewController.platformScreen; - - // The 'window' property of our view is not valid until the window - // has been shown, so we have to access it through the QIOSScreen. - UIWindow *uiWindow = screen->uiWindow(); + // Track UIWindow via explicit property on QIOSViewController, + // as the window property of our own view is not valid until + // the window has been shown (below). + UIWindow *uiWindow = self.qtViewController.window; if (uiWindow.hidden) { - // Associate UIWindow to screen and show it the first time a QWindow - // is mapped to the screen. For external screens this means disabling - // mirroring mode and presenting alternate content on the screen. - uiWindow.screen = screen->uiScreen(); + // Show the UIWindow the first time a QWindow is mapped to the screen. + // For the main screen this hides the launch screen, while for external + // screens this disables mirroring of the main screen, so the external + // screen can be used for alternate content. uiWindow.hidden = NO; } } +#if !defined(Q_OS_VISIONOS) - (void)willRemoveSubview:(UIView *)subview { Q_UNUSED(subview); - Q_ASSERT(self.window); UIWindow *uiWindow = self.window; + // uiWindow can be null when closing from the ios "app manager" and the app is + // showing a native window like UIDocumentBrowserViewController + if (!uiWindow) + return; if (uiWindow.screen != [UIScreen mainScreen] && self.subviews.count == 1) { // We're about to remove the last view of an external screen, so go back @@ -116,10 +122,10 @@ // to ensure that we don't try to layout the view that's being removed. dispatch_async(dispatch_get_main_queue(), ^{ uiWindow.hidden = YES; - uiWindow.screen = [UIScreen mainScreen]; }); } } +#endif - (void)layoutSubviews { @@ -225,15 +231,14 @@ @synthesize preferredStatusBarStyle; #endif -- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen +- (instancetype)initWithWindow:(UIWindow*)window andScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen { if (self = [self init]) { + self.window = window; self.platformScreen = screen; self.changingOrientation = NO; #ifndef Q_OS_TVOS - self.lockedOrientation = UIInterfaceOrientationUnknown; - // Status bar may be initially hidden at startup through Info.plist self.prefersStatusBarHidden = infoPlistValue(@"UIStatusBarHidden", false); self.preferredStatusBarUpdateAnimation = UIStatusBarAnimationNone; @@ -280,7 +285,7 @@ Q_ASSERT(!qt_apple_isApplicationExtension()); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(willChangeStatusBarFrame:) name:UIApplicationWillChangeStatusBarFrameNotification @@ -290,6 +295,15 @@ name:UIApplicationDidChangeStatusBarOrientationNotification object:qt_apple_sharedApplication()]; #endif + + // Make sure any top level windows that have already been created + // for this screen are reparented into our desktop manager view. + for (auto *window : qGuiApp->topLevelWindows()) { + if (window->screen()->handle() != self.platformScreen) + continue; + if (auto *platformWindow = window->handle()) + platformWindow->setParent(nullptr); + } } - (void)viewDidUnload @@ -300,26 +314,6 @@ // ------------------------------------------------------------------------- -- (BOOL)shouldAutorotate -{ -#ifndef Q_OS_TVOS - return self.platformScreen && self.platformScreen->uiScreen() == [UIScreen mainScreen] && !self.lockedOrientation; -#else - return NO; -#endif -} - -- (NSUInteger)supportedInterfaceOrientations -{ - // As documented by Apple in the iOS 6.0 release notes, setStatusBarOrientation:animated: - // only works if the supportedInterfaceOrientations of the view controller is 0, making - // us responsible for ensuring that the status bar orientation is consistent. We enter - // this mode when auto-rotation is disabled due to an explicit content orientation being - // set on the focus window. Note that this is counter to what the documentation for - // supportedInterfaceOrientations says, which states that the method should not return 0. - return [self shouldAutorotate] ? UIInterfaceOrientationMaskAll : 0; -} - - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration { self.changingOrientation = YES; @@ -334,6 +328,7 @@ [super didRotateFromInterfaceOrientation:orientation]; } +#if !defined(Q_OS_VISIONOS) - (void)willChangeStatusBarFrame:(NSNotification*)notification { Q_UNUSED(notification); @@ -377,6 +372,7 @@ [self.view setNeedsLayout]; } +#endif - (void)viewWillLayoutSubviews { @@ -397,10 +393,12 @@ if (!self.platformScreen || !self.platformScreen->screen()) return; +#if !defined(Q_OS_VISIONOS) // For now we only care about the main screen, as both the statusbar // visibility and orientation is only appropriate for the main screen. if (self.platformScreen->uiScreen() != [UIScreen mainScreen]) return; +#endif // Prevent recursion caused by updating the status bar appearance (position // or visibility), which in turn may cause a layout of our subviews, and @@ -427,7 +425,7 @@ // All decisions are based on the top level window focusWindow = qt_window_private(focusWindow)->topLevelWindow(); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) // -------------- Status bar style and visbility --------------- @@ -447,51 +445,6 @@ [self setNeedsStatusBarAppearanceUpdate]; [self.view setNeedsLayout]; } - - - // -------------- Content orientation --------------- - - UIApplication *uiApplication = qt_apple_sharedApplication(); - - static BOOL kAnimateContentOrientationChanges = YES; - - Qt::ScreenOrientation contentOrientation = focusWindow->contentOrientation(); - if (contentOrientation != Qt::PrimaryOrientation) { - // An explicit content orientation has been reported for the focus window, - // so we keep the status bar in sync with content orientation. This will ensure - // that the task bar (and associated gestures) are also rotated accordingly. - - if (!self.lockedOrientation) { - // We are moving from Qt::PrimaryOrientation to an explicit orientation, - // so we need to store the current statusbar orientation, as we need it - // later when mapping screen coordinates for QScreen and for returning - // to Qt::PrimaryOrientation. - self.lockedOrientation = uiApplication.statusBarOrientation; - } - - [uiApplication setStatusBarOrientation: - UIInterfaceOrientation(fromQtScreenOrientation(contentOrientation)) - animated:kAnimateContentOrientationChanges]; - - } else { - // The content orientation is set to Qt::PrimaryOrientation, meaning - // that auto-rotation should be enabled. But we may be coming out of - // a state of locked orientation, which needs some cleanup before we - // can enable auto-rotation again. - if (self.lockedOrientation) { - // First we need to restore the statusbar to what it was at the - // time of locking the orientation, otherwise iOS will be very - // confused when it starts doing auto-rotation again. - [uiApplication setStatusBarOrientation:self.lockedOrientation - animated:kAnimateContentOrientationChanges]; - - // Then we can re-enable auto-rotation - self.lockedOrientation = UIInterfaceOrientationUnknown; - - // And finally let iOS rotate the root view to match the device orientation - [UIViewController attemptRotationToDeviceOrientation]; - } - } #endif } diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 988269a855..88afee80c3 100644 --- a/src/plugins/platforms/ios/qioswindow.h +++ b/src/plugins/platforms/ios/qioswindow.h @@ -21,14 +21,13 @@ class QIOSWindow : public QObject, public QPlatformWindow Q_OBJECT public: - explicit QIOSWindow(QWindow *window); + explicit QIOSWindow(QWindow *window, WId nativeHandle = 0); ~QIOSWindow(); void setGeometry(const QRect &rect) override; void setWindowState(Qt::WindowStates state) override; void setParent(const QPlatformWindow *window) override; - void handleContentOrientationChange(Qt::ScreenOrientation orientation) override; void setVisible(bool visible) override; void setOpacity(qreal level) override; @@ -56,13 +55,20 @@ public: void requestUpdate() override; + void setMask(const QRegion ®ion) override; + +#if QT_CONFIG(opengl) CAEAGLLayer *eaglLayer() const; +#endif + + bool isForeignWindow() const override; + UIView *view() const; private: void applicationStateChanged(Qt::ApplicationState state); void applyGeometry(const QRect &rect); - QUIView *m_view; + UIView *m_view; QRect m_normalGeometry; int m_windowLevel; @@ -78,6 +84,8 @@ private: QDebug operator<<(QDebug debug, const QIOSWindow *window); #endif +QT_MANGLE_NAMESPACE(QUIView) *quiview_cast(UIView *view); + QT_END_NAMESPACE #endif // QIOSWINDOW_H diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index 240a2d9915..f461a5f55b 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -4,7 +4,6 @@ #include "qioswindow.h" #include "qiosapplicationdelegate.h" -#include "qioscontext.h" #include "qiosglobal.h" #include "qiosintegration.h" #include "qiosscreen.h" @@ -12,11 +11,17 @@ #include "quiview.h" #include "qiosinputcontext.h" +#include <QtCore/private/qcore_mac_p.h> + #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <qpa/qplatformintegration.h> +#if QT_CONFIG(opengl) #import <QuartzCore/CAEAGLLayer.h> -#ifdef Q_OS_IOS +#endif + +#if QT_CONFIG(metal) #import <QuartzCore/CAMetalLayer.h> #endif @@ -24,34 +29,50 @@ QT_BEGIN_NAMESPACE -QIOSWindow::QIOSWindow(QWindow *window) +enum { + defaultWindowWidth = 160, + defaultWindowHeight = 160 +}; + +QIOSWindow::QIOSWindow(QWindow *window, WId nativeHandle) : QPlatformWindow(window) , m_windowLevel(0) { -#ifdef Q_OS_IOS - if (window->surfaceType() == QSurface::RasterSurface) - window->setSurfaceType(QSurface::MetalSurface); + if (nativeHandle) { + m_view = reinterpret_cast<UIView *>(nativeHandle); + [m_view retain]; + } else { +#if QT_CONFIG(metal) + if (window->surfaceType() == QSurface::RasterSurface) + window->setSurfaceType(QSurface::MetalSurface); - if (window->surfaceType() == QSurface::MetalSurface) - m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; - else + if (window->surfaceType() == QSurface::MetalSurface) + m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; + else #endif - m_view = [[QUIView alloc] initWithQIOSWindow:this]; + m_view = [[QUIView alloc] initWithQIOSWindow:this]; + } connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QIOSWindow::applicationStateChanged); + // Always set parent, even if we don't have a parent window, + // as we use setParent to reparent top levels into our desktop + // manager view. setParent(QPlatformWindow::parent()); - // Resolve default window geometry in case it was not set before creating the - // platform window. This picks up eg. minimum-size if set, and defaults to - // the "maxmized" geometry (even though we're not in that window state). - // FIXME: Detect if we apply a maximized geometry and send a window state - // change event in that case. - m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), - screen()->availableGeometry().width(), screen()->availableGeometry().height()); + if (!isForeignWindow()) { + // Resolve default window geometry in case it was not set before creating the + // platform window. This picks up eg. minimum-size if set. + m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), + defaultWindowWidth, defaultWindowHeight); - setWindowState(window->windowStates()); - setOpacity(window->opacity()); + setWindowState(window->windowStates()); + setOpacity(window->opacity()); + setMask(QHighDpi::toNativeLocalRegion(window->mask(), window)); + } else { + // Pick up essential foreign window state + QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect()); + } Qt::ScreenOrientation initialOrientation = window->contentOrientation(); if (initialOrientation != Qt::PrimaryOrientation) { @@ -73,8 +94,15 @@ QIOSWindow::~QIOSWindow() [m_view touchesCancelled:[NSSet set] withEvent:0]; clearAccessibleCache(); - m_view.platformWindow = 0; - [m_view removeFromSuperview]; + + quiview_cast(m_view).platformWindow = nullptr; + + // Remove from superview, unless we're a foreign window without a + // Qt window parent, in which case the foreign window is used as + // a window container for a Qt UI hierarchy inside a native UI. + if (!(isForeignWindow() && !QPlatformWindow::parent())) + [m_view removeFromSuperview]; + [m_view release]; } @@ -113,7 +141,7 @@ void QIOSWindow::setVisible(bool visible) if (visible && shouldAutoActivateWindow()) { if (!window()->property("_q_showWithoutActivating").toBool()) requestActivateWindow(); - } else if (!visible && [m_view isActiveWindow]) { + } else if (!visible && [quiview_cast(m_view) isActiveWindow]) { // Our window was active/focus window but now hidden, so relinquish // focus to the next possible window in the stack. NSArray<UIView *> *subviews = m_view.viewController.view.subviews; @@ -202,7 +230,7 @@ void QIOSWindow::applyGeometry(const QRect &rect) QMargins QIOSWindow::safeAreaMargins() const { - UIEdgeInsets safeAreaInsets = m_view.qt_safeAreaInsets; + UIEdgeInsets safeAreaInsets = m_view.safeAreaInsets; return QMargins(safeAreaInsets.left, safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom); } @@ -226,22 +254,45 @@ void QIOSWindow::setWindowState(Qt::WindowStates state) if (state & Qt::WindowMinimized) { applyGeometry(QRect()); } else if (state & (Qt::WindowFullScreen | Qt::WindowMaximized)) { - // When an application is in split-view mode, the UIScreen still has the - // same geometry, but the UIWindow is resized to the area reserved for the - // application. We use this to constrain the geometry used when applying the - // fullscreen or maximized window states. Note that we do not do this - // in applyGeometry(), as we don't want to artificially limit window - // placement "outside" of the screen bounds if that's what the user wants. - QRect uiWindowBounds = QRectF::fromCGRect(m_view.window.bounds).toRect(); - QRect fullscreenGeometry = screen()->geometry().intersected(uiWindowBounds); - QRect maximizedGeometry = window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint ? - fullscreenGeometry : screen()->availableGeometry().intersected(uiWindowBounds); + if (NSProcessInfo.processInfo.iOSAppOnMac) { + // iOS apps running as "Designed for iPad" on macOS do not match + // our current window management implementation where a single + // UIWindow is tied to a single screen. And even if we're on the + // right screen, the UIScreen does not account for the 77% scale + // of the UIUserInterfaceIdiomPad environment, so we can't use + // it to clamp the window geometry. Instead just use the UIWindow + // directly, which represents our "screen". + applyGeometry(uiWindowBounds); + } else if (isRunningOnVisionOS()) { + // On visionOS there is no concept of a screen, and hence no concept of + // screen-relative system UI that we should keep top level windows away + // from, so don't apply the UIWindow safe area insets to the screen. + applyGeometry(uiWindowBounds); + } else { + QRect fullscreenGeometry = screen()->geometry(); + QRect maximizedGeometry = fullscreenGeometry; + +#if !defined(Q_OS_VISIONOS) + if (!(window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint)) { + // If the safe area margins reflect the screen's outer edges, + // then reduce the maximized geometry accordingly. Otherwise + // leave it as is, and assume the client will take the safe + // are margins into account explicitly. + UIScreen *uiScreen = m_view.window.windowScene.screen; + UIEdgeInsets safeAreaInsets = m_view.window.safeAreaInsets; + if (m_view.window.bounds.size.width == uiScreen.bounds.size.width) + maximizedGeometry.adjust(safeAreaInsets.left, 0, -safeAreaInsets.right, 0); + if (m_view.window.bounds.size.height == uiScreen.bounds.size.height) + maximizedGeometry.adjust(0, safeAreaInsets.top, 0, -safeAreaInsets.bottom); + } +#endif - if (state & Qt::WindowFullScreen) - applyGeometry(fullscreenGeometry); - else - applyGeometry(maximizedGeometry); + if (state & Qt::WindowFullScreen) + applyGeometry(fullscreenGeometry.intersected(uiWindowBounds)); + else + applyGeometry(maximizedGeometry.intersected(uiWindowBounds)); + } } else { applyGeometry(m_normalGeometry); } @@ -249,10 +300,16 @@ void QIOSWindow::setWindowState(Qt::WindowStates state) void QIOSWindow::setParent(const QPlatformWindow *parentWindow) { - UIView *parentView = parentWindow ? reinterpret_cast<UIView *>(parentWindow->winId()) - : isQtApplication() ? static_cast<QIOSScreen *>(screen())->uiWindow().rootViewController.view : 0; - - [parentView addSubview:m_view]; + UIView *superview = nullptr; + if (parentWindow) + superview = reinterpret_cast<UIView *>(parentWindow->winId()); + else if (isQtApplication() && !isForeignWindow()) + superview = rootViewForScreen(window()->screen()); + + if (superview) + [superview addSubview:m_view]; + else if (quiview_cast(m_view.superview)) + [m_view removeFromSuperview]; } void QIOSWindow::requestActivateWindow() @@ -263,7 +320,6 @@ void QIOSWindow::requestActivateWindow() if (blockedByModal()) return; - Q_ASSERT(m_view.window); [m_view.window makeKeyWindow]; [m_view becomeFirstResponder]; @@ -273,8 +329,6 @@ void QIOSWindow::requestActivateWindow() void QIOSWindow::raiseOrLower(bool raise) { - // Re-insert m_view at the correct index among its sibling views - // (QWindows) according to their current m_windowLevel: if (!isQtApplication()) return; @@ -282,17 +336,27 @@ void QIOSWindow::raiseOrLower(bool raise) if (subviews.count == 1) return; - for (int i = int(subviews.count) - 1; i >= 0; --i) { - UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); - if (view.hidden || view == m_view || !view.qwindow) - continue; - int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; - if (m_windowLevel > level || (raise && m_windowLevel == level)) { - [m_view.superview insertSubview:m_view aboveSubview:view]; - return; + if (m_view.superview == m_view.qtViewController.view) { + // We're a top level window, so we need to take window + // levels into account. + for (int i = int(subviews.count) - 1; i >= 0; --i) { + UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); + if (view.hidden || view == m_view || !view.qwindow) + continue; + int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; + if (m_windowLevel > level || (raise && m_windowLevel == level)) { + [m_view.superview insertSubview:m_view aboveSubview:view]; + return; + } } + [m_view.superview insertSubview:m_view atIndex:0]; + } else { + // Child window, or embedded into a non-Qt view controller + if (raise) + [m_view.superview bringSubviewToFront:m_view]; + else + [m_view.superview sendSubviewToBack:m_view]; } - [m_view.superview insertSubview:m_view atIndex:0]; } void QIOSWindow::updateWindowLevel() @@ -321,20 +385,13 @@ void QIOSWindow::updateWindowLevel() m_windowLevel = qMax(transientParentWindow->m_windowLevel, m_windowLevel); } -void QIOSWindow::handleContentOrientationChange(Qt::ScreenOrientation orientation) -{ - // Update the QWindow representation straight away, so that - // we can update the statusbar orientation based on the new - // content orientation. - qt_window_private(window())->contentOrientation = orientation; - - [m_view.qtViewController updateProperties]; -} - void QIOSWindow::applicationStateChanged(Qt::ApplicationState) { + if (isForeignWindow()) + return; + if (window()->isExposed() != isExposed()) - [m_view sendUpdatedExposeEvent]; + [quiview_cast(m_view) sendUpdatedExposeEvent]; } qreal QIOSWindow::devicePixelRatio() const @@ -344,7 +401,10 @@ qreal QIOSWindow::devicePixelRatio() const void QIOSWindow::clearAccessibleCache() { - [m_view clearAccessibleCache]; + if (isForeignWindow()) + return; + + [quiview_cast(m_view) clearAccessibleCache]; } void QIOSWindow::requestUpdate() @@ -352,11 +412,27 @@ void QIOSWindow::requestUpdate() static_cast<QIOSScreen *>(screen())->setUpdatesPaused(false); } +void QIOSWindow::setMask(const QRegion ®ion) +{ + if (!region.isEmpty()) { + QCFType<CGMutablePathRef> maskPath = CGPathCreateMutable(); + for (const QRect &r : region) + CGPathAddRect(maskPath, nullptr, r.toCGRect()); + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.path = maskPath; + m_view.layer.mask = maskLayer; + } else { + m_view.layer.mask = nil; + } +} + +#if QT_CONFIG(opengl) CAEAGLLayer *QIOSWindow::eaglLayer() const { Q_ASSERT([m_view.layer isKindOfClass:[CAEAGLLayer class]]); return static_cast<CAEAGLLayer *>(m_view.layer); } +#endif #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug debug, const QIOSWindow *window) @@ -371,6 +447,37 @@ QDebug operator<<(QDebug debug, const QIOSWindow *window) } #endif // !QT_NO_DEBUG_STREAM -#include "moc_qioswindow.cpp" +/*! + Returns the view cast to a QUIview if possible. + + If the view is not a QUIview, nil is returned, which is safe to + send messages to, effectively making [quiview_cast(view) message] + a no-op. + + For extra verbosity and clearer code, please consider checking + that the platform window is not a foreign window before using + this cast, via QPlatformWindow::isForeignWindow(). + + Do not use this method solely to check for foreign windows, as + that will make the code harder to read for people not working + primarily on iOS, who do not know the difference between the + UIView and QUIView cases. +*/ +QUIView *quiview_cast(UIView *view) +{ + return qt_objc_cast<QUIView *>(view); +} + +bool QIOSWindow::isForeignWindow() const +{ + return ![m_view isKindOfClass:QUIView.class]; +} + +UIView *QIOSWindow::view() const +{ + return m_view; +} QT_END_NAMESPACE + +#include "moc_qioswindow.cpp" diff --git a/src/plugins/platforms/ios/quiaccessibilityelement.h b/src/plugins/platforms/ios/quiaccessibilityelement.h index e78fef6d30..8580325436 100644 --- a/src/plugins/platforms/ios/quiaccessibilityelement.h +++ b/src/plugins/platforms/ios/quiaccessibilityelement.h @@ -14,7 +14,7 @@ @property (readonly) QAccessible::Id axid; - (instancetype)initWithId:(QAccessible::Id)anId withAccessibilityContainer:(id)view; -+ (instancetype)elementWithId:(QAccessible::Id)anId withAccessibilityContainer:(id)view; ++ (instancetype)elementWithId:(QAccessible::Id)anId; @end diff --git a/src/plugins/platforms/ios/quiaccessibilityelement.mm b/src/plugins/platforms/ios/quiaccessibilityelement.mm index 08e366f32b..39b2cb8a50 100644 --- a/src/plugins/platforms/ios/quiaccessibilityelement.mm +++ b/src/plugins/platforms/ios/quiaccessibilityelement.mm @@ -8,6 +8,7 @@ #include "private/qaccessiblecache_p.h" #include "private/qcore_mac_p.h" #include "uistrings_p.h" +#include "qioswindow.h" QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); @@ -23,7 +24,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); return self; } -+ (instancetype)elementWithId:(QAccessible::Id)anId withAccessibilityContainer:(id)view ++ (instancetype)elementWithId:(QAccessible::Id)anId { Q_ASSERT(anId); if (!anId) @@ -33,9 +34,17 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); QMacAccessibilityElement *element = cache->elementForId(anId); if (!element) { - Q_ASSERT(QAccessible::accessibleInterface(anId)); - element = [[self alloc] initWithId:anId withAccessibilityContainer:view]; - cache->insertElement(anId, element); + auto *a11yInterface = QAccessible::accessibleInterface(anId); + Q_ASSERT(a11yInterface); + auto *window = a11yInterface->window(); + if (window && window->handle()) { + auto *platformWindow = static_cast<QIOSWindow*>(window->handle()); + element = [[self alloc] initWithId:anId withAccessibilityContainer:platformWindow->view()]; + cache->insertElement(anId, element); + } else { + qWarning() << "Could not create a11y element for" << window + << "with platform window" << (window ? window->handle() : nullptr); + } } return element; } diff --git a/src/plugins/platforms/ios/quiview.h b/src/plugins/platforms/ios/quiview.h index 6d1d19fd3d..7899ec6e0e 100644 --- a/src/plugins/platforms/ios/quiview.h +++ b/src/plugins/platforms/ios/quiview.h @@ -32,10 +32,9 @@ QT_END_NAMESPACE - (QWindow *)qwindow; - (UIViewController *)viewController; - (QIOSViewController*)qtViewController; -@property (nonatomic, readonly) UIEdgeInsets qt_safeAreaInsets; @end -#ifdef Q_OS_IOS +#if QT_CONFIG(metal) @interface QUIMetalView : QUIView @end #endif diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm index 32f27587c6..d5808db305 100644 --- a/src/plugins/platforms/ios/quiview.mm +++ b/src/plugins/platforms/ios/quiview.mm @@ -10,6 +10,7 @@ #include "qiosscreen.h" #include "qioswindow.h" #include "qiosinputcontext.h" +#include "quiwindow.h" #ifndef Q_OS_TVOS #include "qiosmenu.h" #endif @@ -22,17 +23,46 @@ #include <qpa/qwindowsysteminterface_p.h> Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") +Q_LOGGING_CATEGORY(lcQpaInputEvents, "qt.qpa.input.events") + +namespace { +inline ulong getTimeStamp(UIEvent *event) +{ +#if TARGET_OS_SIMULATOR == 1 + // We currently build Qt for simulator using X86_64, even on ARM based macs. + // This results in the simulator running on ARM, while the app is running + // inside it using Rosetta. And with this combination, the event.timestamp, which is + // documented to be in seconds, looks to be something else, and is not progressing + // in sync with a normal clock. + // Sending out mouse events with a timestamp that doesn't follow normal clock time + // will cause problems for mouse-, and pointer handlers that uses them to e.g calculate + // the time between a press and release, and to decide if the user is performing a tap + // or a drag. + // For that reason, we choose to ignore UIEvent.timestamp under the mentioned condition, and + // instead rely on NSProcessInfo. Note that if we force the whole simulator to use Rosetta + // (and not only the Qt app), the timestamps will progress normally. +#if defined(Q_PROCESSOR_ARM) + #warning The timestamp work-around for x86_64 can (probably) be removed when building for ARM +#endif + return ulong(NSProcessInfo.processInfo.systemUptime * 1000); +#endif + + return ulong(event.timestamp * 1000); +} +} @implementation QUIView { QHash<NSUInteger, QWindowSystemInterface::TouchPoint> m_activeTouches; UITouch *m_activePencilTouch; - int m_nextTouchId; NSMutableArray<UIAccessibilityElement *> *m_accessibleElements; + UIPanGestureRecognizer *m_scrollGestureRecognizer; + CGPoint m_lastScrollCursorPos; + CGPoint m_lastScrollDelta; } + (void)load { -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) if (QOperatingSystemVersion::current() < QOperatingSystemVersion(QOperatingSystemVersion::IOS, 11)) { // iOS 11 handles this though [UIView safeAreaInsetsDidChange], but there's no signal for // the corresponding top and bottom layout guides that we use on earlier versions. Note @@ -52,7 +82,10 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") + (Class)layerClass { +#if QT_CONFIG(opengl) return [CAEAGLLayer class]; +#endif + return [super layerClass]; } - (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window @@ -60,6 +93,32 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") if (self = [self initWithFrame:window->geometry().toCGRect()]) { self.platformWindow = window; m_accessibleElements = [[NSMutableArray<UIAccessibilityElement *> alloc] init]; + m_scrollGestureRecognizer = [[UIPanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleScroll:)]; + // The gesture recognizer should only care about scroll gestures (for now) + // Set allowedTouchTypes to empty array here to not interfere with touch events + // handled by the UIView. Scroll gestures, even those coming from touch devices, + // such as trackpads will still be received as they are not touch events + m_scrollGestureRecognizer.allowedTouchTypes = [NSArray array]; + if (@available(ios 13.4, *)) { + m_scrollGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskAll; + } + m_scrollGestureRecognizer.maximumNumberOfTouches = 0; + m_lastScrollDelta = CGPointZero; + m_lastScrollCursorPos = CGPointZero; + [self addGestureRecognizer:m_scrollGestureRecognizer]; + + if ([self.layer isKindOfClass:CAMetalLayer.class]) { + QWindow *window = self.platformWindow->window(); + if (QColorSpace colorSpace = window->format().colorSpace(); colorSpace.isValid()) { + QCFType<CFDataRef> iccData = colorSpace.iccProfile().toCFData(); + QCFType<CGColorSpaceRef> cgColorSpace = CGColorSpaceCreateWithICCData(iccData); + CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(self.layer); + metalLayer.colorspace = cgColorSpace; + qCDebug(lcQpaWindow) << "Set" << self << "color space to" << metalLayer.colorspace; + } + } } return self; @@ -68,6 +127,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { +#if QT_CONFIG(opengl) if ([self.layer isKindOfClass:[CAEAGLLayer class]]) { // Set up EAGL layer CAEAGLLayer *eaglLayer = static_cast<CAEAGLLayer *>(self.layer); @@ -77,6 +137,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 }; } +#endif if (isQtApplication()) self.hidden = YES; @@ -117,6 +178,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") - (void)dealloc { [m_accessibleElements release]; + [m_scrollGestureRecognizer release]; [super dealloc]; } @@ -136,6 +198,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") return description; } +#if !defined(Q_OS_VISIONOS) - (void)willMoveToWindow:(UIWindow *)newWindow { // UIKIt will normally set the scale factor of a view to match the corresponding @@ -145,6 +208,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") // FIXME: Allow the scale factor to be customized through QSurfaceFormat. } +#endif - (void)didAddSubview:(UIView *)subview { @@ -202,6 +266,9 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") Q_UNUSED(layer); Q_ASSERT(layer == self.layer); + if (!self.platformWindow) + return; + [self sendUpdatedExposeEvent]; } @@ -243,7 +310,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") // blocked by this guard. FirstResponderCandidate firstResponderCandidate(self); - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; if (![super becomeFirstResponder]) { qImDebug() << self << "was not allowed to become first responder"; @@ -254,7 +321,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") } if (qGuiApp->focusWindow() != self.platformWindow->window()) - QWindowSystemInterface::handleWindowActivated(self.platformWindow->window(), Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(self.platformWindow->window(), Qt::ActiveWindowFocusReason); else qImDebug() << self.platformWindow->window() << "already active, not sending window activation"; @@ -282,7 +349,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") - (BOOL)resignFirstResponder { - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; if (![super resignFirstResponder]) return NO; @@ -291,7 +358,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") UIResponder *newResponder = FirstResponderCandidate::currentCandidate(); if ([self responderShouldTriggerWindowDeactivation:newResponder]) - QWindowSystemInterface::handleWindowActivated(nullptr, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason); return YES; } @@ -305,7 +372,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") if ([self isFirstResponder]) return YES; - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; if ([firstResponder isKindOfClass:[QIOSTextInputResponder class]] && [firstResponder nextResponder] == self) return YES; @@ -456,7 +523,10 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") { Q_ASSERT(!m_activeTouches.contains(touch.hash)); #endif - m_activeTouches[touch.hash].id = m_nextTouchId++; + // Use window-independent touch identifiers, so that + // multi-touch works across windows. + static quint16 nextTouchId = 0; + m_activeTouches[touch.hash].id = nextTouchId++; #if QT_CONFIG(tabletevent) } #endif @@ -470,17 +540,17 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") topLevel->requestActivateWindow(); } - [self handleTouches:touches withEvent:event withState:QEventPoint::State::Pressed withTimestamp:ulong(event.timestamp * 1000)]; + [self handleTouches:touches withEvent:event withState:QEventPoint::State::Pressed withTimestamp:getTimeStamp(event)]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { - [self handleTouches:touches withEvent:event withState:QEventPoint::State::Updated withTimestamp:ulong(event.timestamp * 1000)]; + [self handleTouches:touches withEvent:event withState:QEventPoint::State::Updated withTimestamp:getTimeStamp(event)]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - [self handleTouches:touches withEvent:event withState:QEventPoint::State::Released withTimestamp:ulong(event.timestamp * 1000)]; + [self handleTouches:touches withEvent:event withState:QEventPoint::State::Released withTimestamp:getTimeStamp(event)]; // Remove ended touch points from the active set: #ifndef Q_OS_TVOS @@ -498,9 +568,6 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") // tvOS only supports single touch m_activeTouches.clear(); #endif - - if (m_activeTouches.isEmpty() && !m_activePencilTouch) - m_nextTouchId = 0; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event @@ -533,10 +600,9 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") qWarning("Subset of active touches cancelled by UIKit"); m_activeTouches.clear(); - m_nextTouchId = 0; m_activePencilTouch = nil; - NSTimeInterval timestamp = event ? event.timestamp : [[NSProcessInfo processInfo] systemUptime]; + ulong timestamp = event ? getTimeStamp(event) : ([[NSProcessInfo processInfo] systemUptime] * 1000); QIOSIntegration *iosIntegration = static_cast<QIOSIntegration *>(QGuiApplicationPrivate::platformIntegration()); @@ -544,7 +610,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") // event loop in response to the touch event (a dialog e.g.), which will deadlock // the UIKit event delivery system (QTBUG-98651). QWindowSystemInterface::handleTouchCancelEvent<QWindowSystemInterface::AsynchronousDelivery>( - self.platformWindow->window(), ulong(timestamp * 1000), iosIntegration->touchDevice()); + self.platformWindow->window(), timestamp, iosIntegration->touchDevice()); } - (int)mapPressTypeToKey:(UIPress*)press withModifiers:(Qt::KeyboardModifiers)qtModifiers text:(QString &)text @@ -558,7 +624,6 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") case UIPressTypeMenu: return Qt::Key_Menu; case UIPressTypePlayPause: return Qt::Key_MediaTogglePlayPause; } -#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_13_4) if (@available(ios 13.4, *)) { NSString *charactersIgnoringModifiers = press.key.charactersIgnoringModifiers; Qt::Key key = QAppleKeyMapper::fromUIKitKey(charactersIgnoringModifiers); @@ -567,47 +632,53 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") return QAppleKeyMapper::fromNSString(qtModifiers, press.key.characters, charactersIgnoringModifiers, text); } -#endif return Qt::Key_unknown; } -- (bool)processPresses:(NSSet *)presses withType:(QEvent::Type)type { +- (bool)isControlKey:(Qt::Key)key +{ + switch (key) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Left: + case Qt::Key_Right: + return true; + default: + break; + } + + return false; +} + +- (bool)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type +{ // Presses on Menu button will generate a Menu key event. By default, not handling // this event will cause the application to return to Headboard (tvOS launcher). // When handling the event (for example, as a back button), both press and // release events must be handled accordingly. + if (!qApp->focusWindow()) + return false; + + bool eventHandled = false; + const bool imEnabled = QIOSInputContext::instance()->inputMethodAccepted(); - bool handled = false; for (UIPress* press in presses) { Qt::KeyboardModifiers qtModifiers = Qt::NoModifier; -#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_13_4) if (@available(ios 13.4, *)) qtModifiers = QAppleKeyMapper::fromUIKitModifiers(press.key.modifierFlags); -#endif QString text; int key = [self mapPressTypeToKey:press withModifiers:qtModifiers text:text]; if (key == Qt::Key_unknown) continue; - if (QWindowSystemInterface::handleKeyEvent(self.platformWindow->window(), type, key, - qtModifiers, text)) { - handled = true; - } - } - - return handled; -} + if (imEnabled && ![self isControlKey:Qt::Key(key)]) + continue; -- (BOOL)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type -{ - bool handlePress = false; - if (qApp->focusWindow()) { - QInputMethodQueryEvent queryEvent(Qt::ImEnabled); - if (qApp->focusObject() && QCoreApplication::sendEvent(qApp->focusObject(), &queryEvent)) - handlePress = queryEvent.value(Qt::ImEnabled).toBool(); - if (!handlePress && [self processPresses:presses withType:type]) - return true; + bool keyHandled = QWindowSystemInterface::handleKeyEvent( + self.platformWindow->window(), type, key, qtModifiers, text); + eventHandled = eventHandled || keyHandled; } - return false; + + return eventHandled; } - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event @@ -619,20 +690,20 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") - (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event { if (![self handlePresses:presses eventType:QEvent::KeyPress]) - [super pressesBegan:presses withEvent:event]; + [super pressesChanged:presses withEvent:event]; [super pressesChanged:presses withEvent:event]; } - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event { if (![self handlePresses:presses eventType:QEvent::KeyRelease]) - [super pressesBegan:presses withEvent:event]; + [super pressesEnded:presses withEvent:event]; [super pressesEnded:presses withEvent:event]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) // Check first if QIOSMenu should handle the action before continuing up the responder chain return [QIOSMenu::menuActionTarget() targetForAction:action withSender:sender] != 0; #else @@ -645,7 +716,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") - (id)forwardingTargetForSelector:(SEL)selector { Q_UNUSED(selector); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) return QIOSMenu::menuActionTarget(); #else return nil; @@ -672,6 +743,63 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") UIEditingInteractionConfigurationDefault : UIEditingInteractionConfigurationNone; } +#if QT_CONFIG(wheelevent) +- (void)handleScroll:(UIPanGestureRecognizer *)recognizer +{ + if (!self.platformWindow->window()) + return; + + if (!self.canBecomeFirstResponder) + return; + + CGPoint translation = [recognizer translationInView:self]; + CGFloat deltaX = translation.x - m_lastScrollDelta.x; + CGFloat deltaY = translation.y - m_lastScrollDelta.y; + + QPoint angleDelta; + // From QNSView implementation: + // "Since deviceDelta is delivered as pixels rather than degrees, we need to + // convert from pixels to degrees in a sensible manner. + // It looks like 1/4 degrees per pixel behaves most native. + // (NB: Qt expects the unit for delta to be 8 per degree):" + const int pixelsToDegrees = 2; // 8 * 1/4 + angleDelta.setX(deltaX * pixelsToDegrees); + angleDelta.setY(deltaY * pixelsToDegrees); + + QPoint pixelDelta; + pixelDelta.setX(deltaX); + pixelDelta.setY(deltaY); + + NSTimeInterval time_stamp = [[NSProcessInfo processInfo] systemUptime]; + ulong qt_timestamp = time_stamp * 1000; + + Qt::KeyboardModifiers qt_modifierFlags = Qt::NoModifier; + if (@available(ios 13.4, *)) + qt_modifierFlags = QAppleKeyMapper::fromUIKitModifiers(recognizer.modifierFlags); + + if (recognizer.state == UIGestureRecognizerStateBegan) + // locationInView: doesn't return the cursor position at the time of the wheel event, + // but rather gives us the position with the deltas applied, so we need to save the + // cursor position at the beginning of the gesture + m_lastScrollCursorPos = [recognizer locationInView:self]; + + if (recognizer.state != UIGestureRecognizerStateEnded) { + m_lastScrollDelta.x = translation.x; + m_lastScrollDelta.y = translation.y; + } else { + m_lastScrollDelta = CGPointZero; + } + + QPoint qt_local = QPointF::fromCGPoint(m_lastScrollCursorPos).toPoint(); + QPoint qt_global = self.platformWindow->mapToGlobal(qt_local); + + qCInfo(lcQpaInputEvents).nospace() << "wheel event" << " at " << qt_local + << " pixelDelta=" << pixelDelta << " angleDelta=" << angleDelta; + + QWindowSystemInterface::handleWheelEvent(self.platformWindow->window(), qt_timestamp, qt_local, qt_global, pixelDelta, angleDelta, qt_modifierFlags); +} +#endif // QT_CONFIG(wheelevent) + @end @implementation UIView (QtHelpers) @@ -704,27 +832,14 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") return nil; } -- (UIEdgeInsets)qt_safeAreaInsets -{ - return self.safeAreaInsets; -} - @end -#ifdef Q_OS_IOS +#if QT_CONFIG(metal) @implementation QUIMetalView + (Class)layerClass { -#ifdef TARGET_IPHONE_SIMULATOR - if (@available(ios 13.0, *)) -#endif - return [CAMetalLayer class]; - -#ifdef TARGET_IPHONE_SIMULATOR - return nil; -#endif } @end diff --git a/src/plugins/platforms/ios/quiview_accessibility.mm b/src/plugins/platforms/ios/quiview_accessibility.mm index bb27d832d1..04e1f8cfb3 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -23,9 +23,11 @@ if (!iface || iface->state().invisible) return; - [self createAccessibleElement: iface]; for (int i = 0; i < iface->childCount(); ++i) [self createAccessibleContainer: iface->child(i)]; + + // The container element must go last, so that it underlays all its children + [self createAccessibleElement:iface]; } - (void)initAccessibility @@ -52,7 +54,6 @@ - (void)clearAccessibleCache { [m_accessibleElements removeAllObjects]; - UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, @""); } // this is a container, returning yes here means the functions below will never be called diff --git a/src/plugins/platforms/ios/quiwindow.h b/src/plugins/platforms/ios/quiwindow.h new file mode 100644 index 0000000000..b5587411e4 --- /dev/null +++ b/src/plugins/platforms/ios/quiwindow.h @@ -0,0 +1,13 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QUIWINDOW_H +#define QUIWINDOW_H + +#include <UIKit/UIWindow.h> + +@interface QUIWindow : UIWindow +@property (nonatomic, readonly) BOOL sendingEvent; +@end + +#endif // QUIWINDOW_H diff --git a/src/plugins/platforms/ios/quiwindow.mm b/src/plugins/platforms/ios/quiwindow.mm new file mode 100644 index 0000000000..783e243e10 --- /dev/null +++ b/src/plugins/platforms/ios/quiwindow.mm @@ -0,0 +1,65 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "quiwindow.h" + +#include "qiostheme.h" + +#include <QtCore/qscopedvaluerollback.h> + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> + +#include <UIKit/UIKit.h> + +@implementation QUIWindow + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) + self->_sendingEvent = NO; + + return self; +} + +- (instancetype)initWithWindowScene:(UIWindowScene *)windowScene +{ + if ((self = [super initWithWindowScene:windowScene])) + self->_sendingEvent = NO; + + QIOSTheme::applyTheme(self); + return self; +} + +- (void)sendEvent:(UIEvent *)event +{ + QScopedValueRollback<BOOL> sendingEvent(self->_sendingEvent, YES); + [super sendEvent:event]; +} + +#if !defined(Q_OS_VISIONOS) +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection +{ + [super traitCollectionDidChange:previousTraitCollection]; + + if (!qGuiApp) + return; + + Qt::ColorScheme colorScheme = self.traitCollection.userInterfaceStyle + == UIUserInterfaceStyleDark + ? Qt::ColorScheme::Dark + : Qt::ColorScheme::Light; + + if (self.screen == UIScreen.mainScreen) { + // Check if the current userInterfaceStyle reports a different appearance than + // the platformTheme's appearance. We might have set that one based on the UIScreen + if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle + || QGuiApplicationPrivate::platformTheme()->colorScheme() != colorScheme) { + QIOSTheme::initializeSystemPalette(); + QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); + } + } +} +#endif + +@end diff --git a/src/plugins/platforms/linuxfb/CMakeLists.txt b/src/plugins/platforms/linuxfb/CMakeLists.txt index a671dcfc1e..ba18cea50c 100644 --- a/src/plugins/platforms/linuxfb/CMakeLists.txt +++ b/src/plugins/platforms/linuxfb/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from linuxfb.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QLinuxFbIntegrationPlugin Plugin: @@ -7,7 +8,7 @@ qt_internal_add_plugin(QLinuxFbIntegrationPlugin OUTPUT_NAME qlinuxfb PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES linuxfb # special case + DEFAULT_IF "linuxfb" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qlinuxfbintegration.cpp qlinuxfbintegration.h @@ -22,9 +23,6 @@ qt_internal_add_plugin(QLinuxFbIntegrationPlugin Qt::GuiPrivate ) -#### Keys ignored in scope 1:.:.:linuxfb.pro:<TRUE>: -# OTHER_FILES = "linuxfb.json" - ## Scopes: ##################################################################### @@ -39,6 +37,3 @@ qt_internal_extend_target(QLinuxFbIntegrationPlugin CONDITION TARGET Qt::KmsSupp LIBRARIES Qt::KmsSupportPrivate ) - -#### Keys ignored in scope 4:.:.:linuxfb.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/linuxfb/main.cpp b/src/plugins/platforms/linuxfb/main.cpp index 9d05e64466..12aaafc21a 100644 --- a/src/plugins/platforms/linuxfb/main.cpp +++ b/src/plugins/platforms/linuxfb/main.cpp @@ -22,7 +22,7 @@ QPlatformIntegration* QLinuxFbIntegrationPlugin::create(const QString& system, c if (!system.compare("linuxfb"_L1, Qt::CaseInsensitive)) return new QLinuxFbIntegration(paramList); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/linuxfb/qlinuxfbdrmscreen.cpp b/src/plugins/platforms/linuxfb/qlinuxfbdrmscreen.cpp index beb8349635..56b28ba48c 100644 --- a/src/plugins/platforms/linuxfb/qlinuxfbdrmscreen.cpp +++ b/src/plugins/platforms/linuxfb/qlinuxfbdrmscreen.cpp @@ -62,7 +62,7 @@ public: void swapBuffers(Output *output); - int outputCount() const { return m_outputs.count(); } + int outputCount() const { return m_outputs.size(); } Output *output(int idx) { return &m_outputs[idx]; } private: @@ -125,8 +125,7 @@ void QLinuxFbDevice::close() void *QLinuxFbDevice::nativeDisplay() const { - Q_UNREACHABLE(); - return nullptr; + Q_UNREACHABLE_RETURN(nullptr); } QPlatformScreen *QLinuxFbDevice::createScreen(const QKmsOutput &output) @@ -243,7 +242,7 @@ bool QLinuxFbDevice::createFramebuffer(QLinuxFbDevice::Output *output, int buffe qErrnoWarning(errno, "Failed to map dumb buffer"); return false; } - fb.p = mmap(0, fb.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd(), mreq.offset); + fb.p = mmap(nullptr, fb.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd(), mreq.offset); if (fb.p == MAP_FAILED) { qErrnoWarning(errno, "Failed to mmap dumb buffer"); return false; @@ -408,7 +407,7 @@ QRegion QLinuxFbDrmScreen::doRedraw() // Image has alpha but no need for blending at this stage. // Do not waste time with the default SourceOver. pntr.setCompositionMode(QPainter::CompositionMode_Source); - for (const QRect &rect : qAsConst(output->dirty[output->backFb])) + for (const QRect &rect : std::as_const(output->dirty[output->backFb])) pntr.drawImage(rect, mScreenImage, rect); pntr.end(); diff --git a/src/plugins/platforms/linuxfb/qlinuxfbintegration.cpp b/src/plugins/platforms/linuxfb/qlinuxfbintegration.cpp index a9ab8463f4..f87766cf1b 100644 --- a/src/plugins/platforms/linuxfb/qlinuxfbintegration.cpp +++ b/src/plugins/platforms/linuxfb/qlinuxfbintegration.cpp @@ -42,7 +42,7 @@ QLinuxFbIntegration::QLinuxFbIntegration(const QStringList ¶mList) : m_primaryScreen(nullptr), m_fontDb(new QGenericUnixFontDatabase), m_services(new QGenericUnixServices), - m_kbdMgr(0) + m_kbdMgr(nullptr) { #if QT_CONFIG(kms) if (qEnvironmentVariableIntValue("QT_QPA_FB_DRM") != 0) @@ -147,7 +147,7 @@ QPlatformNativeInterface *QLinuxFbIntegration::nativeInterface() const QFunctionPointer QLinuxFbIntegration::platformFunction(const QByteArray &function) const { Q_UNUSED(function); - return 0; + return nullptr; } #if QT_CONFIG(evdev) diff --git a/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp b/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp index 1fedb546eb..e2826f3946 100644 --- a/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp +++ b/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp @@ -213,7 +213,7 @@ static QImage::Format determineFormat(const fb_var_screeninfo &info, int depth) static int openTtyDevice(const QString &device) { - const char *const devs[] = { "/dev/tty0", "/dev/tty", "/dev/console", 0 }; + const char *const devs[] = { "/dev/tty0", "/dev/tty", "/dev/console", nullptr }; int fd = -1; if (device.isEmpty()) { @@ -253,9 +253,9 @@ static void blankScreen(int fd, bool on) } QLinuxFbScreen::QLinuxFbScreen(const QStringList &args) - : mArgs(args), mFbFd(-1), mTtyFd(-1), mBlitter(0) + : mArgs(args), mFbFd(-1), mTtyFd(-1), mBlitter(nullptr) { - mMmap.data = 0; + mMmap.data = nullptr; } QLinuxFbScreen::~QLinuxFbScreen() @@ -286,7 +286,7 @@ bool QLinuxFbScreen::initialize() bool doSwitchToGraphicsMode = true; // Parse arguments - for (const QString &arg : qAsConst(mArgs)) { + for (const QString &arg : std::as_const(mArgs)) { QRegularExpressionMatch match; if (arg == "nographicsmodeswitch"_L1) doSwitchToGraphicsMode = false; @@ -344,7 +344,7 @@ bool QLinuxFbScreen::initialize() // mmap the framebuffer mMmap.size = finfo.smem_len; - uchar *data = (unsigned char *)mmap(0, mMmap.size, PROT_READ | PROT_WRITE, MAP_SHARED, mFbFd, 0); + uchar *data = (unsigned char *)mmap(nullptr, mMmap.size, PROT_READ | PROT_WRITE, MAP_SHARED, mFbFd, 0); if ((long)data == -1) { qErrnoWarning(errno, "Failed to mmap framebuffer"); return false; diff --git a/src/plugins/platforms/minimal/CMakeLists.txt b/src/plugins/platforms/minimal/CMakeLists.txt index a763dbac9a..18d8828134 100644 --- a/src/plugins/platforms/minimal/CMakeLists.txt +++ b/src/plugins/platforms/minimal/CMakeLists.txt @@ -1,15 +1,16 @@ -# Generated from minimal.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QMinimalIntegrationPlugin Plugin: ##################################################################### -qt_find_package(WrapFreetype PROVIDED_TARGETS WrapFreetype::WrapFreetype) # special case +qt_find_package(WrapFreetype PROVIDED_TARGETS WrapFreetype::WrapFreetype) qt_internal_add_plugin(QMinimalIntegrationPlugin OUTPUT_NAME qminimal PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES minimal # special case + DEFAULT_IF "minimal" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qminimalbackingstore.cpp qminimalbackingstore.h @@ -23,9 +24,6 @@ qt_internal_add_plugin(QMinimalIntegrationPlugin Qt::GuiPrivate ) -#### Keys ignored in scope 1:.:.:minimal.pro:<TRUE>: -# OTHER_FILES = "minimal.json" - ## Scopes: ##################################################################### @@ -33,6 +31,3 @@ qt_internal_extend_target(QMinimalIntegrationPlugin CONDITION QT_FEATURE_freetyp LIBRARIES WrapFreetype::WrapFreetype ) - -#### Keys ignored in scope 3:.:.:minimal.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/minimal/main.cpp b/src/plugins/platforms/minimal/main.cpp index 426ca2ad11..03cb3ca588 100644 --- a/src/plugins/platforms/minimal/main.cpp +++ b/src/plugins/platforms/minimal/main.cpp @@ -22,7 +22,7 @@ QPlatformIntegration *QMinimalIntegrationPlugin::create(const QString& system, c if (!system.compare("minimal"_L1, Qt::CaseInsensitive)) return new QMinimalIntegration(paramList); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/minimal/qminimalintegration.cpp b/src/plugins/platforms/minimal/qminimalintegration.cpp index 7523a2d966..4679967431 100644 --- a/src/plugins/platforms/minimal/qminimalintegration.cpp +++ b/src/plugins/platforms/minimal/qminimalintegration.cpp @@ -6,10 +6,11 @@ #include <QtGui/private/qpixmap_raster_p.h> #include <QtGui/private/qguiapplication_p.h> +#include <qpa/qplatformfontdatabase.h> +#include <qpa/qplatformnativeinterface.h> #include <qpa/qplatformwindow.h> #include <qpa/qwindowsysteminterface.h> -#include <QtGui/private/qfreetypefontdatabase_p.h> #if defined(Q_OS_WIN) # include <QtGui/private/qwindowsfontdatabase_p.h> # if QT_CONFIG(freetype) @@ -21,11 +22,11 @@ #if QT_CONFIG(fontconfig) # include <QtGui/private/qgenericunixfontdatabase_p.h> -# include <qpa/qplatformfontdatabase.h> #endif #if QT_CONFIG(freetype) #include <QtGui/private/qfontengine_ft_p.h> +#include <QtGui/private/qfreetypefontdatabase_p.h> #endif #if !defined(Q_OS_WIN) @@ -57,7 +58,7 @@ static inline unsigned parseOptions(const QStringList ¶mList) } QMinimalIntegration::QMinimalIntegration(const QStringList ¶meters) - : m_fontDatabase(0) + : m_fontDatabase(nullptr) , m_options(parseOptions(parameters)) { if (qEnvironmentVariableIsSet(debugBackingStoreEnvironmentVariable) @@ -157,6 +158,13 @@ QAbstractEventDispatcher *QMinimalIntegration::createEventDispatcher() const #endif } +QPlatformNativeInterface *QMinimalIntegration::nativeInterface() const +{ + if (!m_nativeInterface) + m_nativeInterface.reset(new QPlatformNativeInterface); + return m_nativeInterface.get(); +} + QMinimalIntegration *QMinimalIntegration::instance() { return static_cast<QMinimalIntegration *>(QGuiApplicationPrivate::platformIntegration()); diff --git a/src/plugins/platforms/minimal/qminimalintegration.h b/src/plugins/platforms/minimal/qminimalintegration.h index 914f26bf25..6070972b1b 100644 --- a/src/plugins/platforms/minimal/qminimalintegration.h +++ b/src/plugins/platforms/minimal/qminimalintegration.h @@ -7,6 +7,8 @@ #include <qpa/qplatformintegration.h> #include <qpa/qplatformscreen.h> +#include <qscopedpointer.h> + QT_BEGIN_NAMESPACE class QMinimalScreen : public QPlatformScreen @@ -46,12 +48,15 @@ public: QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override; QAbstractEventDispatcher *createEventDispatcher() const override; + QPlatformNativeInterface *nativeInterface() const override; + unsigned options() const { return m_options; } static QMinimalIntegration *instance(); private: mutable QPlatformFontDatabase *m_fontDatabase; + mutable QScopedPointer<QPlatformNativeInterface> m_nativeInterface; QMinimalScreen *m_primaryScreen; unsigned m_options; }; diff --git a/src/plugins/platforms/minimalegl/CMakeLists.txt b/src/plugins/platforms/minimalegl/CMakeLists.txt index f285d8ece6..b93f325b8f 100644 --- a/src/plugins/platforms/minimalegl/CMakeLists.txt +++ b/src/plugins/platforms/minimalegl/CMakeLists.txt @@ -1,5 +1,7 @@ -# Generated from minimalegl.pro. -qt_find_package(EGL) # special case +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_find_package(EGL) ##################################################################### ## QMinimalEglIntegrationPlugin Plugin: @@ -8,7 +10,7 @@ qt_find_package(EGL) # special case qt_internal_add_plugin(QMinimalEglIntegrationPlugin OUTPUT_NAME qminimalegl PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES minimalegl # special case + DEFAULT_IF "minimalegl" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qminimaleglintegration.cpp qminimaleglintegration.h @@ -21,12 +23,9 @@ qt_internal_add_plugin(QMinimalEglIntegrationPlugin Qt::CorePrivate Qt::Gui Qt::GuiPrivate - EGL::EGL # special case + EGL::EGL ) -#### Keys ignored in scope 1:.:.:minimalegl.pro:<TRUE>: -# OTHER_FILES = "minimalegl.json" - ## Scopes: ##################################################################### @@ -36,6 +35,3 @@ qt_internal_extend_target(QMinimalEglIntegrationPlugin CONDITION QT_FEATURE_open LIBRARIES Qt::OpenGL ) - -#### Keys ignored in scope 3:.:.:minimalegl.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/minimalegl/main.cpp b/src/plugins/platforms/minimalegl/main.cpp index 8f7d91dce9..3586535277 100644 --- a/src/plugins/platforms/minimalegl/main.cpp +++ b/src/plugins/platforms/minimalegl/main.cpp @@ -22,7 +22,7 @@ QPlatformIntegration* QMinimalEglIntegrationPlugin::create(const QString& system if (!system.compare("minimalegl"_L1, Qt::CaseInsensitive)) return new QMinimalEglIntegration; - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/minimalegl/qminimaleglbackingstore.cpp b/src/plugins/platforms/minimalegl/qminimaleglbackingstore.cpp index 5b18ffda2c..88131f4f99 100644 --- a/src/plugins/platforms/minimalegl/qminimaleglbackingstore.cpp +++ b/src/plugins/platforms/minimalegl/qminimaleglbackingstore.cpp @@ -11,7 +11,7 @@ QT_BEGIN_NAMESPACE QMinimalEglBackingStore::QMinimalEglBackingStore(QWindow *window) : QPlatformBackingStore(window) , m_context(new QOpenGLContext) - , m_device(0) + , m_device(nullptr) { m_context->setFormat(window->requestedFormat()); m_context->setScreen(window->screen()); diff --git a/src/plugins/platforms/minimalegl/qminimaleglscreen.cpp b/src/plugins/platforms/minimalegl/qminimaleglscreen.cpp index 64123f0e8f..716caea067 100644 --- a/src/plugins/platforms/minimalegl/qminimaleglscreen.cpp +++ b/src/plugins/platforms/minimalegl/qminimaleglscreen.cpp @@ -41,8 +41,8 @@ public: QMinimalEglScreen::QMinimalEglScreen(EGLNativeDisplayType display) : m_depth(32) , m_format(QImage::Format_Invalid) - , m_platformContext(0) - , m_surface(0) + , m_platformContext(nullptr) + , m_surface(nullptr) { #ifdef QEGL_EXTRA_DEBUG qWarning("QEglScreen %p\n", this); @@ -123,7 +123,7 @@ void QMinimalEglScreen::createAndSetPlatformContext() q_printEglConfig(m_dpy, config); #endif - m_surface = eglCreateWindowSurface(m_dpy, config, eglWindow, NULL); + m_surface = eglCreateWindowSurface(m_dpy, config, eglWindow, nullptr); if (Q_UNLIKELY(m_surface == EGL_NO_SURFACE)) { qWarning("Could not create the egl surface: error = 0x%x\n", eglGetError()); eglTerminate(m_dpy); @@ -132,7 +132,7 @@ void QMinimalEglScreen::createAndSetPlatformContext() // qWarning("Created surface %dx%d\n", w, h); #ifndef QT_NO_OPENGL - QEGLPlatformContext *platformContext = new QMinimalEglContext(platformFormat, 0, m_dpy); + QEGLPlatformContext *platformContext = new QMinimalEglContext(platformFormat, nullptr, m_dpy); m_platformContext = platformContext; #endif EGLint w,h; // screen size detection diff --git a/src/plugins/platforms/offscreen/CMakeLists.txt b/src/plugins/platforms/offscreen/CMakeLists.txt index 424da0a980..907c2c9cc6 100644 --- a/src/plugins/platforms/offscreen/CMakeLists.txt +++ b/src/plugins/platforms/offscreen/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from offscreen.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QOffscreenIntegrationPlugin Plugin: @@ -7,7 +8,7 @@ qt_internal_add_plugin(QOffscreenIntegrationPlugin OUTPUT_NAME qoffscreen PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES offscreen # special case + DEFAULT_IF "offscreen" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qoffscreencommon.cpp qoffscreencommon.h @@ -22,9 +23,6 @@ qt_internal_add_plugin(QOffscreenIntegrationPlugin Qt::GuiPrivate ) -#### Keys ignored in scope 1:.:.:offscreen.pro:<TRUE>: -# OTHER_FILES = "offscreen.json" - ## Scopes: ##################################################################### @@ -34,6 +32,3 @@ qt_internal_extend_target(QOffscreenIntegrationPlugin CONDITION QT_FEATURE_openg LIBRARIES X11::X11 ) - -#### Keys ignored in scope 3:.:.:offscreen.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/offscreen/qoffscreencommon.cpp b/src/plugins/platforms/offscreen/qoffscreencommon.cpp index 6e063a2b56..923fffb29c 100644 --- a/src/plugins/platforms/offscreen/qoffscreencommon.cpp +++ b/src/plugins/platforms/offscreen/qoffscreencommon.cpp @@ -189,13 +189,13 @@ QPixmap QOffscreenBackingStore::grabWindow(WId window, const QRect &rect) const QOffscreenBackingStore *QOffscreenBackingStore::backingStoreForWinId(WId id) { - return m_backingStoreForWinIdHash.value(id, 0); + return m_backingStoreForWinIdHash.value(id, nullptr); } void QOffscreenBackingStore::clearHash() { for (auto it = m_windowAreaHash.cbegin(), end = m_windowAreaHash.cend(); it != end; ++it) { - const auto it2 = qAsConst(m_backingStoreForWinIdHash).find(it.key()); + const auto it2 = std::as_const(m_backingStoreForWinIdHash).find(it.key()); if (it2.value() == this) m_backingStoreForWinIdHash.erase(it2); } diff --git a/src/plugins/platforms/offscreen/qoffscreenintegration.cpp b/src/plugins/platforms/offscreen/qoffscreenintegration.cpp index cb02205601..ea8042928a 100644 --- a/src/plugins/platforms/offscreen/qoffscreenintegration.cpp +++ b/src/plugins/platforms/offscreen/qoffscreenintegration.cpp @@ -82,8 +82,8 @@ QOffscreenIntegration::QOffscreenIntegration(const QStringList& paramList) QOffscreenIntegration::~QOffscreenIntegration() { - for (auto screen : std::as_const(m_screens)) - QWindowSystemInterface::handleScreenRemoved(screen); + while (!m_screens.isEmpty()) + QWindowSystemInterface::handleScreenRemoved(m_screens.takeLast()); } /* @@ -145,7 +145,7 @@ std::optional<QJsonObject> QOffscreenIntegration::resolveConfigFileConfiguration QString configPrefix("configfile="_L1); if (param.startsWith(configPrefix)) { hasConfigFile = true; - configFilePath = param.mid(configPrefix.length()); + configFilePath = param.mid(configPrefix.size()); } } if (!hasConfigFile) diff --git a/src/plugins/platforms/offscreen/qoffscreenwindow.cpp b/src/plugins/platforms/offscreen/qoffscreenwindow.cpp index b009722e56..1a1471c687 100644 --- a/src/plugins/platforms/offscreen/qoffscreenwindow.cpp +++ b/src/plugins/platforms/offscreen/qoffscreenwindow.cpp @@ -86,7 +86,7 @@ void QOffscreenWindow::setVisible(bool visible) if (visible) { if (window()->type() != Qt::ToolTip) - QWindowSystemInterface::handleWindowActivated(window(), Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(window(), Qt::ActiveWindowFocusReason); if (m_pendingGeometryChangeOnShow) { m_pendingGeometryChangeOnShow = false; @@ -122,7 +122,7 @@ void QOffscreenWindow::setVisible(bool visible) void QOffscreenWindow::requestActivateWindow() { if (m_visible) - QWindowSystemInterface::handleWindowActivated(window(), Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(window(), Qt::ActiveWindowFocusReason); } WId QOffscreenWindow::winId() const @@ -165,9 +165,9 @@ void QOffscreenWindow::setWindowState(Qt::WindowStates state) QOffscreenWindow *QOffscreenWindow::windowForWinId(WId id) { - return m_windowForWinIdHash.value(id, 0); + return m_windowForWinIdHash.value(id, nullptr); } -QHash<WId, QOffscreenWindow *> QOffscreenWindow::m_windowForWinIdHash; +Q_CONSTINIT QHash<WId, QOffscreenWindow *> QOffscreenWindow::m_windowForWinIdHash; QT_END_NAMESPACE diff --git a/src/plugins/platforms/offscreen/qoffscreenwindow.h b/src/plugins/platforms/offscreen/qoffscreenwindow.h index f1a437c64c..d525f2c657 100644 --- a/src/plugins/platforms/offscreen/qoffscreenwindow.h +++ b/src/plugins/platforms/offscreen/qoffscreenwindow.h @@ -41,7 +41,7 @@ private: bool m_frameMarginsRequested; WId m_winId; - static QHash<WId, QOffscreenWindow *> m_windowForWinIdHash; + Q_CONSTINIT static QHash<WId, QOffscreenWindow *> m_windowForWinIdHash; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/CMakeLists.txt b/src/plugins/platforms/qnx/CMakeLists.txt index 3e952e6ddf..0f9deaa00b 100644 --- a/src/plugins/platforms/qnx/CMakeLists.txt +++ b/src/plugins/platforms/qnx/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from qnx.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QQnxIntegrationPlugin Plugin: @@ -7,7 +8,7 @@ qt_internal_add_plugin(QQnxIntegrationPlugin OUTPUT_NAME qqnx PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES qnx # special case + DEFAULT_IF "qnx" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp main.h qqnxabstractcover.h @@ -30,6 +31,12 @@ qt_internal_add_plugin(QQnxIntegrationPlugin qqnxscreeneventthread.cpp qqnxscreeneventthread.h qqnxservices.cpp qqnxservices.h qqnxwindow.cpp qqnxwindow.h + NO_PCH_SOURCES + qqnxclipboard.cpp # undef QT_NO_FOREACH + qqnxintegration.cpp # undef QT_NO_FOREACH + qqnxscreen.cpp # undef QT_NO_FOREACH + qqnxscreeneventhandler.cpp # undef QT_NO_FOREACH + qqnxwindow.cpp # undef QT_NO_FOREACH LIBRARIES Qt::Core Qt::CorePrivate @@ -38,9 +45,6 @@ qt_internal_add_plugin(QQnxIntegrationPlugin screen ) -#### Keys ignored in scope 1:.:.:qnx.pro:<TRUE>: -# OTHER_FILES = "qnx.json" - ## Scopes: ##################################################################### @@ -87,6 +91,3 @@ qt_internal_extend_target(QQnxIntegrationPlugin CONDITION lgmon LIBRARIES lgmon ) - -#### Keys ignored in scope 8:.:.:qnx.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/qnx/qqnxabstractnavigator.cpp b/src/plugins/platforms/qnx/qqnxabstractnavigator.cpp index 05389d3ea6..9b1107b7ec 100644 --- a/src/plugins/platforms/qnx/qqnxabstractnavigator.cpp +++ b/src/plugins/platforms/qnx/qqnxabstractnavigator.cpp @@ -6,14 +6,10 @@ #include <QDebug> #include <QUrl> -#if defined(QQNXNAVIGATOR_DEBUG) -#define qNavigatorDebug qDebug -#else -#define qNavigatorDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaQnxNavigator, "qt.qpa.qnx.navigator"); + QQnxAbstractNavigator::QQnxAbstractNavigator(QObject *parent) : QObject(parent) { @@ -32,7 +28,7 @@ bool QQnxAbstractNavigator::invokeUrl(const QUrl &url) // which is not recognized by the navigator anymore const bool result = requestInvokeUrl(url.toString().toUtf8()); - qNavigatorDebug() << "url=" << url << "result=" << result; + qCDebug(lcQpaQnxNavigator) << Q_FUNC_INFO << "url =" << url << "result =" << result; return result; } diff --git a/src/plugins/platforms/qnx/qqnxabstractnavigator.h b/src/plugins/platforms/qnx/qqnxabstractnavigator.h index b4e4dd9bf8..fc92592307 100644 --- a/src/plugins/platforms/qnx/qqnxabstractnavigator.h +++ b/src/plugins/platforms/qnx/qqnxabstractnavigator.h @@ -5,9 +5,12 @@ #define QQNXABSTRACTNAVIGATOR_H #include <QObject> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnxNavigator); + class QUrl; class QQnxAbstractNavigator : public QObject diff --git a/src/plugins/platforms/qnx/qqnxbuffer.cpp b/src/plugins/platforms/qnx/qqnxbuffer.cpp index 1ea9a8b0d3..4d3b5256b2 100644 --- a/src/plugins/platforms/qnx/qqnxbuffer.cpp +++ b/src/plugins/platforms/qnx/qqnxbuffer.cpp @@ -10,24 +10,20 @@ #include <errno.h> #include <sys/mman.h> -#if defined(QQNXBUFFER_DEBUG) -#define qBufferDebug qDebug -#else -#define qBufferDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaScreenBuffer, "qt.qpa.screen.buffer"); + QQnxBuffer::QQnxBuffer() : m_buffer(0) { - qBufferDebug("empty"); + qCDebug(lcQpaScreenBuffer) << Q_FUNC_INFO << "Empty"; } QQnxBuffer::QQnxBuffer(screen_buffer_t buffer) : m_buffer(buffer) { - qBufferDebug("normal"); + qCDebug(lcQpaScreenBuffer) << Q_FUNC_INFO << "Normal"; // Get size of buffer int size[2]; @@ -77,7 +73,7 @@ QQnxBuffer::QQnxBuffer(screen_buffer_t buffer) imageFormat = QImage::Format_ARGB32_Premultiplied; break; default: - qFatal("QQNX: unsupported buffer format, format=%d", screenFormat); + qFatal(lcQpaScreenBuffer, "QQNX: unsupported buffer format, format=%d", screenFormat); } // wrap buffer in an image @@ -88,27 +84,27 @@ QQnxBuffer::QQnxBuffer(const QQnxBuffer &other) : m_buffer(other.m_buffer), m_image(other.m_image) { - qBufferDebug("copy"); + qCDebug(lcQpaScreenBuffer) << Q_FUNC_INFO << "Copy"; } QQnxBuffer::~QQnxBuffer() { - qBufferDebug(); + qCDebug(lcQpaScreenBuffer) << Q_FUNC_INFO; } void QQnxBuffer::invalidateInCache() { - qBufferDebug(); + qCDebug(lcQpaScreenBuffer) << Q_FUNC_INFO; // Verify native buffer exists if (Q_UNLIKELY(!m_buffer)) - qFatal("QQNX: can't invalidate cache for null buffer"); + qFatal(lcQpaScreenBuffer, "QQNX: can't invalidate cache for null buffer"); // Evict buffer's data from cache errno = 0; int result = msync(m_image.bits(), m_image.height() * m_image.bytesPerLine(), MS_INVALIDATE | MS_CACHE_ONLY); if (Q_UNLIKELY(result != 0)) - qFatal("QQNX: failed to invalidate cache, errno=%d", errno); + qFatal(lcQpaScreenBuffer, "QQNX: failed to invalidate cache, errno=%d", errno); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/qqnxbuffer.h b/src/plugins/platforms/qnx/qqnxbuffer.h index b8f1443ad1..b456eb2a62 100644 --- a/src/plugins/platforms/qnx/qqnxbuffer.h +++ b/src/plugins/platforms/qnx/qqnxbuffer.h @@ -5,11 +5,14 @@ #define QQNXBUFFER_H #include <QtGui/QImage> +#include <QtCore/QLoggingCategory> #include <screen/screen.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreenBuffer) + class QQnxBuffer { public: diff --git a/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.cpp b/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.cpp index bf66d54bba..788cddea87 100644 --- a/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.cpp +++ b/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.cpp @@ -13,16 +13,12 @@ #include <QtCore/QSocketNotifier> #include <QtCore/private/qcore_unix_p.h> -#if defined(QQNXBUTTON_DEBUG) -#define qButtonDebug qDebug -#else -#define qButtonDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE -static const char *ppsPath = "/pps/system/buttons/status"; -static const int ppsBufferSize = 256; +Q_LOGGING_CATEGORY(lcQpaInputHwButton, "qt.qpa.input.hwbutton"); + +const char *QQnxButtonEventNotifier::ppsPath = "/pps/system/buttons/status"; +const size_t QQnxButtonEventNotifier::ppsBufferSize = 256; QQnxButtonEventNotifier::QQnxButtonEventNotifier(QObject *parent) : QObject(parent), @@ -47,7 +43,7 @@ QQnxButtonEventNotifier::~QQnxButtonEventNotifier() void QQnxButtonEventNotifier::start() { - qButtonDebug("starting hardware button event processing"); + qCDebug(lcQpaInputHwButton) << "Starting hardware button event processing"; if (m_fd != -1) return; @@ -64,7 +60,7 @@ void QQnxButtonEventNotifier::start() m_readNotifier = new QSocketNotifier(m_fd, QSocketNotifier::Read); QObject::connect(m_readNotifier, SIGNAL(activated(QSocketDescriptor)), this, SLOT(updateButtonStates())); - qButtonDebug("successfully connected to Navigator. fd = %d", m_fd); + qCDebug(lcQpaInputHwButton, "successfully connected to Navigator. fd = %d", m_fd); } void QQnxButtonEventNotifier::updateButtonStates() @@ -75,7 +71,8 @@ void QQnxButtonEventNotifier::updateButtonStates() // Attempt to read pps data errno = 0; int bytes = qt_safe_read(m_fd, buffer, ppsBufferSize - 1); - qButtonDebug() << "Read" << bytes << "bytes of data"; + qCDebug(lcQpaInputHwButton) << "Read" << bytes << "bytes of data"; + if (bytes == -1) { qWarning("QQNX: failed to read hardware buttons pps object, errno=%d", errno); return; @@ -88,7 +85,7 @@ void QQnxButtonEventNotifier::updateButtonStates() // Ensure data is null terminated buffer[bytes] = '\0'; - qButtonDebug("received PPS message:\n%s", buffer); + qCDebug(lcQpaInputHwButton, "Received PPS message:\n%s", buffer); // Process received message QByteArray ppsData = QByteArray::fromRawData(buffer, bytes); @@ -104,7 +101,8 @@ void QQnxButtonEventNotifier::updateButtonStates() // If state has changed, update our state and inject a keypress event if (m_state[buttonId] != newState) { - qButtonDebug() << "Hardware button event: button =" << key << "state =" << fields.value(key); + qCDebug(lcQpaInputHwButton) << "Hardware button event: button =" << key << "state =" << fields.value(key); + m_state[buttonId] = newState; // Is it a key press or key release event? @@ -129,7 +127,7 @@ void QQnxButtonEventNotifier::updateButtonStates() break; default: - qButtonDebug("Unknown hardware button"); + qCDebug(lcQpaInputHwButton) << "Unknown hardware button"; continue; } @@ -170,7 +168,7 @@ bool QQnxButtonEventNotifier::parsePPS(const QByteArray &ppsData, QHash<QByteArr // tokenize current attribute const QByteArray &attr = lines.at(i); - qButtonDebug() << "attr=" << attr; + qCDebug(lcQpaInputHwButton) << Q_FUNC_INFO << "attr =" << attr; int doubleColon = attr.indexOf(QByteArrayLiteral("::")); if (doubleColon == -1) { diff --git a/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.h b/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.h index 987055f903..476119ae27 100644 --- a/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.h +++ b/src/plugins/platforms/qnx/qqnxbuttoneventnotifier.h @@ -5,9 +5,12 @@ #define QQNXBUTTONSEVENTNOTIFIER_H #include <QObject> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaInputHwButton); + class QSocketNotifier; class QQnxButtonEventNotifier : public QObject @@ -45,6 +48,9 @@ private: QSocketNotifier *m_readNotifier; ButtonState m_state[ButtonCount]; QList<QByteArray> m_buttonKeys; + + static const char *ppsPath; + static const size_t ppsBufferSize; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/qqnxclipboard.cpp b/src/plugins/platforms/qnx/qqnxclipboard.cpp index 80f9d73a3b..3f27ec8069 100644 --- a/src/plugins/platforms/qnx/qqnxclipboard.cpp +++ b/src/plugins/platforms/qnx/qqnxclipboard.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2011 - 2012 Research In Motion // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #if !defined(QT_NO_CLIPBOARD) #include "qqnxclipboard.h" @@ -15,14 +17,10 @@ #include <clipboard/clipboard.h> #include <errno.h> -#if defined(QQNXCLIPBOARD_DEBUG) -#define qClipboardDebug qDebug -#else -#define qClipboardDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaClipboard, "qt.qpa.clipboard"); + // null terminated array static const char *typeList[] = {"text/html", "text/plain", "image/png", "image/jpeg", "application/x-color", 0}; @@ -64,13 +62,13 @@ public: void addFormatToCheck(const QString &format) { m_formatsToCheck << format; - qClipboardDebug() << "formats=" << m_formatsToCheck; + qCDebug(lcQpaClipboard) << "formats=" << m_formatsToCheck; } bool hasFormat(const QString &mimetype) const override { const bool result = is_clipboard_format_present(mimetype.toUtf8().constData()) == 0; - qClipboardDebug() << "mimetype=" << mimetype << "result=" << result; + qCDebug(lcQpaClipboard) << "mimetype=" << mimetype << "result=" << result; return result; } @@ -83,7 +81,7 @@ public: result << format; } - qClipboardDebug() << "result=" << result; + qCDebug(lcQpaClipboard) << "result=" << result; return result; } @@ -107,7 +105,7 @@ public: protected: QVariant retrieveData(const QString &mimetype, QMetaType preferredType) const override { - qClipboardDebug() << "mimetype=" << mimetype << "preferredType=" << preferredType.name(); + qCDebug(lcQpaClipboard) << "mimetype=" << mimetype << "preferredType=" << preferredType.name(); if (is_clipboard_format_present(mimetype.toUtf8().constData()) != 0) return QMimeData::retrieveData(mimetype, preferredType); @@ -119,7 +117,7 @@ private Q_SLOTS: void releaseOwnership() { if (m_userMimeData) { - qClipboardDebug() << "user data formats=" << m_userMimeData->formats() << "system formats=" << formats(); + qCDebug(lcQpaClipboard) << "user data formats=" << m_userMimeData->formats() << "system formats=" << formats(); delete m_userMimeData; m_userMimeData = 0; m_clipboard->emitChanged(QClipboard::Clipboard); @@ -165,7 +163,7 @@ void QQnxClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) } const QStringList formats = data->formats(); - qClipboardDebug() << "formats=" << formats; + qCDebug(lcQpaClipboard) << "formats=" << formats; Q_FOREACH (const QString &format, formats) { const QByteArray buf = data->data(format); @@ -174,7 +172,7 @@ void QQnxClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) continue; int ret = set_clipboard_data(format.toUtf8().data(), buf.size(), buf.data()); - qClipboardDebug() << "set " << format << "to clipboard, size=" << buf.size() << ";ret=" << ret; + qCDebug(lcQpaClipboard) << "set " << format << "to clipboard, size=" << buf.size() << ";ret=" << ret; if (ret) m_mimeData->addFormatToCheck(format); } diff --git a/src/plugins/platforms/qnx/qqnxclipboard.h b/src/plugins/platforms/qnx/qqnxclipboard.h index adc70b158a..66f862f0dc 100644 --- a/src/plugins/platforms/qnx/qqnxclipboard.h +++ b/src/plugins/platforms/qnx/qqnxclipboard.h @@ -11,6 +11,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaClipboard); + class QQnxClipboard : public QPlatformClipboard { public: diff --git a/src/plugins/platforms/qnx/qqnxcursor.cpp b/src/plugins/platforms/qnx/qqnxcursor.cpp index 03c4e16401..7c55dd79b3 100644 --- a/src/plugins/platforms/qnx/qqnxcursor.cpp +++ b/src/plugins/platforms/qnx/qqnxcursor.cpp @@ -5,14 +5,10 @@ #include <QtCore/QDebug> -#if defined(QQNXCURSOR_DEBUG) -#define qCursorDebug qDebug -#else -#define qCursorDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaQnx, "qt.qpa.qnx"); + QQnxCursor::QQnxCursor() { } @@ -27,13 +23,13 @@ void QQnxCursor::changeCursor(QCursor *windowCursor, QWindow *window) void QQnxCursor::setPos(const QPoint &pos) { - qCursorDebug() << "QQnxCursor::setPos -" << pos; + qCDebug(lcQpaQnx) << "QQnxCursor::setPos -" << pos; m_pos = pos; } QPoint QQnxCursor::pos() const { - qCursorDebug() << "QQnxCursor::pos -" << m_pos; + qCDebug(lcQpaQnx) << "QQnxCursor::pos -" << m_pos; return m_pos; } diff --git a/src/plugins/platforms/qnx/qqnxcursor.h b/src/plugins/platforms/qnx/qqnxcursor.h index 6592e0a5ee..707bdbaaa2 100644 --- a/src/plugins/platforms/qnx/qqnxcursor.h +++ b/src/plugins/platforms/qnx/qqnxcursor.h @@ -5,9 +5,12 @@ #define QQNXCURSOR_H #include <qpa/qplatformcursor.h> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +// Q_DECLARE_LOGGING_CATEGORY(lcQpaQnx); + class QQnxCursor : public QPlatformCursor { public: diff --git a/src/plugins/platforms/qnx/qqnxeglwindow.cpp b/src/plugins/platforms/qnx/qqnxeglwindow.cpp index ed53308216..854ad46c3d 100644 --- a/src/plugins/platforms/qnx/qqnxeglwindow.cpp +++ b/src/plugins/platforms/qnx/qqnxeglwindow.cpp @@ -10,14 +10,10 @@ #include <errno.h> -#if defined(QQNXEGLWINDOW_DEBUG) -#define qEglWindowDebug qDebug -#else -#define qEglWindowDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaWindowEgl, "qt.qpa.window.egl"); + QQnxEglWindow::QQnxEglWindow(QWindow *window, screen_context_t context, bool needRootWindow) : QQnxWindow(window, context, needRootWindow), m_newSurfaceRequested(true), @@ -96,8 +92,8 @@ void QQnxEglWindow::createEGLSurface(QQnxGLContext *context) EGL_NONE }; - qEglWindowDebug() << "Creating EGL surface from" << this << context - << window()->surfaceType() << window()->type(); + qCDebug(lcQpaWindowEgl) << "Creating EGL surface from" << this << context + << window()->surfaceType() << window()->type(); // Create EGL surface EGLSurface eglSurface = eglCreateWindowSurface( diff --git a/src/plugins/platforms/qnx/qqnxeglwindow.h b/src/plugins/platforms/qnx/qqnxeglwindow.h index 909a901d3c..548d9be1ee 100644 --- a/src/plugins/platforms/qnx/qqnxeglwindow.h +++ b/src/plugins/platforms/qnx/qqnxeglwindow.h @@ -6,9 +6,12 @@ #include "qqnxwindow.h" #include <QtCore/QMutex> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindowEgl); + class QQnxGLContext; class QQnxEglWindow : public QQnxWindow diff --git a/src/plugins/platforms/qnx/qqnxglcontext.cpp b/src/plugins/platforms/qnx/qqnxglcontext.cpp index 7b5b11b2e4..3d8fecf88b 100644 --- a/src/plugins/platforms/qnx/qqnxglcontext.cpp +++ b/src/plugins/platforms/qnx/qqnxglcontext.cpp @@ -14,14 +14,10 @@ #include <dlfcn.h> -#if defined(QQNXGLCONTEXT_DEBUG) -#define qGLContextDebug qDebug -#else -#define qGLContextDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaGLContext, "qt.qpa.glcontext"); + static QEGLPlatformContext::Flags makeFlags() { QEGLPlatformContext::Flags result = {}; @@ -51,13 +47,13 @@ EGLSurface QQnxGLContext::eglSurfaceForPlatformSurface(QPlatformSurface *surface bool QQnxGLContext::makeCurrent(QPlatformSurface *surface) { - qGLContextDebug(); + qCDebug(lcQpaGLContext) << Q_FUNC_INFO; return QEGLPlatformContext::makeCurrent(surface); } void QQnxGLContext::swapBuffers(QPlatformSurface *surface) { - qGLContextDebug(); + qCDebug(lcQpaGLContext) << Q_FUNC_INFO; QEGLPlatformContext::swapBuffers(surface); diff --git a/src/plugins/platforms/qnx/qqnxglobal.cpp b/src/plugins/platforms/qnx/qqnxglobal.cpp index 85642b44b3..9cfc328e8f 100644 --- a/src/plugins/platforms/qnx/qqnxglobal.cpp +++ b/src/plugins/platforms/qnx/qqnxglobal.cpp @@ -15,6 +15,8 @@ void qScreenCheckError(int rc, const char *funcInfo, const char *message, bool c } if (Q_UNLIKELY(rc)) { + qCDebug(lcQpaQnx, "%s - Screen: %s - Error: %s (%i)", funcInfo, message, strerror(errno), errno); + if (Q_UNLIKELY(critical)) qCritical("%s - Screen: %s - Error: %s (%i)", funcInfo, message, strerror(errno), errno); else diff --git a/src/plugins/platforms/qnx/qqnxinputcontext_imf.cpp b/src/plugins/platforms/qnx/qqnxinputcontext_imf.cpp index a1063444d1..cd41ebb8c2 100644 --- a/src/plugins/platforms/qnx/qqnxinputcontext_imf.cpp +++ b/src/plugins/platforms/qnx/qqnxinputcontext_imf.cpp @@ -25,17 +25,7 @@ #include <process.h> #include <sys/keycodes.h> -#if defined(QQNXINPUTCONTEXT_IMF_EVENT_DEBUG) -#define qInputContextIMFRequestDebug qDebug -#else -#define qInputContextIMFRequestDebug QT_NO_QDEBUG_MACRO -#endif - -#if defined(QQNXINPUTCONTEXT_DEBUG) -#define qInputContextDebug qDebug -#else -#define qInputContextDebug QT_NO_QDEBUG_MACRO -#endif +Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); static QQnxInputContext *sInputContextInstance; static QColor sSelectedColor(0,0xb8,0,85); @@ -161,7 +151,7 @@ static int32_t ic_begin_batch_edit(input_session_t *ic) // See comment at beginning of namespace declaration for general information static int32_t ic_commit_text(input_session_t *ic, spannable_string_t *text, int32_t new_cursor_position) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfCommitText); event.ct.text = text; @@ -176,7 +166,7 @@ static int32_t ic_commit_text(input_session_t *ic, spannable_string_t *text, int // See comment at beginning of namespace declaration for general information static int32_t ic_delete_surrounding_text(input_session_t *ic, int32_t left_length, int32_t right_length) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfDeleteSurroundingText); event.dst.left_length = left_length; @@ -200,7 +190,7 @@ static int32_t ic_end_batch_edit(input_session_t *ic) // See comment at beginning of namespace declaration for general information static int32_t ic_finish_composing_text(input_session_t *ic) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfFinishComposingText); event.fct.result = -1; @@ -213,7 +203,7 @@ static int32_t ic_finish_composing_text(input_session_t *ic) // See comment at beginning of namespace declaration for general information static int32_t ic_get_cursor_position(input_session_t *ic) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfGetCursorPosition); event.gcp.result = -1; @@ -226,7 +216,7 @@ static int32_t ic_get_cursor_position(input_session_t *ic) // See comment at beginning of namespace declaration for general information static spannable_string_t *ic_get_text_after_cursor(input_session_t *ic, int32_t n, int32_t flags) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfGetTextAfterCursor); event.gtac.n = n; @@ -241,7 +231,7 @@ static spannable_string_t *ic_get_text_after_cursor(input_session_t *ic, int32_t // See comment at beginning of namespace declaration for general information static spannable_string_t *ic_get_text_before_cursor(input_session_t *ic, int32_t n, int32_t flags) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfGetTextBeforeCursor); event.gtac.n = n; @@ -256,7 +246,7 @@ static spannable_string_t *ic_get_text_before_cursor(input_session_t *ic, int32_ // See comment at beginning of namespace declaration for general information static int32_t ic_send_event(input_session_t *ic, event_t *event) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest imfEvent(ic, ImfSendEvent); imfEvent.sae.event = event; @@ -270,7 +260,7 @@ static int32_t ic_send_event(input_session_t *ic, event_t *event) // See comment at beginning of namespace declaration for general information static int32_t ic_send_async_event(input_session_t *ic, event_t *event) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; // There's no difference from our point of view between ic_send_event & ic_send_async_event QQnxImfRequest imfEvent(ic, ImfSendEvent); @@ -285,7 +275,7 @@ static int32_t ic_send_async_event(input_session_t *ic, event_t *event) // See comment at beginning of namespace declaration for general information static int32_t ic_set_composing_region(input_session_t *ic, int32_t start, int32_t end) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfSetComposingRegion); event.scr.start = start; @@ -301,7 +291,7 @@ static int32_t ic_set_composing_region(input_session_t *ic, int32_t start, int32 // See comment at beginning of namespace declaration for general information static int32_t ic_set_composing_text(input_session_t *ic, spannable_string_t *text, int32_t new_cursor_position) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfSetComposingText); event.sct.text = text; @@ -316,7 +306,7 @@ static int32_t ic_set_composing_text(input_session_t *ic, spannable_string_t *te // See comment at beginning of namespace declaration for general information static int32_t ic_is_text_selected(input_session_t* ic, int32_t* pIsSelected) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfIsTextSelected); event.its.pIsSelected = pIsSelected; @@ -330,7 +320,7 @@ static int32_t ic_is_text_selected(input_session_t* ic, int32_t* pIsSelected) // See comment at beginning of namespace declaration for general information static int32_t ic_is_all_text_selected(input_session_t* ic, int32_t* pIsSelected) { - qInputContextIMFRequestDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QQnxImfRequest event(ic, ImfIsAllTextSelected); event.its.pIsSelected = pIsSelected; @@ -466,7 +456,7 @@ initEvent(event_t *pEvent, const input_session_t *pSession, EventType eventType, static spannable_string_t *toSpannableString(const QString &text) { - qInputContextDebug() << text; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "Text:" << text; spannable_string_t *pString = static_cast<spannable_string_t *>(malloc(sizeof(spannable_string_t))); pString->str = static_cast<wchar_t *>(malloc(sizeof(wchar_t) * text.length() + 1)); @@ -540,7 +530,7 @@ QQnxInputContext::QQnxInputContext(QQnxIntegration *integration, QQnxAbstractVir m_integration(integration), m_virtualKeyboard(keyboard) { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; if (!imfAvailable()) return; @@ -563,7 +553,7 @@ QQnxInputContext::QQnxInputContext(QQnxIntegration *integration, QQnxAbstractVir QQnxInputContext::~QQnxInputContext() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; Q_ASSERT(sInputContextInstance == this); sInputContextInstance = nullptr; @@ -638,7 +628,7 @@ void QQnxInputContext::processImfEvent(QQnxImfRequest *imfEvent) bool QQnxInputContext::filterEvent( const QEvent *event ) { - qInputContextDebug() << event; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << event; switch (event->type()) { case QEvent::CloseSoftwareInputPanel: @@ -661,19 +651,19 @@ QRectF QQnxInputContext::keyboardRect() const void QQnxInputContext::reset() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; endComposition(); } void QQnxInputContext::commit() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; endComposition(); } void QQnxInputContext::update(Qt::InputMethodQueries queries) { - qInputContextDebug() << queries; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "Queries:" << queries; if (queries & Qt::ImCursorPosition) { int lastCaret = m_caretPosition; @@ -685,7 +675,8 @@ void QQnxInputContext::update(Qt::InputMethodQueries queries) initEvent(&caretEvent.event, sInputSession, EVENT_CARET, CARET_POS_CHANGED, sizeof(caretEvent)); caretEvent.old_pos = lastCaret; caretEvent.new_pos = m_caretPosition; - qInputContextDebug("ictrl_dispatch_event caret changed %d %d", lastCaret, m_caretPosition); + qCDebug(lcQpaInputMethods, "ictrl_dispatch_event caret changed %d %d", lastCaret, m_caretPosition); + p_ictrl_dispatch_event(&caretEvent.event); } } @@ -693,7 +684,7 @@ void QQnxInputContext::update(Qt::InputMethodQueries queries) void QQnxInputContext::closeSession() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; if (!imfAvailable()) return; @@ -715,7 +706,7 @@ bool QQnxInputContext::openSession() closeSession(); sInputSession = p_ictrl_open_session(&ic_funcs); - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; return sInputSession != 0; } @@ -739,7 +730,7 @@ bool QQnxInputContext::hasSelectedText() bool QQnxInputContext::dispatchRequestSoftwareInputPanel() { - qInputContextDebug() << "requesting keyboard" << m_inputPanelVisible; + qCDebug(lcQpaInputMethods) << "Requesting keyboard" << m_inputPanelVisible; m_virtualKeyboard.showKeyboard(); return true; @@ -747,7 +738,7 @@ bool QQnxInputContext::dispatchRequestSoftwareInputPanel() bool QQnxInputContext::dispatchCloseSoftwareInputPanel() { - qInputContextDebug() << "hiding keyboard" << m_inputPanelVisible; + qCDebug(lcQpaInputMethods) << "Hiding keyboard" << m_inputPanelVisible; m_virtualKeyboard.hideKeyboard(); return true; @@ -793,7 +784,7 @@ bool QQnxInputContext::dispatchFocusGainEvent(int inputHints) focusEvent.style |= IMF_EMAIL_TYPE; } - qInputContextDebug() << "ictrl_dispatch_event focus gain style:" << focusEvent.style; + qCDebug(lcQpaInputMethods) << "ictrl_dispatch_event focus gain style:" << focusEvent.style; p_ictrl_dispatch_event((event_t *)&focusEvent); @@ -803,7 +794,7 @@ bool QQnxInputContext::dispatchFocusGainEvent(int inputHints) void QQnxInputContext::dispatchFocusLossEvent() { if (hasSession()) { - qInputContextDebug("ictrl_dispatch_event focus lost"); + qCDebug(lcQpaInputMethods) << "ictrl_dispatch_event focus lost"; focus_event_t focusEvent; initEvent(&focusEvent.event, sInputSession, EVENT_FOCUS, FOCUS_LOST, sizeof(focusEvent)); @@ -878,7 +869,7 @@ bool QQnxInputContext::handleKeyboardEvent(int flags, int sym, int mod, int scan navigation_event_t navEvent; initEvent(&navEvent.event, sInputSession, EVENT_NAVIGATION, key, sizeof(navEvent)); navEvent.magnitude = 1; - qInputContextDebug("ictrl_dispatch_even navigation %d", key); + qCDebug(lcQpaInputMethods, "ictrl_dispatch_even navigation %d", key); p_ictrl_dispatch_event(&navEvent.event); } } else { @@ -891,7 +882,8 @@ bool QQnxInputContext::handleKeyboardEvent(int flags, int sym, int mod, int scan keyEvent.sequence_id = sequenceId; p_ictrl_dispatch_event(&keyEvent.event); - qInputContextDebug("ictrl_dispatch_even key %d", key); + qCDebug(lcQpaInputMethods, "ictrl_dispatch_even key %d", key); + } return true; @@ -907,7 +899,7 @@ void QQnxInputContext::updateCursorPosition() QCoreApplication::sendEvent(input, &query); m_caretPosition = query.value(Qt::ImCursorPosition).toInt(); - qInputContextDebug("%d", m_caretPosition); + qCDebug(lcQpaInputMethods, "ictrl_dispatch_even key %d", key); } void QQnxInputContext::endComposition() @@ -920,7 +912,7 @@ void QQnxInputContext::endComposition() if (hasSession()) { action_event_t actionEvent; initEvent(&actionEvent.event, sInputSession, EVENT_ACTION, ACTION_END_COMPOSITION, sizeof(actionEvent)); - qInputContextDebug("ictrl_dispatch_even end composition"); + qCDebug(lcQpaInputMethods, "ictrl_dispatch_even end composition"); p_ictrl_dispatch_event(&actionEvent.event); } } @@ -937,7 +929,7 @@ void QQnxInputContext::updateComposition(spannable_string_t *text, int32_t new_c m_composingText = QString::fromWCharArray(text->str, text->length); m_isComposing = true; - qInputContextDebug() << m_composingText << new_cursor_position; + qCDebug(lcQpaInputMethods) << "Text =" << m_composingText << "Cursor position =" << new_cursor_position; QList<QInputMethodEvent::Attribute> attributes; attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, @@ -967,7 +959,7 @@ void QQnxInputContext::updateComposition(spannable_string_t *text, int32_t new_c format.setFontUnderline(true); if (highlightColor.isValid()) format.setBackground(QBrush(highlightColor)); - qInputContextDebug() << " attrib: " << underline << highlightColor << text->spans[i].start << text->spans[i].end; + qCDebug(lcQpaInputMethods) << "attrib: " << underline << highlightColor << text->spans[i].start << text->spans[i].end; attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, text->spans[i].start, text->spans[i].end - text->spans[i].start + 1, QVariant(format))); @@ -986,7 +978,7 @@ void QQnxInputContext::finishComposingText() QObject *input = qGuiApp->focusObject(); if (input) { - qInputContextDebug() << m_composingText; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "Text =" << m_composingText; QInputMethodEvent event; event.setCommitString(m_composingText); @@ -1051,13 +1043,13 @@ int32_t QQnxInputContext::processEvent(event_t *event) int32_t result = -1; switch (event->event_type) { case EVENT_SPELL_CHECK: { - qInputContextDebug("EVENT_SPELL_CHECK"); + qCDebug(lcQpaInputMethods) << "EVENT_SPELL_CHECK"; result = handleSpellCheck(reinterpret_cast<spell_check_event_t *>(event)); break; } case EVENT_NAVIGATION: { - qInputContextDebug("EVENT_NAVIGATION"); + qCDebug(lcQpaInputMethods) << "EVENT_NAVIGATION"; int key = event->event_id == NAVIGATE_UP ? KEYCODE_UP : event->event_id == NAVIGATE_DOWN ? KEYCODE_DOWN : @@ -1080,7 +1072,7 @@ int32_t QQnxInputContext::processEvent(event_t *event) int flags = KEY_SYM_VALID | KEY_CAP_VALID; if (event->event_id == IMF_KEY_DOWN) flags |= KEY_DOWN; - qInputContextDebug("EVENT_KEY %d %d", flags, keySym); + qCDebug(lcQpaInputMethods, "EVENT_KEY %d %d", flags, keySym); QQnxScreenEventHandler::injectKeyboardEvent(flags, keySym, modifiers, 0, keyCap); result = 0; break; @@ -1120,7 +1112,7 @@ int32_t QQnxInputContext::onCommitText(spannable_string_t *text, int32_t new_cur int32_t QQnxInputContext::onDeleteSurroundingText(int32_t left_length, int32_t right_length) { - qInputContextDebug("L: %d R: %d", int(left_length), int(right_length)); + qCDebug(lcQpaInputMethods, "L: %d R: %d", int(left_length), int(right_length)); QObject *input = qGuiApp->focusObject(); if (!input) @@ -1151,7 +1143,7 @@ int32_t QQnxInputContext::onFinishComposingText() int32_t QQnxInputContext::onGetCursorPosition() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QObject *input = qGuiApp->focusObject(); if (!input) @@ -1165,7 +1157,7 @@ int32_t QQnxInputContext::onGetCursorPosition() spannable_string_t *QQnxInputContext::onGetTextAfterCursor(int32_t n, int32_t flags) { Q_UNUSED(flags); - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QObject *input = qGuiApp->focusObject(); if (!input) @@ -1182,7 +1174,7 @@ spannable_string_t *QQnxInputContext::onGetTextAfterCursor(int32_t n, int32_t fl spannable_string_t *QQnxInputContext::onGetTextBeforeCursor(int32_t n, int32_t flags) { Q_UNUSED(flags); - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; QObject *input = qGuiApp->focusObject(); if (!input) @@ -1201,7 +1193,7 @@ spannable_string_t *QQnxInputContext::onGetTextBeforeCursor(int32_t n, int32_t f int32_t QQnxInputContext::onSendEvent(event_t *event) { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; return processEvent(event); } @@ -1217,7 +1209,7 @@ int32_t QQnxInputContext::onSetComposingRegion(int32_t start, int32_t end) QString text = query.value(Qt::ImSurroundingText).toString(); m_caretPosition = query.value(Qt::ImCursorPosition).toInt(); - qInputContextDebug() << text; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "Text =" << text; m_isUpdatingText = true; @@ -1260,7 +1252,7 @@ int32_t QQnxInputContext::onIsTextSelected(int32_t* pIsSelected) { *pIsSelected = hasSelectedText(); - qInputContextDebug() << *pIsSelected; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << *pIsSelected; return 0; } @@ -1276,20 +1268,20 @@ int32_t QQnxInputContext::onIsAllTextSelected(int32_t* pIsSelected) *pIsSelected = query.value(Qt::ImSurroundingText).toString().length() == query.value(Qt::ImCurrentSelection).toString().length(); - qInputContextDebug() << *pIsSelected; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << *pIsSelected; return 0; } void QQnxInputContext::showInputPanel() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; dispatchRequestSoftwareInputPanel(); } void QQnxInputContext::hideInputPanel() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; dispatchCloseSoftwareInputPanel(); } @@ -1305,7 +1297,7 @@ QLocale QQnxInputContext::locale() const void QQnxInputContext::keyboardVisibilityChanged(bool visible) { - qInputContextDebug() << "visible=" << visible; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "visible=" << visible; if (m_inputPanelVisible != visible) { m_inputPanelVisible = visible; emitInputPanelVisibleChanged(); @@ -1314,7 +1306,7 @@ void QQnxInputContext::keyboardVisibilityChanged(bool visible) void QQnxInputContext::keyboardLocaleChanged(const QLocale &locale) { - qInputContextDebug() << "locale=" << locale; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "locale=" << locale; if (m_inputPanelLocale != locale) { m_inputPanelLocale = locale; emitLocaleChanged(); @@ -1323,7 +1315,7 @@ void QQnxInputContext::keyboardLocaleChanged(const QLocale &locale) void QQnxInputContext::setHighlightColor(int index, const QColor &color) { - qInputContextDebug() << "setHighlightColor" << index << color << qGuiApp->focusObject(); + qCDebug(lcQpaInputMethods) << "setHighlightColor" << index << color << qGuiApp->focusObject(); if (!sInputContextInstance) return; @@ -1342,7 +1334,7 @@ void QQnxInputContext::setHighlightColor(int index, const QColor &color) void QQnxInputContext::setFocusObject(QObject *object) { - qInputContextDebug() << "input item=" << object; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "input item=" << object; // Ensure the colors are reset if we've a change in focus object setHighlightColor(-1, QColor()); @@ -1372,7 +1364,7 @@ void QQnxInputContext::setFocusObject(QObject *object) bool QQnxInputContext::checkSpelling(const QString &text, void *context, void (*spellCheckDone)(void *context, const QString &text, const QList<int> &indices)) { - qInputContextDebug() << "text" << text; + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO << "Text =" << text; if (!imfAvailable()) return false; diff --git a/src/plugins/platforms/qnx/qqnxinputcontext_noimf.cpp b/src/plugins/platforms/qnx/qqnxinputcontext_noimf.cpp index 2a4e5b509b..7789b2830a 100644 --- a/src/plugins/platforms/qnx/qqnxinputcontext_noimf.cpp +++ b/src/plugins/platforms/qnx/qqnxinputcontext_noimf.cpp @@ -10,14 +10,10 @@ #include <QtGui/QGuiApplication> #include <QtGui/QInputMethodEvent> -#if defined(QQNXINPUTCONTEXT_DEBUG) -#define qInputContextDebug qDebug -#else -#define qInputContextDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); + QQnxInputContext::QQnxInputContext(QQnxIntegration *integration, QQnxAbstractVirtualKeyboard &keyboard) : QPlatformInputContext(), m_inputPanelVisible(false), @@ -58,13 +54,13 @@ bool QQnxInputContext::filterEvent( const QEvent *event ) if (event->type() == QEvent::CloseSoftwareInputPanel) { m_virtualKeyboard.hideKeyboard(); - qInputContextDebug("hiding virtual keyboard"); + qCDebug(lcQpaInputMethods) << "hiding virtual keyboard"; return false; } if (event->type() == QEvent::RequestSoftwareInputPanel) { m_virtualKeyboard.showKeyboard(); - qInputContextDebug("requesting virtual keyboard"); + qCDebug(lcQpaInputMethods) << "requesting virtual keyboard"; return false; } @@ -91,13 +87,13 @@ bool QQnxInputContext::handleKeyboardEvent(int flags, int sym, int mod, int scan void QQnxInputContext::showInputPanel() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; m_virtualKeyboard.showKeyboard(); } void QQnxInputContext::hideInputPanel() { - qInputContextDebug(); + qCDebug(lcQpaInputMethods) << Q_FUNC_INFO; m_virtualKeyboard.hideKeyboard(); } @@ -118,7 +114,7 @@ void QQnxInputContext::keyboardHeightChanged() void QQnxInputContext::keyboardVisibilityChanged(bool visible) { - qInputContextDebug() << "visible=" << visible; + qCDebug(lcQpaInputMethods) << "visible=" << visible; if (m_inputPanelVisible != visible) { m_inputPanelVisible = visible; emitInputPanelVisibleChanged(); @@ -127,7 +123,7 @@ void QQnxInputContext::keyboardVisibilityChanged(bool visible) void QQnxInputContext::keyboardLocaleChanged(const QLocale &locale) { - qInputContextDebug() << "locale=" << locale; + qCDebug(lcQpaInputMethods) << "locale=" << locale; if (m_inputPanelLocale != locale) { m_inputPanelLocale = locale; emitLocaleChanged(); @@ -136,7 +132,7 @@ void QQnxInputContext::keyboardLocaleChanged(const QLocale &locale) void QQnxInputContext::setFocusObject(QObject *object) { - qInputContextDebug() << "input item=" << object; + qCDebug(lcQpaInputMethods) << "input item=" << object;; if (!inputMethodAccepted()) { if (m_inputPanelVisible) diff --git a/src/plugins/platforms/qnx/qqnxintegration.cpp b/src/plugins/platforms/qnx/qqnxintegration.cpp index ed9ad6ccbc..b308c956f2 100644 --- a/src/plugins/platforms/qnx/qqnxintegration.cpp +++ b/src/plugins/platforms/qnx/qqnxintegration.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2013 BlackBerry Limited. All rights reserved. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qqnxglobal.h" #include "qqnxintegration.h" @@ -47,6 +49,7 @@ #include <qpa/qwindowsysteminterface.h> #include <QtGui/private/qguiapplication_p.h> +#include <QtGui/private/qrhibackingstore_p.h> #if !defined(QT_NO_OPENGL) #include "qqnxglcontext.h" @@ -62,14 +65,10 @@ #include <QtCore/QFile> #include <errno.h> -#if defined(QQNXINTEGRATION_DEBUG) -#define qIntegrationDebug qDebug -#else -#define qIntegrationDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +// Q_LOGGING_CATEGORY(lcQpaQnx, "qt.qpa.qnx"); + using namespace Qt::StringLiterals; QQnxIntegration *QQnxIntegration::ms_instance; @@ -98,7 +97,7 @@ static inline QQnxIntegration::Options parseOptions(const QStringList ¶mList static inline int getContextCapabilities(const QStringList ¶mList) { - QString contextCapabilitiesPrefix = QStringLiteral("screen-context-capabilities="); + constexpr auto contextCapabilitiesPrefix = "screen-context-capabilities="_L1; int contextCapabilities = SCREEN_APPLICATION_CONTEXT; for (const QString ¶m : paramList) { if (param.startsWith(contextCapabilitiesPrefix)) { @@ -142,7 +141,7 @@ QQnxIntegration::QQnxIntegration(const QStringList ¶mList) { ms_instance = this; m_options = parseOptions(paramList); - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Open connection to QNX composition manager if (screen_create_context(&m_screenContext, getContextCapabilities(paramList))) { @@ -219,7 +218,7 @@ QQnxIntegration::QQnxIntegration(const QStringList ¶mList) QQnxIntegration::~QQnxIntegration() { - qIntegrationDebug("platform plugin shutdown begin"); + qCDebug(lcQpaQnx) << "Platform plugin shutdown begin"; delete m_nativeInterface; #if QT_CONFIG(draganddrop) @@ -276,12 +275,12 @@ QQnxIntegration::~QQnxIntegration() ms_instance = nullptr; - qIntegrationDebug("platform plugin shutdown end"); + qCDebug(lcQpaQnx) << "Platform plugin shutdown end"; } bool QQnxIntegration::hasCapability(QPlatformIntegration::Capability cap) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; switch (cap) { case MultipleWindows: case ForeignWindows: @@ -312,7 +311,7 @@ QPlatformWindow *QQnxIntegration::createForeignWindow(QWindow *window, WId nativ QPlatformWindow *QQnxIntegration::createPlatformWindow(QWindow *window) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QSurface::SurfaceType surfaceType = window->surfaceType(); const bool needRootWindow = options() & RootWindow; switch (surfaceType) { @@ -330,14 +329,25 @@ QPlatformWindow *QQnxIntegration::createPlatformWindow(QWindow *window) const QPlatformBackingStore *QQnxIntegration::createPlatformBackingStore(QWindow *window) const { - qIntegrationDebug(); - return new QQnxRasterBackingStore(window); + QSurface::SurfaceType surfaceType = window->surfaceType(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO << surfaceType; + switch (surfaceType) { + case QSurface::RasterSurface: + return new QQnxRasterBackingStore(window); +#if !defined(QT_NO_OPENGL) + // Return a QRhiBackingStore for non-raster surface windows + case QSurface::OpenGLSurface: + return new QRhiBackingStore(window); +#endif + default: + return nullptr; + } } #if !defined(QT_NO_OPENGL) QPlatformOpenGLContext *QQnxIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Get color channel sizes from window format QSurfaceFormat format = context->format(); @@ -396,7 +406,7 @@ QPlatformOpenGLContext *QQnxIntegration::createPlatformOpenGLContext(QOpenGLCont #if QT_CONFIG(qqnx_pps) QPlatformInputContext *QQnxIntegration::inputContext() const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; if (m_qpaInputContext) return m_qpaInputContext; return m_inputContext; @@ -405,7 +415,7 @@ QPlatformInputContext *QQnxIntegration::inputContext() const void QQnxIntegration::moveToScreen(QWindow *window, int screen) { - qIntegrationDebug() << "w =" << window << ", s =" << screen; + qCDebug(lcQpaQnx) << Q_FUNC_INFO << "w =" << window << ", s =" << screen; // get platform window used by widget QQnxWindow *platformWindow = static_cast<QQnxWindow *>(window->handle()); @@ -419,7 +429,7 @@ void QQnxIntegration::moveToScreen(QWindow *window, int screen) QAbstractEventDispatcher *QQnxIntegration::createEventDispatcher() const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // We transfer ownersip of the event-dispatcher to QtCoreApplication QAbstractEventDispatcher *eventDispatcher = m_eventDispatcher; @@ -436,7 +446,7 @@ QPlatformNativeInterface *QQnxIntegration::nativeInterface() const #if !defined(QT_NO_CLIPBOARD) QPlatformClipboard *QQnxIntegration::clipboard() const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; #if QT_CONFIG(qqnx_pps) if (!m_clipboard) @@ -455,7 +465,7 @@ QPlatformDrag *QQnxIntegration::drag() const QVariant QQnxIntegration::styleHint(QPlatformIntegration::StyleHint hint) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; if ((hint == ShowIsFullScreen) && (m_options & FullScreenApplication)) return true; @@ -469,7 +479,7 @@ QPlatformServices * QQnxIntegration::services() const QWindow *QQnxIntegration::window(screen_window_t qnxWindow) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QMutexLocker locker(&m_windowMapperMutex); Q_UNUSED(locker); return m_windowMapper.value(qnxWindow, 0); @@ -477,7 +487,7 @@ QWindow *QQnxIntegration::window(screen_window_t qnxWindow) const void QQnxIntegration::addWindow(screen_window_t qnxWindow, QWindow *window) { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QMutexLocker locker(&m_windowMapperMutex); Q_UNUSED(locker); m_windowMapper.insert(qnxWindow, window); @@ -485,7 +495,7 @@ void QQnxIntegration::addWindow(screen_window_t qnxWindow, QWindow *window) void QQnxIntegration::removeWindow(screen_window_t qnxWindow) { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QMutexLocker locker(&m_windowMapperMutex); Q_UNUSED(locker); m_windowMapper.remove(qnxWindow); @@ -571,7 +581,7 @@ QList<screen_display_t *> QQnxIntegration::sortDisplays(screen_display_t *availa // Go through all the requested displays IDs QList<screen_display_t *> orderedDisplays; - for (const QJsonValue &value : qAsConst(requestedDisplays)) { + for (const QJsonValue &value : std::as_const(requestedDisplays)) { int requestedValue = value.toInt(); // Move all displays with matching ID from the intermediate list @@ -594,7 +604,7 @@ QList<screen_display_t *> QQnxIntegration::sortDisplays(screen_display_t *availa void QQnxIntegration::createDisplays() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Query number of displays int displayCount = 0; int result = screen_get_context_property_iv(m_screenContext, SCREEN_PROPERTY_DISPLAY_COUNT, @@ -624,11 +634,12 @@ void QQnxIntegration::createDisplays() Q_SCREEN_CHECKERROR(result, "Failed to query display attachment"); if (!isAttached) { - qIntegrationDebug("Skipping non-attached display %d", i); + qCDebug(lcQpaQnx) << "Skipping non-attached display " << i; continue; } - qIntegrationDebug("Creating screen for display %d", i); + qCDebug(lcQpaQnx) << "Creating screen for display " << i; + createDisplay(*orderedDisplays[i], /*isPrimary=*/false); } // of displays iteration } @@ -662,7 +673,8 @@ void QQnxIntegration::removeDisplay(QQnxScreen *screen) void QQnxIntegration::destroyDisplays() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; + Q_FOREACH (QQnxScreen *screen, m_screens) { QWindowSystemInterface::handleScreenRemoved(screen); } @@ -713,7 +725,7 @@ bool QQnxIntegration::supportsNavigatorEvents() const #if QT_CONFIG(opengl) void QQnxIntegration::createEglDisplay() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Initialize connection to EGL m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); @@ -727,7 +739,7 @@ void QQnxIntegration::createEglDisplay() void QQnxIntegration::destroyEglDisplay() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Close connection to EGL eglTerminate(m_eglDisplay); diff --git a/src/plugins/platforms/qnx/qqnxintegration.h b/src/plugins/platforms/qnx/qqnxintegration.h index 590b9450c2..8a78d54ceb 100644 --- a/src/plugins/platforms/qnx/qqnxintegration.h +++ b/src/plugins/platforms/qnx/qqnxintegration.h @@ -6,9 +6,11 @@ #include <qpa/qplatformintegration.h> #include <private/qtguiglobal_p.h> +#include <QtCore/qhash.h> #include <QtCore/qmutex.h> #include <screen/screen.h> +#include <QtCore/QLoggingCategory> #if QT_CONFIG(opengl) #include <EGL/egl.h> @@ -16,6 +18,9 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnx); +Q_DECLARE_LOGGING_CATEGORY(lcQpaGLContext); + class QQnxScreenEventThread; class QQnxFileDialogHelper; class QQnxNativeInterface; @@ -39,8 +44,7 @@ class QQnxButtonEventNotifier; class QQnxClipboard; #endif -template<class K, class V> class QHash; -typedef QHash<screen_window_t, QWindow *> QQnxWindowMapper; +using QQnxWindowMapper = QHash<screen_window_t, QWindow *>; class QQnxIntegration : public QPlatformIntegration { diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp index cee86a68a0..7580e560aa 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp @@ -10,11 +10,7 @@ #include <QGuiApplication> #include <qpa/qwindowsysteminterface.h> -#if defined(QQNXNAVIGATOREVENTHANDLER_DEBUG) -#define qNavigatorEventHandlerDebug qDebug -#else -#define qNavigatorEventHandlerDebug QT_NO_QDEBUG_MACRO -#endif +Q_LOGGING_CATEGORY(lcQpaQnxNavigatorEvents, "qt.qpa.qnx.navigator.events"); QT_BEGIN_NAMESPACE @@ -27,20 +23,20 @@ bool QQnxNavigatorEventHandler::handleOrientationCheck(int angle) { // reply to navigator that (any) orientation is acceptable // TODO: check if top window flags prohibit orientation change - qNavigatorEventHandlerDebug("angle=%d", angle); + qCDebug(lcQpaQnxNavigatorEvents, "angle=%d", angle); return true; } void QQnxNavigatorEventHandler::handleOrientationChange(int angle) { // update screen geometry and reply to navigator that we're ready - qNavigatorEventHandlerDebug("angle=%d", angle); + qCDebug(lcQpaQnxNavigatorEvents, "angle=%d", angle); emit rotationChanged(angle); } void QQnxNavigatorEventHandler::handleSwipeDown() { - qNavigatorEventHandlerDebug(); + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO; Q_EMIT swipeDown(); } @@ -48,25 +44,25 @@ void QQnxNavigatorEventHandler::handleSwipeDown() void QQnxNavigatorEventHandler::handleExit() { // shutdown everything - qNavigatorEventHandlerDebug(); + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO; QCoreApplication::quit(); } void QQnxNavigatorEventHandler::handleWindowGroupActivated(const QByteArray &id) { - qNavigatorEventHandlerDebug() << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << id; Q_EMIT windowGroupActivated(id); } void QQnxNavigatorEventHandler::handleWindowGroupDeactivated(const QByteArray &id) { - qNavigatorEventHandlerDebug() << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << id; Q_EMIT windowGroupDeactivated(id); } void QQnxNavigatorEventHandler::handleWindowGroupStateChanged(const QByteArray &id, Qt::WindowState state) { - qNavigatorEventHandlerDebug() << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << id; Q_EMIT windowGroupStateChanged(id, state); } diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h index 342b6c3ef6..826c9a0ff9 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h @@ -5,9 +5,12 @@ #define QQNXNAVIGATOREVENTHANDLER_H #include <QObject> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnxNavigatorEvents); + class QQnxNavigatorEventHandler : public QObject { Q_OBJECT diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp index 5d099b7e46..44c71dad52 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp @@ -17,16 +17,12 @@ #include <sys/types.h> #include <sys/stat.h> -#if defined(QQNXNAVIGATOREVENTNOTIFIER_DEBUG) -#define qNavigatorEventNotifierDebug qDebug -#else -#define qNavigatorEventNotifierDebug QT_NO_QDEBUG_MACRO -#endif +QT_BEGIN_NAMESPACE -static const char *navigatorControlPath = "/pps/services/navigator/control"; -static const int ppsBufferSize = 4096; +// Q_LOGGING_CATEGORY(lcQpaQnxNavigatorEvents, "qt.qpa.qnx.navigator.events"); -QT_BEGIN_NAMESPACE +const char *QQnxNavigatorEventNotifier::navigatorControlPath = "/pps/services/navigator/control"; +const size_t QQnxNavigatorEventNotifier::ppsBufferSize = 4096; QQnxNavigatorEventNotifier::QQnxNavigatorEventNotifier(QQnxNavigatorEventHandler *eventHandler, QObject *parent) : QObject(parent), @@ -44,18 +40,18 @@ QQnxNavigatorEventNotifier::~QQnxNavigatorEventNotifier() if (m_fd != -1) close(m_fd); - qNavigatorEventNotifierDebug("navigator event notifier stopped"); + qCDebug(lcQpaQnxNavigatorEvents) << "Navigator event notifier stopped"; } void QQnxNavigatorEventNotifier::start() { - qNavigatorEventNotifierDebug("navigator event notifier started"); + qCDebug(lcQpaQnxNavigatorEvents) << "Navigator event notifier started"; // open connection to navigator errno = 0; m_fd = open(navigatorControlPath, O_RDWR); if (m_fd == -1) { - qNavigatorEventNotifierDebug("failed to open navigator pps: %s", strerror(errno)); + qCDebug(lcQpaQnxNavigatorEvents, "Failed to open navigator pps: %s", strerror(errno)); return; } @@ -65,7 +61,7 @@ void QQnxNavigatorEventNotifier::start() void QQnxNavigatorEventNotifier::parsePPS(const QByteArray &ppsData, QByteArray &msg, QByteArray &dat, QByteArray &id) { - qNavigatorEventNotifierDebug() << "data=" << ppsData; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "data=" << ppsData; // tokenize pps data into lines QList<QByteArray> lines = ppsData.split('\n'); @@ -79,7 +75,7 @@ void QQnxNavigatorEventNotifier::parsePPS(const QByteArray &ppsData, QByteArray // tokenize current attribute const QByteArray &attr = lines.at(i); - qNavigatorEventNotifierDebug() << "attr=" << attr; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "attr=" << attr; int firstColon = attr.indexOf(':'); if (firstColon == -1) { @@ -96,8 +92,7 @@ void QQnxNavigatorEventNotifier::parsePPS(const QByteArray &ppsData, QByteArray QByteArray key = attr.left(firstColon); QByteArray value = attr.mid(secondColon + 1); - qNavigatorEventNotifierDebug() << "key=" << key; - qNavigatorEventNotifierDebug() << "val=" << value; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "key =" << key << "value =" << value; // save attribute value if (key == "msg") @@ -124,7 +119,7 @@ void QQnxNavigatorEventNotifier::replyPPS(const QByteArray &res, const QByteArra } ppsData += "\n"; - qNavigatorEventNotifierDebug() << "reply=" << ppsData; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "reply=" << ppsData; // send pps message to navigator errno = 0; @@ -135,7 +130,7 @@ void QQnxNavigatorEventNotifier::replyPPS(const QByteArray &res, const QByteArra void QQnxNavigatorEventNotifier::handleMessage(const QByteArray &msg, const QByteArray &dat, const QByteArray &id) { - qNavigatorEventNotifierDebug() << "msg=" << msg << ", dat=" << dat << ", id=" << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "msg=" << msg << ", dat=" << dat << ", id=" << id; // check message type if (msg == "orientationCheck") { @@ -159,7 +154,7 @@ void QQnxNavigatorEventNotifier::handleMessage(const QByteArray &msg, const QByt void QQnxNavigatorEventNotifier::readData() { - qNavigatorEventNotifierDebug("reading navigator data"); + qCDebug(lcQpaQnxNavigatorEvents) << "Reading navigator data"; // allocate buffer for pps data char buffer[ppsBufferSize]; diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h index 6ecf776f36..66100ece3f 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h @@ -32,6 +32,9 @@ private: int m_fd; QSocketNotifier *m_readNotifier; QQnxNavigatorEventHandler *m_eventHandler; + + static const char *navigatorControlPath; + static const size_t ppsBufferSize; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp b/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp index 9ca402822d..3a2fee0afb 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp +++ b/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp @@ -8,17 +8,11 @@ #include <QByteArray> #include <private/qcore_unix_p.h> -#if defined(QQNXNAVIGATOR_DEBUG) -#define qNavigatorDebug qDebug -#else -#define qNavigatorDebug QT_NO_QDEBUG_MACRO -#endif - -static const char *navigatorControlPath = "/pps/services/navigator/control"; -static const int ppsBufferSize = 4096; - QT_BEGIN_NAMESPACE +const char *QQnxNavigatorPps::navigatorControlPath = "/pps/services/navigator/control"; +const size_t QQnxNavigatorPps::ppsBufferSize = 4096; + QQnxNavigatorPps::QQnxNavigatorPps(QObject *parent) : QQnxAbstractNavigator(parent) , m_fd(-1) @@ -45,7 +39,7 @@ bool QQnxNavigatorPps::openPpsConnection() return false; } - qNavigatorDebug("successfully connected to Navigator. fd=%d", m_fd); + qCDebug(lcQpaQnxNavigator) << "successfully connected to Navigator. fd=" << m_fd; return true; } @@ -67,7 +61,7 @@ bool QQnxNavigatorPps::sendPpsMessage(const QByteArray &message, const QByteArra ppsMessage += "\n"; - qNavigatorDebug() << "sending PPS message:\n" << ppsMessage; + qCDebug(lcQpaQnxNavigator) << "sending PPS message:\n" << ppsMessage; // send pps message to navigator errno = 0; @@ -89,7 +83,7 @@ bool QQnxNavigatorPps::sendPpsMessage(const QByteArray &message, const QByteArra // ensure data is null terminated buffer[bytes] = '\0'; - qNavigatorDebug() << "received PPS message:\n" << buffer; + qCDebug(lcQpaQnxNavigator) << "received PPS message:\n" << buffer; // process received message QByteArray ppsData(buffer); @@ -108,7 +102,7 @@ bool QQnxNavigatorPps::sendPpsMessage(const QByteArray &message, const QByteArra void QQnxNavigatorPps::parsePPS(const QByteArray &ppsData, QHash<QByteArray, QByteArray> &messageFields) { - qNavigatorDebug() << "data=" << ppsData; + qCDebug(lcQpaQnxNavigator) << "data=" << ppsData; // tokenize pps data into lines QList<QByteArray> lines = ppsData.split('\n'); @@ -123,7 +117,7 @@ void QQnxNavigatorPps::parsePPS(const QByteArray &ppsData, QHash<QByteArray, QBy // tokenize current attribute const QByteArray &attr = lines.at(i); - qNavigatorDebug() << "attr=" << attr; + qCDebug(lcQpaQnxNavigator) << "attr=" << attr; int firstColon = attr.indexOf(':'); if (firstColon == -1) { @@ -140,8 +134,7 @@ void QQnxNavigatorPps::parsePPS(const QByteArray &ppsData, QHash<QByteArray, QBy QByteArray key = attr.left(firstColon); QByteArray value = attr.mid(secondColon + 1); - qNavigatorDebug() << "key=" << key; - qNavigatorDebug() << "val=" << value; + qCDebug(lcQpaQnxNavigator) << "key=" << key << "value=" << value; messageFields[key] = value; } } diff --git a/src/plugins/platforms/qnx/qqnxnavigatorpps.h b/src/plugins/platforms/qnx/qqnxnavigatorpps.h index 7f23097bc9..889b9f62eb 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatorpps.h +++ b/src/plugins/platforms/qnx/qqnxnavigatorpps.h @@ -5,9 +5,12 @@ #define QQNXNAVIGATORPPS_H #include "qqnxabstractnavigator.h" +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnxNavigator); + template <typename K, typename V> class QHash; class QQnxNavigatorPps : public QQnxAbstractNavigator @@ -26,8 +29,9 @@ private: bool sendPpsMessage(const QByteArray &message, const QByteArray &data); void parsePPS(const QByteArray &ppsData, QHash<QByteArray, QByteArray> &messageFields); -private: int m_fd; + static const char *navigatorControlPath; + static const size_t ppsBufferSize; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp b/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp index cf80e44f84..b94c056a79 100644 --- a/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp +++ b/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp @@ -10,12 +10,6 @@ #include <errno.h> -#if defined(QQNXRASTERBACKINGSTORE_DEBUG) -#define qRasterBackingStoreDebug qDebug -#else -#define qRasterBackingStoreDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE QQnxRasterBackingStore::QQnxRasterBackingStore(QWindow *window) @@ -23,14 +17,14 @@ QQnxRasterBackingStore::QQnxRasterBackingStore(QWindow *window) m_needsPosting(false), m_scrolled(false) { - qRasterBackingStoreDebug() << "w =" << window; + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window; m_window = window; } QQnxRasterBackingStore::~QQnxRasterBackingStore() { - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); } QPaintDevice *QQnxRasterBackingStore::paintDevice() @@ -45,7 +39,7 @@ void QQnxRasterBackingStore::flush(QWindow *window, const QRegion ®ion, const { Q_UNUSED(offset); - qRasterBackingStoreDebug() << "w =" << this->window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << this->window(); // Sometimes this method is called even though there is nothing to be // flushed (posted in "screen" parlance), for instance, after an expose @@ -67,7 +61,7 @@ void QQnxRasterBackingStore::resize(const QSize &size, const QRegion &staticCont { Q_UNUSED(size); Q_UNUSED(staticContents); - qRasterBackingStoreDebug() << "w =" << window() << ", s =" << size; + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window() << ", s =" << size; // NOTE: defer resizing window buffers until next paint as // resize() can be called multiple times before a paint occurs @@ -75,7 +69,7 @@ void QQnxRasterBackingStore::resize(const QSize &size, const QRegion &staticCont bool QQnxRasterBackingStore::scroll(const QRegion &area, int dx, int dy) { - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); m_needsPosting = true; @@ -91,7 +85,7 @@ void QQnxRasterBackingStore::beginPaint(const QRegion ®ion) { Q_UNUSED(region); - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); m_needsPosting = true; platformWindow()->adjustBufferSize(); @@ -119,7 +113,7 @@ void QQnxRasterBackingStore::beginPaint(const QRegion ®ion) void QQnxRasterBackingStore::endPaint() { - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); } QQnxRasterWindow *QQnxRasterBackingStore::platformWindow() const diff --git a/src/plugins/platforms/qnx/qqnxrasterwindow.cpp b/src/plugins/platforms/qnx/qqnxrasterwindow.cpp index fb7a1d3fd3..303c9e7c06 100644 --- a/src/plugins/platforms/qnx/qqnxrasterwindow.cpp +++ b/src/plugins/platforms/qnx/qqnxrasterwindow.cpp @@ -10,12 +10,6 @@ #include <errno.h> -#if defined(QQNXRASTERWINDOW_DEBUG) -#define qRasterWindowDebug qDebug -#else -#define qRasterWindowDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE QQnxRasterWindow::QQnxRasterWindow(QWindow *window, screen_context_t context, bool needRootWindow) : @@ -61,7 +55,7 @@ void QQnxRasterWindow::post(const QRegion &dirty) // Check if render buffer exists and something was rendered if (m_currentBufferIndex != -1 && !dirty.isEmpty()) { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); QQnxBuffer ¤tBuffer = m_buffers[m_currentBufferIndex]; // Copy unmodified region from old render buffer to new render buffer; @@ -94,14 +88,14 @@ void QQnxRasterWindow::post(const QRegion &dirty) void QQnxRasterWindow::scroll(const QRegion ®ion, int dx, int dy, bool flush) { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); blitPreviousToCurrent(region, dx, dy, flush); m_scrolled += region; } QQnxBuffer &QQnxRasterWindow::renderBuffer() { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); // Check if render buffer is invalid if (m_currentBufferIndex == -1) { @@ -162,7 +156,7 @@ void QQnxRasterWindow::resetBuffers() void QQnxRasterWindow::blitPreviousToCurrent(const QRegion ®ion, int dx, int dy, bool flush) { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); // Abort if previous buffer is invalid or if nothing to copy if (m_previousBufferIndex == -1 || region.isEmpty()) diff --git a/src/plugins/platforms/qnx/qqnxscreen.cpp b/src/plugins/platforms/qnx/qqnxscreen.cpp index 66f8bfae01..f2c3b3847d 100644 --- a/src/plugins/platforms/qnx/qqnxscreen.cpp +++ b/src/plugins/platforms/qnx/qqnxscreen.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2011 - 2013 BlackBerry Limited. All rights reserved. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qqnxglobal.h" #include "qqnxscreen.h" @@ -13,12 +15,6 @@ #include <errno.h> -#if defined(QQNXSCREEN_DEBUG) -#define qScreenDebug qDebug -#else -#define qScreenDebug QT_NO_QDEBUG_MACRO -#endif - #if defined(QQNX_PHYSICAL_SCREEN_WIDTH) && QQNX_PHYSICAL_SCREEN_WIDTH > 0 \ && defined(QQNX_PHYSICAL_SCREEN_HEIGHT) && QQNX_PHYSICAL_SCREEN_HEIGHT > 0 #define QQNX_PHYSICAL_SCREEN_SIZE_DEFINED @@ -32,6 +28,8 @@ static const int MAX_UNDERLAY_ZORDER = -1; QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaScreen, "qt.qpa.screen"); + static QSize determineScreenSize(screen_display_t display, bool primaryScreen) { int val[2]; @@ -44,9 +42,9 @@ static QSize determineScreenSize(screen_display_t display, bool primaryScreen) { if (val[0] > 0 && val[1] > 0) return QSize(val[0], val[1]); - qScreenDebug("QQnxScreen: screen_get_display_property_iv() reported an invalid " - "physical screen size (%dx%d). Falling back to QQNX_PHYSICAL_SCREEN_SIZE " - "environment variable.", val[0], val[1]); + qCDebug(lcQpaScreen, "QQnxScreen: screen_get_display_property_iv() reported an invalid " + "physical screen size (%dx%d). Falling back to QQNX_PHYSICAL_SCREEN_SIZE " + "environment variable.", val[0], val[1]); const QString envPhySizeStr = qgetenv("QQNX_PHYSICAL_SCREEN_SIZE"); if (!envPhySizeStr.isEmpty()) { @@ -88,7 +86,7 @@ QQnxScreen::QQnxScreen(screen_context_t screenContext, screen_display_t display, m_coverWindow(0), m_cursor(new QQnxCursor()) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; // Cache initial orientation of this display int result = screen_get_display_property_iv(m_display, SCREEN_PROPERTY_ROTATION, &m_initialRotation); @@ -125,7 +123,7 @@ QQnxScreen::QQnxScreen(screen_context_t screenContext, screen_display_t display, QQnxScreen::~QQnxScreen() { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; Q_FOREACH (QQnxWindow *childWindow, m_childWindows) childWindow->setScreen(0); @@ -234,7 +232,7 @@ QPixmap QQnxScreen::grabWindow(WId window, int x, int y, int width, int height) static int defaultDepth() { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; static int defaultDepth = 0; if (defaultDepth == 0) { // check if display depth was specified in environment variable; @@ -248,7 +246,7 @@ static int defaultDepth() QRect QQnxScreen::availableGeometry() const { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; // available geometry = total geometry - keyboard return QRect(m_currentGeometry.x(), m_currentGeometry.y(), m_currentGeometry.width(), m_currentGeometry.height() - m_keyboardHeight); @@ -268,12 +266,12 @@ qreal QQnxScreen::refreshRate() const qWarning("QQnxScreen: Failed to query screen mode. Using default value of 60Hz"); return 60.0; } - qScreenDebug("screen mode:\n" - " width = %u\n" - " height = %u\n" - " refresh = %u\n" - " interlaced = %u", - uint(displayMode.width), uint(displayMode.height), uint(displayMode.refresh), uint(displayMode.interlaced)); + qCDebug(lcQpaScreen, "screen mode:\n" + " width = %u\n" + " height = %u\n" + " refresh = %u\n" + " interlaced = %u", + uint(displayMode.width), uint(displayMode.height), uint(displayMode.refresh), uint(displayMode.interlaced)); return static_cast<qreal>(displayMode.refresh); } @@ -307,7 +305,7 @@ Qt::ScreenOrientation QQnxScreen::orientation() const else orient = Qt::InvertedLandscapeOrientation; } - qScreenDebug() << "orientation =" << orient; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Orientation =" << orient; return orient; } @@ -331,7 +329,7 @@ static bool isOrthogonal(int angle1, int angle2) void QQnxScreen::setRotation(int rotation) { - qScreenDebug("orientation = %d", rotation); + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "orientation =" << rotation; // Check if rotation changed // We only want to rotate if we are the primary screen if (m_currentRotation != rotation && isPrimaryScreen()) { @@ -352,7 +350,7 @@ void QQnxScreen::setRotation(int rotation) // Resize root window if we've rotated 90 or 270 from previous orientation if (isOrthogonal(m_currentRotation, rotation)) { - qScreenDebug() << "resize, size =" << m_currentGeometry.size(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "resize, size =" << m_currentGeometry.size(); if (rootWindow()) rootWindow()->setGeometry(QRect(QPoint(0,0), m_currentGeometry.size())); @@ -499,7 +497,7 @@ QQnxWindow *QQnxScreen::findWindow(screen_window_t windowHandle) const void QQnxScreen::addWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (m_childWindows.contains(window)) return; @@ -522,7 +520,7 @@ void QQnxScreen::addWindow(QQnxWindow *window) void QQnxScreen::removeWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (window != m_coverWindow) { const int numWindowsRemoved = m_childWindows.removeAll(window); @@ -537,7 +535,7 @@ void QQnxScreen::removeWindow(QQnxWindow *window) void QQnxScreen::raiseWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (window != m_coverWindow) { removeWindow(window); @@ -547,7 +545,7 @@ void QQnxScreen::raiseWindow(QQnxWindow *window) void QQnxScreen::lowerWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (window != m_coverWindow) { removeWindow(window); @@ -557,7 +555,7 @@ void QQnxScreen::lowerWindow(QQnxWindow *window) void QQnxScreen::updateHierarchy() { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; QList<QQnxWindow*>::const_iterator it; int result; @@ -707,7 +705,7 @@ void QQnxScreen::windowClosed(void *window) void QQnxScreen::windowGroupStateChanged(const QByteArray &id, Qt::WindowState state) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; if (!rootWindow() || id != rootWindow()->groupName()) return; @@ -722,7 +720,7 @@ void QQnxScreen::windowGroupStateChanged(const QByteArray &id, Qt::WindowState s void QQnxScreen::activateWindowGroup(const QByteArray &id) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; if (!rootWindow() || id != rootWindow()->groupName()) return; @@ -741,7 +739,7 @@ void QQnxScreen::activateWindowGroup(const QByteArray &id) void QQnxScreen::deactivateWindowGroup(const QByteArray &id) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; if (!rootWindow() || id != rootWindow()->groupName()) return; diff --git a/src/plugins/platforms/qnx/qqnxscreen.h b/src/plugins/platforms/qnx/qqnxscreen.h index f988b42ab4..17b282bdc1 100644 --- a/src/plugins/platforms/qnx/qqnxscreen.h +++ b/src/plugins/platforms/qnx/qqnxscreen.h @@ -30,6 +30,10 @@ const int SCREEN_PROPERTY_SYM = SCREEN_PROPERTY_KEY_SYM; QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen); +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreenEvents); +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreenBuffer); + class QQnxWindow; class QQnxScreen : public QObject, public QPlatformScreen diff --git a/src/plugins/platforms/qnx/qqnxscreeneventhandler.cpp b/src/plugins/platforms/qnx/qqnxscreeneventhandler.cpp index 2a2422f5cc..6d923bc3a8 100644 --- a/src/plugins/platforms/qnx/qqnxscreeneventhandler.cpp +++ b/src/plugins/platforms/qnx/qqnxscreeneventhandler.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2013 BlackBerry Limited. All rights reserved. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qqnxglobal.h" #include "qqnxscreeneventhandler.h" @@ -17,11 +19,7 @@ #include <errno.h> #include <sys/keycodes.h> -#if defined(QQNXSCREENEVENT_DEBUG) -#define qScreenEventDebug qDebug -#else -#define qScreenEventDebug QT_NO_QDEBUG_MACRO -#endif +Q_LOGGING_CATEGORY(lcQpaScreenEvents, "qt.qpa.screen.events"); static int qtKey(int virtualKey, QChar::Category category) { @@ -195,7 +193,7 @@ bool QQnxScreenEventHandler::handleEvent(screen_event_t event, int qnxType) default: // event ignored - qScreenEventDebug("unknown event %d", qnxType); + qCDebug(lcQpaScreenEvents) << Q_FUNC_INFO << "Unknown event" << qnxType; return false; } @@ -233,7 +231,7 @@ void QQnxScreenEventHandler::injectKeyboardEvent(int flags, int sym, int modifie QWindowSystemInterface::handleExtendedKeyEvent(QGuiApplication::focusWindow(), type, key, qtMod, scan, virtualKey, modifiers, keyStr, flags & KEY_REPEAT); - qScreenEventDebug() << "Qt key t=" << type << ", k=" << key << ", s=" << keyStr; + qCDebug(lcQpaScreenEvents) << "Qt key t=" << type << ", k=" << key << ", s=" << keyStr; } void QQnxScreenEventHandler::setScreenEventThread(QQnxScreenEventThread *eventThread) @@ -362,12 +360,12 @@ void QQnxScreenEventHandler::handlePointerEvent(screen_event_t event) if (wOld) { QWindowSystemInterface::handleLeaveEvent(wOld); - qScreenEventDebug() << "Qt leave, w=" << wOld; + qCDebug(lcQpaScreenEvents) << "Qt leave, w=" << wOld; } if (w) { QWindowSystemInterface::handleEnterEvent(w); - qScreenEventDebug() << "Qt enter, w=" << w; + qCDebug(lcQpaScreenEvents) << "Qt enter, w=" << w; } } @@ -410,8 +408,8 @@ void QQnxScreenEventHandler::handlePointerEvent(screen_event_t event) QWindowSystemInterface::handleMouseEvent(w, timestamp, m_mouseDevice, localPoint, globalPoint, buttons, Qt::NoButton, QEvent::MouseMove); - qScreenEventDebug() << "Qt mouse move, w=" << w << ", (" << localPoint.x() << "," - << localPoint.y() << "), b=" << static_cast<int>(buttons); + qCDebug(lcQpaScreenEvents) << "Qt mouse move, w=" << w << ", (" << localPoint.x() << "," + << localPoint.y() << "), b=" << static_cast<int>(buttons); } if (m_lastButtonState != buttons) { @@ -426,8 +424,8 @@ void QQnxScreenEventHandler::handlePointerEvent(screen_event_t event) QWindowSystemInterface::handleMouseEvent(w, timestamp, m_mouseDevice, localPoint, globalPoint, buttons, button, QEvent::MouseButtonRelease); - qScreenEventDebug() << "Qt mouse release, w=" << w << ", (" << localPoint.x() - << "," << localPoint.y() << "), b=" << button; + qCDebug(lcQpaScreenEvents) << "Qt mouse release, w=" << w << ", (" << localPoint.x() + << "," << localPoint.y() << "), b=" << button; } } @@ -441,8 +439,8 @@ void QQnxScreenEventHandler::handlePointerEvent(screen_event_t event) QWindowSystemInterface::handleMouseEvent(w, timestamp, m_mouseDevice, localPoint, globalPoint, buttons, button, QEvent::MouseButtonPress); - qScreenEventDebug() << "Qt mouse press, w=" << w << ", (" << localPoint.x() - << "," << localPoint.y() << "), b=" << button; + qCDebug(lcQpaScreenEvents) << "Qt mouse press, w=" << w << ", (" << localPoint.x() + << "," << localPoint.y() << "), b=" << button; } } } @@ -453,7 +451,7 @@ void QQnxScreenEventHandler::handlePointerEvent(screen_event_t event) QPoint angleDelta(0, wheelDelta); QWindowSystemInterface::handleWheelEvent(w, timestamp, m_mouseDevice, localPoint, globalPoint, QPoint(), angleDelta); - qScreenEventDebug() << "Qt wheel, w=" << w << ", (" << localPoint.x() << "," + qCDebug(lcQpaScreenEvents) << "Qt wheel, w=" << w << ", (" << localPoint.x() << "," << localPoint.y() << "), d=" << static_cast<int>(wheelDelta); } } @@ -511,12 +509,12 @@ void QQnxScreenEventHandler::handleTouchEvent(screen_event_t event, int qnxType) if (wOld) { QWindowSystemInterface::handleLeaveEvent(wOld); - qScreenEventDebug() << "Qt leave, w=" << wOld; + qCDebug(lcQpaScreenEvents) << "Qt leave, w=" << wOld; } if (w) { QWindowSystemInterface::handleEnterEvent(w); - qScreenEventDebug() << "Qt enter, w=" << w; + qCDebug(lcQpaScreenEvents) << "Qt enter, w=" << w; } } m_lastMouseWindow = qnxWindow; @@ -583,9 +581,9 @@ void QQnxScreenEventHandler::handleTouchEvent(screen_event_t event, int qnxType) // inject event into Qt QWindowSystemInterface::handleTouchEvent(w, m_touchDevice, pointList); - qScreenEventDebug() << "Qt touch, w =" << w - << ", p=" << m_touchPoints[touchId].area.topLeft() - << ", t=" << type; + qCDebug(lcQpaScreenEvents) << "Qt touch, w =" << w + << ", p=" << m_touchPoints[touchId].area.topLeft() + << ", t=" << type; } } } @@ -629,7 +627,8 @@ void QQnxScreenEventHandler::handleDisplayEvent(screen_event_t event) return; } - qScreenEventDebug() << "display attachment is now:" << isAttached; + qCDebug(lcQpaScreenEvents) << "display attachment is now:" << isAttached; + QQnxScreen *screen = m_qnxIntegration->screenForNative(nativeDisplay); if (!screen) { @@ -639,7 +638,7 @@ void QQnxScreenEventHandler::handleDisplayEvent(screen_event_t event) if (val[0] == 0 && val[1] == 0) //If screen size is invalid, wait for the next event return; - qScreenEventDebug("creating new QQnxScreen for newly attached display"); + qCDebug(lcQpaScreenEvents) << "Creating new QQnxScreen for newly attached display"; m_qnxIntegration->createDisplay(nativeDisplay, false /* not primary, we assume */); } } else if (!isAttached) { @@ -652,7 +651,7 @@ void QQnxScreenEventHandler::handleDisplayEvent(screen_event_t event) if (!screen->isPrimaryScreen()) { // libscreen display is deactivated, let's remove the QQnxScreen / QScreen - qScreenEventDebug("removing display"); + qCDebug(lcQpaScreenEvents) << "Removing display"; m_qnxIntegration->removeDisplay(screen); } } @@ -689,7 +688,7 @@ void QQnxScreenEventHandler::handlePropertyEvent(screen_event_t event) break; default: // event ignored - qScreenEventDebug() << "Ignore property event for property: " << property; + qCDebug(lcQpaScreenEvents) << "Ignore property event for property: " << property; } } @@ -708,7 +707,7 @@ void QQnxScreenEventHandler::handleKeyboardFocusPropertyEvent(screen_window_t wi } if (focus && focusWindow != QGuiApplication::focusWindow()) - QWindowSystemInterface::handleWindowActivated(focusWindow, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(focusWindow, Qt::ActiveWindowFocusReason); else if (!focus && focusWindow == QGuiApplication::focusWindow()) m_focusLostTimer = startTimer(50); } @@ -732,7 +731,7 @@ void QQnxScreenEventHandler::handleGeometryPropertyEvent(screen_window_t window) QWindowSystemInterface::handleGeometryChange(qtWindow, rect); } - qScreenEventDebug() << qtWindow << "moved to" << rect; + qCDebug(lcQpaScreenEvents) << qtWindow << "moved to" << rect; } void QQnxScreenEventHandler::timerEvent(QTimerEvent *event) @@ -746,6 +745,6 @@ void QQnxScreenEventHandler::timerEvent(QTimerEvent *event) } } -#include "moc_qqnxscreeneventhandler.cpp" - QT_END_NAMESPACE + +#include "moc_qqnxscreeneventhandler.cpp" diff --git a/src/plugins/platforms/qnx/qqnxscreeneventhandler.h b/src/plugins/platforms/qnx/qqnxscreeneventhandler.h index d27186af9e..ba3579aaf7 100644 --- a/src/plugins/platforms/qnx/qqnxscreeneventhandler.h +++ b/src/plugins/platforms/qnx/qqnxscreeneventhandler.h @@ -5,11 +5,14 @@ #define QQNXSCREENEVENTHANDLER_H #include <qpa/qwindowsysteminterface.h> +#include <QtCore/QLoggingCategory> #include <screen/screen.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreenEvents); + class QQnxIntegration; class QQnxScreenEventFilter; class QQnxScreenEventThread; diff --git a/src/plugins/platforms/qnx/qqnxscreeneventthread.cpp b/src/plugins/platforms/qnx/qqnxscreeneventthread.cpp index 69997e4ba0..6b4ffc3962 100644 --- a/src/plugins/platforms/qnx/qqnxscreeneventthread.cpp +++ b/src/plugins/platforms/qnx/qqnxscreeneventthread.cpp @@ -14,12 +14,6 @@ #include <cctype> -#if defined(QQNXSCREENEVENTTHREAD_DEBUG) -#define qScreenEventThreadDebug qDebug -#else -#define qScreenEventThreadDebug QT_NO_QDEBUG_MACRO -#endif - static const int c_screenCode = _PULSE_CODE_MINAVAIL + 0; static const int c_armCode = _PULSE_CODE_MINAVAIL + 1; static const int c_quitCode = _PULSE_CODE_MINAVAIL + 2; @@ -74,7 +68,7 @@ QQnxScreenEventThread::~QQnxScreenEventThread() void QQnxScreenEventThread::run() { - qScreenEventThreadDebug("screen event thread started"); + qCDebug(lcQpaScreenEvents) << "Screen event thread started"; while (1) { struct _pulse msg; @@ -90,7 +84,7 @@ void QQnxScreenEventThread::run() qWarning() << "MsgReceive error" << strerror(errno); } - qScreenEventThreadDebug("screen event thread stopped"); + qCDebug(lcQpaScreenEvents) << "Screen event thread stopped"; } void QQnxScreenEventThread::armEventsPending(int count) @@ -134,10 +128,10 @@ void QQnxScreenEventThread::shutdown() { MsgSendPulse(m_connectionId, SIGEV_PULSE_PRIO_INHERIT, c_quitCode, 0); - qScreenEventThreadDebug("screen event thread shutdown begin"); + qCDebug(lcQpaScreenEvents) << "Screen event thread shutdown begin"; // block until thread terminates wait(); - qScreenEventThreadDebug("screen event thread shutdown end"); + qCDebug(lcQpaScreenEvents) << "Screen event thread shutdown end"; } diff --git a/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.cpp b/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.cpp index b4650de315..5eceacef95 100644 --- a/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.cpp +++ b/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.cpp @@ -18,14 +18,10 @@ #include <sys/types.h> #include <unistd.h> -#if defined(QQNXVIRTUALKEYBOARD_DEBUG) -#define qVirtualKeyboardDebug qDebug -#else -#define qVirtualKeyboardDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaQnxVirtualKeyboard, "qt.qpa.qnx.virtualkeyboard"); + const char *QQnxVirtualKeyboardPps::ms_PPSPath = "/pps/services/input/control"; const size_t QQnxVirtualKeyboardPps::ms_bufferSize = 2048; @@ -45,7 +41,7 @@ QQnxVirtualKeyboardPps::~QQnxVirtualKeyboardPps() void QQnxVirtualKeyboardPps::start() { - qVirtualKeyboardDebug("starting keyboard event processing"); + qCDebug(lcQpaQnxVirtualKeyboard) << "Starting keyboard event processing"; if (!connect()) return; } @@ -90,8 +86,8 @@ bool QQnxVirtualKeyboardPps::connect() m_fd = ::open(ms_PPSPath, O_RDWR); if (m_fd == -1) { - qVirtualKeyboardDebug() << "Unable to open" << ms_PPSPath - << ':' << strerror(errno); + qCDebug(lcQpaQnxVirtualKeyboard) << "Unable to open" << ms_PPSPath + << ':' << strerror(errno); close(); return false; } @@ -128,7 +124,7 @@ void QQnxVirtualKeyboardPps::ppsDataReady() { qint64 nread = qt_safe_read(m_fd, m_buffer, ms_bufferSize - 1); - qVirtualKeyboardDebug("keyboardMessage size: %lld", nread); + qCDebug(lcQpaQnxVirtualKeyboard, "keyboardMessage size: %lld", nread); if (nread < 0){ connect(); // reconnect return; @@ -167,7 +163,7 @@ void QQnxVirtualKeyboardPps::ppsDataReady() else if (strcmp(value, "info") == 0) handleKeyboardInfoMessage(); else if (strcmp(value, "connect") == 0) - qVirtualKeyboardDebug("Unhandled command 'connect'"); + qCDebug(lcQpaQnxVirtualKeyboard, "Unhandled command 'connect'"); else qCritical("QQnxVirtualKeyboard: Unexpected keyboard PPS msg value: %s", value ? value : "[null]"); } else if (pps_decoder_get_string(m_decoder, "res", &value) == PPS_DECODER_OK) { @@ -194,12 +190,12 @@ void QQnxVirtualKeyboardPps::handleKeyboardInfoMessage() } setHeight(newHeight); - qVirtualKeyboardDebug("size=%d", newHeight); + qCDebug(lcQpaQnxVirtualKeyboard, "size=%d", newHeight); } bool QQnxVirtualKeyboardPps::showKeyboard() { - qVirtualKeyboardDebug(); + qCDebug(lcQpaQnxVirtualKeyboard) << Q_FUNC_INFO; if (!prepareToSend()) return false; @@ -221,7 +217,7 @@ bool QQnxVirtualKeyboardPps::showKeyboard() bool QQnxVirtualKeyboardPps::hideKeyboard() { - qVirtualKeyboardDebug(); + qCDebug(lcQpaQnxVirtualKeyboard) << Q_FUNC_INFO; if (!prepareToSend()) return false; diff --git a/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.h b/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.h index 88af284db3..8f1d390760 100644 --- a/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.h +++ b/src/plugins/platforms/qnx/qqnxvirtualkeyboardpps.h @@ -5,11 +5,13 @@ #define VIRTUALKEYBOARDPPS_H #include "qqnxabstractvirtualkeyboard.h" +#include <QtCore/QLoggingCategory> #include <sys/pps.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnxVirtualKeyboard); class QSocketNotifier; diff --git a/src/plugins/platforms/qnx/qqnxwindow.cpp b/src/plugins/platforms/qnx/qqnxwindow.cpp index 5310ff3c65..3384932a8b 100644 --- a/src/plugins/platforms/qnx/qqnxwindow.cpp +++ b/src/plugins/platforms/qnx/qqnxwindow.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2011 - 2013 BlackBerry Limited. All rights reserved. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qqnxglobal.h" #include "qqnxwindow.h" @@ -19,14 +21,10 @@ #include <errno.h> -#if defined(QQNXWINDOW_DEBUG) -#define qWindowDebug qDebug -#else -#define qWindowDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window"); + #define DECLARE_DEBUG_VAR(variable) \ static bool debug_ ## variable() \ { static bool value = qgetenv("QNX_SCREEN_DEBUG").contains(QT_STRINGIFY(variable)); return value; } @@ -123,7 +121,7 @@ QQnxWindow::QQnxWindow(QWindow *window, screen_context_t context, bool needRootW m_windowState(Qt::WindowNoState), m_firstActivateHandled(false) { - qWindowDebug() << "window =" << window << ", size =" << window->size(); + qCDebug(lcQpaWindow) << "window =" << window << ", size =" << window->size(); QQnxScreen *platformScreen = static_cast<QQnxScreen *>(window->screen()->handle()); @@ -193,13 +191,13 @@ QQnxWindow::QQnxWindow(QWindow *window, screen_context_t context, bool needRootW bool ok = false; int pipeline = pipelineValue.toInt(&ok); if (ok) { - qWindowDebug() << "Set pipeline value to" << pipeline; + qCDebug(lcQpaWindow) << "Set pipeline value to" << pipeline; Q_SCREEN_CHECKERROR( screen_set_window_property_iv(m_window, SCREEN_PROPERTY_PIPELINE, &pipeline), "Failed to set window pipeline"); } else { - qWindowDebug() << "Invalid pipeline value:" << pipelineValue; + qCDebug(lcQpaWindow) << "Invalid pipeline value:" << pipelineValue; } } @@ -229,7 +227,7 @@ QQnxWindow::QQnxWindow(QWindow *window, screen_context_t context, bool needRootW if (debug > 0) { Q_SCREEN_CHECKERROR(screen_set_window_property_iv(nativeHandle(), SCREEN_PROPERTY_DEBUG, &debug), "Could not set SCREEN_PROPERTY_DEBUG"); - qWindowDebug() << "window SCREEN_PROPERTY_DEBUG= " << debug; + qCDebug(lcQpaWindow) << "window SCREEN_PROPERTY_DEBUG= " << debug; } } @@ -246,7 +244,7 @@ QQnxWindow::QQnxWindow(QWindow *window, screen_context_t context, screen_window_ , m_parentGroupName(256, 0) , m_isTopLevel(false) { - qWindowDebug() << "window =" << window << ", size =" << window->size(); + qCDebug(lcQpaWindow) << "window =" << window << ", size =" << window->size(); collectWindowGroup(); @@ -267,7 +265,7 @@ QQnxWindow::QQnxWindow(QWindow *window, screen_context_t context, screen_window_ QQnxWindow::~QQnxWindow() { - qWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << "window =" << window(); // Qt should have already deleted the children before deleting the parent. Q_ASSERT(m_childWindows.size() == 0); @@ -303,9 +301,9 @@ void QQnxWindow::setGeometry(const QRect &rect) void QQnxWindow::setGeometryHelper(const QRect &rect) { - qWindowDebug() << "window =" << window() - << ", (" << rect.x() << "," << rect.y() - << "," << rect.width() << "," << rect.height() << ")"; + qCDebug(lcQpaWindow) << "window =" << window() + << ", (" << rect.x() << "," << rect.y() + << "," << rect.width() << "," << rect.height() << ")"; // Call base class method QPlatformWindow::setGeometry(rect); @@ -333,7 +331,7 @@ void QQnxWindow::setGeometryHelper(const QRect &rect) void QQnxWindow::setVisible(bool visible) { - qWindowDebug() << "window =" << window() << "visible =" << visible; + qCDebug(lcQpaWindow) << "window =" << window() << "visible =" << visible; if (m_visible == visible || window()->type() == Qt::Desktop) return; @@ -372,7 +370,7 @@ void QQnxWindow::setVisible(bool visible) void QQnxWindow::updateVisibility(bool parentVisible) { - qWindowDebug() << "parentVisible =" << parentVisible << "window =" << window(); + qCDebug(lcQpaWindow) << "parentVisible =" << parentVisible << "window =" << window(); // Set window visibility int val = (m_visible && parentVisible) ? 1 : 0; Q_SCREEN_CHECKERROR(screen_set_window_property_iv(m_window, SCREEN_PROPERTY_VISIBLE, &val), @@ -384,7 +382,7 @@ void QQnxWindow::updateVisibility(bool parentVisible) void QQnxWindow::setOpacity(qreal level) { - qWindowDebug() << "window =" << window() << "opacity =" << level; + qCDebug(lcQpaWindow) << "window =" << window() << "opacity =" << level; // Set window global alpha int val = (int)(level * 255); Q_SCREEN_CHECKERROR(screen_set_window_property_iv(m_window, SCREEN_PROPERTY_GLOBAL_ALPHA, &val), @@ -395,7 +393,7 @@ void QQnxWindow::setOpacity(qreal level) void QQnxWindow::setExposed(bool exposed) { - qWindowDebug() << "window =" << window() << "expose =" << exposed; + qCDebug(lcQpaWindow) << "window =" << window() << "expose =" << exposed; if (m_exposed != exposed) { m_exposed = exposed; @@ -410,7 +408,7 @@ bool QQnxWindow::isExposed() const void QQnxWindow::setBufferSize(const QSize &size) { - qWindowDebug() << "window =" << window() << "size =" << size; + qCDebug(lcQpaWindow) << "window =" << window() << "size =" << size; // libscreen fails when creating empty buffers const QSize nonEmptySize = size.isEmpty() ? QSize(1, 1) : size; @@ -477,7 +475,7 @@ void QQnxWindow::setBufferSize(const QSize &size) void QQnxWindow::setScreen(QQnxScreen *platformScreen) { - qWindowDebug() << "window =" << window() << "platformScreen =" << platformScreen; + qCDebug(lcQpaWindow) << "window =" << window() << "platformScreen =" << platformScreen; if (platformScreen == 0) { // The screen has been destroyed m_screen = 0; @@ -491,7 +489,7 @@ void QQnxWindow::setScreen(QQnxScreen *platformScreen) return; if (m_screen) { - qWindowDebug("Moving window to different screen"); + qCDebug(lcQpaWindow) << "Moving window to different screen"; m_screen->removeWindow(this); if ((QQnxIntegration::instance()->options() & QQnxIntegration::RootWindow)) { @@ -522,7 +520,7 @@ void QQnxWindow::setScreen(QQnxScreen *platformScreen) void QQnxWindow::removeFromParent() { - qWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window =" << window(); // Remove from old Hierarchy position if (m_parentWindow) { if (Q_UNLIKELY(!m_parentWindow->m_childWindows.removeAll(this))) @@ -536,7 +534,7 @@ void QQnxWindow::removeFromParent() void QQnxWindow::setParent(const QPlatformWindow *window) { - qWindowDebug() << "window =" << this->window() << "platformWindow =" << window; + qCDebug(lcQpaWindow) << "window =" << this->window() << "platformWindow =" << window; // Cast away the const, we need to modify the hierarchy. QQnxWindow* const newParent = static_cast<QQnxWindow*>(const_cast<QPlatformWindow*>(window)); @@ -568,7 +566,7 @@ void QQnxWindow::setParent(const QPlatformWindow *window) void QQnxWindow::raise() { - qWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window =" << window(); if (m_parentWindow) { m_parentWindow->m_childWindows.removeAll(this); @@ -582,7 +580,7 @@ void QQnxWindow::raise() void QQnxWindow::lower() { - qWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window =" << window(); if (m_parentWindow) { m_parentWindow->m_childWindows.removeAll(this); @@ -696,7 +694,7 @@ void QQnxWindow::setFocus(screen_window_t newFocusWindow) void QQnxWindow::setWindowState(Qt::WindowStates state) { - qWindowDebug() << "state =" << state; + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "state =" << state; // Prevent two calls with Qt::WindowFullScreen from changing m_unmaximizedGeometry if (m_windowState == state) @@ -711,7 +709,7 @@ void QQnxWindow::setWindowState(Qt::WindowStates state) void QQnxWindow::propagateSizeHints() { // nothing to do; silence base class warning - qWindowDebug("ignored"); + // qWindowDebug("ignored"); } QPlatformScreen *QQnxWindow::screen() const @@ -740,7 +738,7 @@ void QQnxWindow::minimize() void QQnxWindow::setRotation(int rotation) { - qWindowDebug() << "angle =" << rotation; + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "angle =" << rotation; Q_SCREEN_CHECKERROR( screen_set_window_property_iv(m_window, SCREEN_PROPERTY_ROTATION, &rotation), "Failed to set window rotation"); @@ -816,7 +814,7 @@ void QQnxWindow::joinWindowGroup(const QByteArray &groupName) { bool changed = false; - qWindowDebug() << "group:" << groupName; + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "group:" << groupName; // screen has this annoying habit of generating a CLOSE/CREATE when the owner context of // the parent group moves a foreign window to another group that it also owns. The diff --git a/src/plugins/platforms/qnx/qqnxwindow.h b/src/plugins/platforms/qnx/qqnxwindow.h index d302f22415..013ea342e4 100644 --- a/src/plugins/platforms/qnx/qqnxwindow.h +++ b/src/plugins/platforms/qnx/qqnxwindow.h @@ -8,6 +8,7 @@ #include "qqnxabstractcover.h" #include <QtCore/QScopedPointer> +#include <QtCore/QLoggingCategory> #if !defined(QT_NO_OPENGL) #include <EGL/egl.h> @@ -17,6 +18,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow); + // all surfaces double buffered #define MAX_BUFFER_COUNT 2 diff --git a/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt b/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt index 98fb6d39b7..406487f1e9 100644 --- a/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt +++ b/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt @@ -1,9 +1,12 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_find_package(WrapFreetype PROVIDED_TARGETS WrapFreetype::WrapFreetype) qt_internal_add_plugin(QVkKhrDisplayIntegrationPlugin OUTPUT_NAME qvkkhrdisplay PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES vkkhrdisplay + DEFAULT_IF "vkkhrdisplay" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qvkkhrdisplayintegration.cpp qvkkhrdisplayintegration.h diff --git a/src/plugins/platforms/vkkhrdisplay/main.cpp b/src/plugins/platforms/vkkhrdisplay/main.cpp index c11305607b..aa2dc3abf5 100644 --- a/src/plugins/platforms/vkkhrdisplay/main.cpp +++ b/src/plugins/platforms/vkkhrdisplay/main.cpp @@ -21,7 +21,7 @@ QPlatformIntegration *QVkKhrDisplayIntegrationPlugin::create(const QString &syst if (!system.compare("vkkhrdisplay"_L1, Qt::CaseInsensitive)) return new QVkKhrDisplayIntegration(paramList); - return 0; + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayintegration.cpp b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayintegration.cpp index 411cda4af8..502c2518f2 100644 --- a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayintegration.cpp +++ b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayintegration.cpp @@ -219,7 +219,11 @@ QPlatformWindow *QVkKhrDisplayIntegration::createPlatformWindow(QWindow *window) { if (window->surfaceType() != QSurface::VulkanSurface) { qWarning("vkkhrdisplay platform plugin only supports QWindow with surfaceType == VulkanSurface"); - return nullptr; + // Assume VulkanSurface, better than crashing. Consider e.g. an autotest + // creating a default QWindow just to have something to be used with + // QRhi's Null backend. Continuing to set up a Vulkan window (even + // though the request was Raster or something) is better than failing to + // create a platform window, and may even be sufficient in some cases. } QVkKhrDisplayWindow *w = new QVkKhrDisplayWindow(window); diff --git a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp index 96c7fbfbdd..2e8d60209e 100644 --- a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp +++ b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp @@ -46,6 +46,9 @@ void QVkKhrDisplayVulkanInstance::createOrAdoptInstance() m_enumeratePhysicalDevices = (PFN_vkEnumeratePhysicalDevices) m_vkGetInstanceProcAddr(m_vkInst, "vkEnumeratePhysicalDevices"); + m_getPhysicalDeviceSurfaceSupportKHR = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>( + m_vkGetInstanceProcAddr(m_vkInst, "vkGetPhysicalDeviceSurfaceSupportKHR")); + // Use for first physical device, unless overridden by QT_VK_PHYSICAL_DEVICE_INDEX. // This behavior matches what the Vulkan backend of QRhi would do. @@ -81,10 +84,14 @@ bool QVkKhrDisplayVulkanInstance::supportsPresent(VkPhysicalDevice physicalDevic uint32_t queueFamilyIndex, QWindow *window) { - Q_UNUSED(physicalDevice); - Q_UNUSED(queueFamilyIndex); - Q_UNUSED(window); - return true; + if (!m_getPhysicalDeviceSurfaceSupportKHR) + return true; + + VkSurfaceKHR surface = QVulkanInstance::surfaceForWindow(window); + VkBool32 supported = false; + m_getPhysicalDeviceSurfaceSupportKHR(physicalDevice, queueFamilyIndex, surface, &supported); + + return supported; } bool QVkKhrDisplayVulkanInstance::chooseDisplay() @@ -136,7 +143,7 @@ bool QVkKhrDisplayVulkanInstance::chooseDisplay() j, (void *) mode.displayMode, mode.parameters.visibleRegion.width, mode.parameters.visibleRegion.height, mode.parameters.refreshRate); - if (j == wantedModeIndex) { + if (j == wantedModeIndex && i == wantedDisplayIndex) { m_displayMode = mode.displayMode; m_width = mode.parameters.visibleRegion.width; m_height = mode.parameters.visibleRegion.height; diff --git a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.h b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.h index a47878e344..bf99dc037f 100644 --- a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.h +++ b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.h @@ -45,6 +45,7 @@ private: QVulkanInstance *m_instance; VkPhysicalDevice m_physDev = VK_NULL_HANDLE; PFN_vkEnumeratePhysicalDevices m_enumeratePhysicalDevices = nullptr; + PFN_vkGetPhysicalDeviceSurfaceSupportKHR m_getPhysicalDeviceSurfaceSupportKHR = nullptr; #if VK_KHR_display PFN_vkGetPhysicalDeviceDisplayPropertiesKHR m_getPhysicalDeviceDisplayPropertiesKHR = nullptr; PFN_vkGetDisplayModePropertiesKHR m_getDisplayModePropertiesKHR = nullptr; diff --git a/src/plugins/platforms/vnc/CMakeLists.txt b/src/plugins/platforms/vnc/CMakeLists.txt index ca9445207b..34370807ae 100644 --- a/src/plugins/platforms/vnc/CMakeLists.txt +++ b/src/plugins/platforms/vnc/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from vnc.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QVncIntegrationPlugin Plugin: @@ -7,7 +8,7 @@ qt_internal_add_plugin(QVncIntegrationPlugin OUTPUT_NAME qvnc PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES vnc # special case + DEFAULT_IF "vnc" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qvnc.cpp qvnc_p.h @@ -25,9 +26,6 @@ qt_internal_add_plugin(QVncIntegrationPlugin Qt::Network ) -#### Keys ignored in scope 1:.:.:vnc.pro:<TRUE>: -# OTHER_FILES = "vnc.json" - ## Scopes: ##################################################################### @@ -35,6 +33,3 @@ qt_internal_extend_target(QVncIntegrationPlugin CONDITION TARGET Qt::InputSuppor LIBRARIES Qt::InputSupportPrivate ) - -#### Keys ignored in scope 3:.:.:vnc.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platforms/vnc/qvnc.cpp b/src/plugins/platforms/vnc/qvnc.cpp index d09c6789b7..99e80e5801 100644 --- a/src/plugins/platforms/vnc/qvnc.cpp +++ b/src/plugins/platforms/vnc/qvnc.cpp @@ -567,7 +567,7 @@ void QVncClientCursor::changeCursor(QCursor *widgetCursor, QWindow *window) cursor = *platformImage.image(); hotspot = platformImage.hotspot(); } - for (auto client : qAsConst(clients)) + for (auto client : std::as_const(clients)) client->setDirtyCursor(); } @@ -583,7 +583,7 @@ void QVncClientCursor::addClient(QVncClient *client) uint QVncClientCursor::removeClient(QVncClient *client) { clients.removeOne(client); - return clients.count(); + return clients.size(); } #endif // QT_CONFIG(cursor) @@ -613,7 +613,7 @@ QVncServer::~QVncServer() void QVncServer::setDirty() { - for (auto client : qAsConst(clients)) + for (auto client : std::as_const(clients)) client->setDirty(qvnc_screen->dirtyRegion); qvnc_screen->clearDirty(); @@ -635,11 +635,10 @@ void QVncServer::newConnection() void QVncServer::discardClient(QVncClient *client) { clients.removeOne(client); + qvnc_screen->disableClientCursor(client); client->deleteLater(); - if (clients.isEmpty()) { - qvnc_screen->disableClientCursor(client); + if (clients.isEmpty()) qvnc_screen->setPowerState(QPlatformScreen::PowerStateOff); - } } inline QImage QVncServer::screenImage() const diff --git a/src/plugins/platforms/vnc/qvncscreen.cpp b/src/plugins/platforms/vnc/qvncscreen.cpp index eb8241138e..c87df20ef6 100644 --- a/src/plugins/platforms/vnc/qvncscreen.cpp +++ b/src/plugins/platforms/vnc/qvncscreen.cpp @@ -41,7 +41,7 @@ bool QVncScreen::initialize() mDepth = 32; mPhysicalSize = QSizeF(mGeometry.width()/96.*25.4, mGeometry.height()/96.*25.4); - for (const QString &arg : qAsConst(mArgs)) { + for (const QString &arg : std::as_const(mArgs)) { QRegularExpressionMatch match; if (arg.contains(mmSizeRx, &match)) { mPhysicalSize = QSizeF(match.captured("width").toDouble(), match.captured("height").toDouble()); @@ -113,9 +113,10 @@ void QVncScreen::disableClientCursor(QVncClient *client) if (clientCount == 0) { delete clientCursor; clientCursor = nullptr; - } - mCursor = new QFbCursor(this); + if (mCursor == nullptr) + mCursor = new QFbCursor(this); + } #else Q_UNUSED(client); #endif diff --git a/src/plugins/platforms/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index efd468c9be..775946aaf9 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from wasm.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QWasmIntegrationPlugin Plugin: @@ -6,27 +7,34 @@ qt_internal_add_plugin(QWasmIntegrationPlugin OUTPUT_NAME qwasm - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES wasm # special case + DEFAULT_IF "wasm" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms - STATIC SOURCES main.cpp + qwasmaccessibility.cpp qwasmaccessibility.h + qwasmbase64iconstore.cpp qwasmbase64iconstore.h qwasmclipboard.cpp qwasmclipboard.h qwasmcompositor.cpp qwasmcompositor.h + qwasmcssstyle.cpp qwasmcssstyle.h qwasmcursor.cpp qwasmcursor.h + qwasmdom.cpp qwasmdom.h + qwasmevent.cpp qwasmevent.h qwasmeventdispatcher.cpp qwasmeventdispatcher.h - qwasmeventtranslator.cpp qwasmeventtranslator.h qwasmfontdatabase.cpp qwasmfontdatabase.h qwasmintegration.cpp qwasmintegration.h + qwasmkeytranslator.cpp qwasmkeytranslator.h qwasmoffscreensurface.cpp qwasmoffscreensurface.h qwasmopenglcontext.cpp qwasmopenglcontext.h + qwasmplatform.cpp qwasmplatform.h qwasmscreen.cpp qwasmscreen.h qwasmservices.cpp qwasmservices.h - qwasmstring.cpp qwasmstring.h - qwasmstylepixmaps_p.h qwasmtheme.cpp qwasmtheme.h qwasmwindow.cpp qwasmwindow.h + qwasmwindowclientarea.cpp qwasmwindowclientarea.h + qwasmwindowtreenode.cpp qwasmwindowtreenode.h + qwasmwindownonclientarea.cpp qwasmwindownonclientarea.h qwasminputcontext.cpp qwasminputcontext.h + qwasmwindowstack.cpp qwasmwindowstack.h qwasmdrag.cpp qwasmdrag.h DEFINES QT_EGL_NO_X11 @@ -40,7 +48,6 @@ qt_internal_add_plugin(QWasmIntegrationPlugin # Resources: set(wasmfonts_resource_files - "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/Vera.ttf" "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSans.ttf" "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSansMono.ttf" ) @@ -53,6 +60,7 @@ qt_internal_add_resource(QWasmIntegrationPlugin "wasmfonts" FILES ${wasmfonts_resource_files} ) + qt_internal_extend_target(QWasmIntegrationPlugin CONDITION QT_FEATURE_opengl SOURCES qwasmbackingstore.cpp qwasmbackingstore.h @@ -61,13 +69,28 @@ qt_internal_extend_target(QWasmIntegrationPlugin CONDITION QT_FEATURE_opengl Qt::OpenGLPrivate ) -#### Keys ignored in scope 4:.:.:wasm.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: # PLUGIN_EXTENDS = "-" set(wasm_support_files wasm_shell.html qtloader.js - qtlogo.svg + resources/qtlogo.svg +) + +set(wasmwindow_resource_files + "resources/maximize.svg" + "resources/qtlogo.svg" + "resources/restore.svg" + "resources/x.svg" +) + +qt_internal_add_resource(QWasmIntegrationPlugin "wasmwindow" + PREFIX + "/wasm-window" + BASE + "resources" + FILES + ${wasmwindow_resource_files} ) qt_path_join(destination ${QT_INSTALL_DIR} "plugins/platforms") diff --git a/src/plugins/platforms/wasm/main.cpp b/src/plugins/platforms/wasm/main.cpp index 1b430829ad..f32ef5aab8 100644 --- a/src/plugins/platforms/wasm/main.cpp +++ b/src/plugins/platforms/wasm/main.cpp @@ -6,6 +6,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + class QWasmIntegrationPlugin : public QPlatformIntegrationPlugin { Q_OBJECT @@ -17,7 +19,7 @@ public: QPlatformIntegration *QWasmIntegrationPlugin::create(const QString& system, const QStringList& paramList) { Q_UNUSED(paramList); - if (!system.compare(QStringLiteral("wasm"), Qt::CaseInsensitive)) + if (!system.compare("wasm"_L1, Qt::CaseInsensitive)) return new QWasmIntegration; return nullptr; diff --git a/src/plugins/platforms/wasm/qtloader.js b/src/plugins/platforms/wasm/qtloader.js index f51a574078..8027dd8fa9 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -1,581 +1,301 @@ -// Copyright (C) 2018 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only -// QtLoader provides javascript API for managing Qt application modules. -// -// QtLoader provides API on top of Emscripten which supports common lifecycle -// tasks such as displaying placeholder content while the module downloads, -// handing application exits, and checking for browser wasm support. -// -// There are two usage modes: -// * Managed: QtLoader owns and manages the HTML display elements like -// the loader and canvas. -// * External: The embedding HTML page owns the display elements. QtLoader -// provides event callbacks which the page reacts to. -// -// Managed mode usage: -// -// var config = { -// containerElements : [$("container-id")]; -// } -// var qtLoader = QtLoader(config); -// qtLoader.loadEmscriptenModule("applicationName"); -// -// External mode.usage: -// -// var config = { -// canvasElements : [$("canvas-id")], -// showLoader: function() { -// loader.style.display = 'block' -// canvas.style.display = 'hidden' -// }, -// showCanvas: function() { -// loader.style.display = 'hidden' -// canvas.style.display = 'block' -// return canvas; -// } -// } -// var qtLoader = QtLoader(config); -// qtLoader.loadEmscriptenModule("applicationName"); -// -// Config keys -// -// containerElements : [container-element, ...] -// One or more HTML elements. QtLoader will display loader elements -// on these while loading the application, and replace the loader with a -// canvas on load complete. -// canvasElements : [canvas-element, ...] -// One or more canvas elements. -// showLoader : function(status, containerElement) -// Optional loading element constructor function. Implement to create -// a custom loading screen. This function may be called multiple times, -// while preparing the application binary. "status" is a string -// containing the loading sub-status, and may be either "Downloading", -// or "Compiling". The browser may be using streaming compilation, in -// which case the wasm module is compiled during downloading and the -// there is no separate compile step. -// showCanvas : function(containerElement) -// Optional canvas constructor function. Implement to create custom -// canvas elements. -// showExit : function(crashed, exitCode, containerElement) -// Optional exited element constructor function. -// showError : function(crashed, exitCode, containerElement) -// Optional error element constructor function. -// statusChanged : function(newStatus) -// Optional callback called when the status of the app has changed -// -// path : <string> -// Prefix path for wasm file, realative to the loading HMTL file. -// restartMode : "DoNotRestart", "RestartOnExit", "RestartOnCrash" -// Controls whether the application should be reloaded on exits. The default is "DoNotRestart" -// restartType : "RestartModule", "ReloadPage" -// restartLimit : <int> -// Restart attempts limit. The default is 10. -// stdoutEnabled : <bool> -// stderrEnabled : <bool> -// environment : <object> -// key-value environment variable pairs. -// -// QtLoader object API -// -// webAssemblySupported : bool -// webGLSupported : bool -// canLoadQt : bool -// Reports if WebAssembly and WebGL are supported. These are requirements for -// running Qt applications. -// loadEmscriptenModule(applicationName) -// Loads the application from the given emscripten javascript module file and wasm file -// status -// One of "Created", "Loading", "Running", "Exited". -// crashed -// Set to true if there was an unclean exit. -// exitCode -// main()/emscripten_force_exit() return code. Valid on status change to -// "Exited", iff crashed is false. -// exitText -// Abort/exit message. -// addCanvasElement -// Add canvas at run-time. Adds a corresponding QScreen, -// removeCanvasElement -// Remove canvas at run-time. Removes the corresponding QScreen. -// resizeCanvasElement -// Signals to the application that a canvas has been resized. -// setFontDpi -// Sets the logical font dpi for the application. -// module -// Returns the Emscripten module object, or undefined if the module -// has not been created yet. Note that the module object becomes available -// at the very end of the loading sequence, _after_ the transition from -// Loading to Running occurs. - - -function QtLoader(config) +/** + * Loads the instance of a WASM module. + * + * @param config May contain any key normally accepted by emscripten and the 'qt' extra key, with + * the following sub-keys: + * - environment: { [name:string] : string } + * environment variables set on the instance + * - onExit: (exitStatus: { text: string, code?: number, crashed: bool }) => void + * called when the application has exited for any reason. There are two cases: + * aborted: crashed is true, text contains an error message. + * exited: crashed is false, code contians the exit code. + * + * Note that by default Emscripten does not exit when main() returns. This behavior + * is controlled by the EXIT_RUNTIME linker flag; set "-s EXIT_RUNTIME=1" to make + * Emscripten tear down the runtime and exit when main() returns. + * + * - containerElements: HTMLDivElement[] + * Array of host elements for Qt screens. Each of these elements is mapped to a QScreen on + * launch. + * - fontDpi: number + * Specifies font DPI for the instance + * - onLoaded: () => void + * Called when the module has loaded, at the point in time where any loading placeholder + * should be hidden and the application window should be shown. + * - entryFunction: (emscriptenConfig: object) => Promise<EmscriptenModule> + * Qt always uses emscripten's MODULARIZE option. This is the MODULARIZE entry function. + * - module: Promise<WebAssembly.Module> + * The module to create the instance from (optional). Specifying the module allows optimizing + * use cases where several instances are created from a single WebAssembly source. + * - qtdir: string + * Path to Qt installation. This path will be used for loading Qt shared libraries and plugins. + * The path is set to 'qt' by default, and is relative to the path of the web page's html file. + * This property is not in use when static linking is used, since this build mode includes all + * libraries and plugins in the wasm file. + * - preload: [string]: Array of file paths to json-encoded files which specifying which files to preload. + * The preloaded files will be downloaded at application startup and copied to the in-memory file + * system provided by Emscripten. + * + * Each json file must contain an array of source, destination objects: + * [ + * { + * "source": "path/to/source", + * "destination": "/path/to/destination" + * }, + * ... + * ] + * The source path is relative to the html file path. The destination path must be + * an absolute path. + * + * $QTDIR may be used as a placeholder for the "qtdir" configuration property (see @qtdir), for instance: + * "source": "$QTDIR/plugins/imageformats/libqjpeg.so" + * - localFonts.requestPermission: bool + * Whether Qt should request for local fonts access permission on startup (default false). + * - localFonts.familiesCollection string + * Specifies a collection of local fonts to load. Possible values are: + * "NoFontFamilies" : Don't load any font families + * "DefaultFontFamilies" : A subset of available font families; currently the "web-safe" fonts (default). + * "AllFontFamilies" : All local font families (not reccomended) + * - localFonts.extraFamilies: [string] + * Adds additional font families to be loaded at startup. + * + * @return Promise<instance: EmscriptenModule> + * The promise is resolved when the module has been instantiated and its main function has been + * called. + * + * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten for + * EmscriptenModule + */ +async function qtLoad(config) { - // The Emscripten module and module configuration object. The module - // object is created in completeLoadEmscriptenModule(). - self.module = undefined; - self.moduleConfig = {}; - - // Qt properties. These are propagated to the Emscripten module after - // it has been created. - self.qtContainerElements = undefined; - self.qtFontDpi = 96; - - function webAssemblySupported() { - return typeof WebAssembly !== "undefined" - } - - function webGLSupported() { - // We expect that WebGL is supported if WebAssembly is; however - // the GPU may be blacklisted. - try { - var canvas = document.createElement("canvas"); - return !!(window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))); - } catch (e) { - return false; + const throwIfEnvUsedButNotExported = (instance, config) => + { + const environment = config.environment; + if (!environment || Object.keys(environment).length === 0) + return; + const isEnvExported = typeof instance.ENV === 'object'; + if (!isEnvExported) + throw new Error('ENV must be exported if environment variables are passed'); + }; + + if (typeof config !== 'object') + throw new Error('config is required, expected an object'); + if (typeof config.qt !== 'object') + throw new Error('config.qt is required, expected an object'); + if (typeof config.qt.entryFunction !== 'function') + throw new Error('config.qt.entryFunction is required, expected a function'); + + config.qt.qtdir ??= 'qt'; + config.qt.preload ??= []; + + config.qtContainerElements = config.qt.containerElements; + delete config.qt.containerElements; + config.qtFontDpi = config.qt.fontDpi; + delete config.qt.fontDpi; + + // Make Emscripten not call main(); this gives us more control over + // the startup sequence. + const originalNoInitialRun = config.noInitialRun; + const originalArguments = config.arguments; + config.noInitialRun = true; + + // Used for rejecting a failed load's promise where emscripten itself does not allow it, + // like in instantiateWasm below. This allows us to throw in case of a load error instead of + // hanging on a promise to entry function, which emscripten unfortunately does. + let circuitBreakerReject; + const circuitBreaker = new Promise((_, reject) => { circuitBreakerReject = reject; }); + + // If module async getter is present, use it so that module reuse is possible. + if (config.qt.module) { + config.instantiateWasm = async (imports, successCallback) => + { + try { + const module = await config.qt.module; + successCallback( + await WebAssembly.instantiate(module, imports), module); + } catch (e) { + circuitBreakerReject(e); + } } } - - function canLoadQt() { - // The current Qt implementation requires WebAssembly (asm.js is not in use), - // and also WebGL (there is no raster fallback). - return webAssemblySupported() && webGLSupported(); - } - - function removeChildren(element) { - while (element.firstChild) element.removeChild(element.firstChild); - } - - function createCanvas() { - var canvas = document.createElement("canvas"); - canvas.className = "QtCanvas"; - canvas.style.height = "100%"; - canvas.style.width = "100%"; - - // Set contentEditable in order to enable clipboard events; hide the resulting focus frame. - canvas.contentEditable = true; - canvas.style.outline = "0px solid transparent"; - canvas.style.caretColor = "transparent"; - canvas.style.cursor = "default"; - - return canvas; - } - - // Set default state handler functions and create canvases if needed - if (config.containerElements !== undefined) { - - config.canvasElements = config.containerElements.map(createCanvas); - - config.showError = config.showError || function(errorText, container) { - removeChildren(container); - var errorTextElement = document.createElement("text"); - errorTextElement.className = "QtError" - errorTextElement.innerHTML = errorText; - return errorTextElement; + const fetchJsonHelper = async path => (await fetch(path)).json(); + const filesToPreload = (await Promise.all(config.qt.preload.map(fetchJsonHelper))).flat(); + const qtPreRun = (instance) => { + // Copy qt.environment to instance.ENV + throwIfEnvUsedButNotExported(instance, config); + for (const [name, value] of Object.entries(config.qt.environment ?? {})) + instance.ENV[name] = value; + + // Preload files from qt.preload + const makeDirs = (FS, filePath) => { + const parts = filePath.split("/"); + let path = "/"; + for (let i = 0; i < parts.length - 1; ++i) { + const part = parts[i]; + if (part == "") + continue; + path += part + "/"; + try { + FS.mkdir(path); + } catch (error) { + const EEXIST = 20; + if (error.errno != EEXIST) + throw error; + } + } } - config.showLoader = config.showLoader || function(loadingState, container) { - removeChildren(container); - var loadingText = document.createElement("text"); - loadingText.className = "QtLoading" - loadingText.innerHTML = '<p><center> ${loadingState}...</center><p>'; - return loadingText; - }; - - config.showCanvas = config.showCanvas || function(canvas, container) { - removeChildren(container); + const extractFilenameAndDir = (path) => { + const parts = path.split('/'); + const filename = parts.pop(); + const dir = parts.join('/'); + return { + filename: filename, + dir: dir + }; } - - config.showExit = config.showExit || function(crashed, exitCode, container) { - if (!crashed) - return undefined; - - removeChildren(container); - var fontSize = 54; - var crashSymbols = ["\u{1F615}", "\u{1F614}", "\u{1F644}", "\u{1F928}", "\u{1F62C}", - "\u{1F915}", "\u{2639}", "\u{1F62E}", "\u{1F61E}", "\u{1F633}"]; - var symbolIndex = Math.floor(Math.random() * crashSymbols.length); - var errorHtml = `<font size='${fontSize}'> ${crashSymbols[symbolIndex]} </font>` - var errorElement = document.createElement("text"); - errorElement.className = "QtExit" - errorElement.innerHTML = errorHtml; - return errorElement; + const preloadFile = (file) => { + makeDirs(instance.FS, file.destination); + const source = file.source.replace('$QTDIR', config.qt.qtdir); + const filenameAndDir = extractFilenameAndDir(file.destination); + instance.FS.createPreloadedFile(filenameAndDir.dir, filenameAndDir.filename, source, true, true); } + const isFsExported = typeof instance.FS === 'object'; + if (!isFsExported) + throw new Error('FS must be exported if preload is used'); + filesToPreload.forEach(preloadFile); } - config.restartMode = config.restartMode || "DoNotRestart"; - config.restartLimit = config.restartLimit || 10; - - if (config.stdoutEnabled === undefined) config.stdoutEnabled = true; - if (config.stderrEnabled === undefined) config.stderrEnabled = true; - - // Make sure config.path is defined and ends with "/" if needed - if (config.path === undefined) - config.path = ""; - if (config.path.length > 0 && !config.path.endsWith("/")) - config.path = config.path.concat("/"); - - if (config.environment === undefined) - config.environment = {}; - - var publicAPI = {}; - publicAPI.webAssemblySupported = webAssemblySupported(); - publicAPI.webGLSupported = webGLSupported(); - publicAPI.canLoadQt = canLoadQt(); - publicAPI.canLoadApplication = canLoadQt(); - publicAPI.status = undefined; - publicAPI.loadEmscriptenModule = loadEmscriptenModule; - publicAPI.addCanvasElement = addCanvasElement; - publicAPI.removeCanvasElement = removeCanvasElement; - publicAPI.resizeCanvasElement = resizeCanvasElement; - publicAPI.setFontDpi = setFontDpi; - publicAPI.fontDpi = fontDpi; - publicAPI.module = module; - - self.restartCount = 0; - - function handleError(error) { - self.error = error; - setStatus("Error"); - console.error(error); - } - - function fetchResource(filePath) { - var fullPath = config.path + filePath; - return fetch(fullPath).then(function(response) { - if (!response.ok) { - let err = response.status + " " + response.statusText + " " + response.url; - handleError(err); - return Promise.reject(err) - } else { - return response; - } - }); - } - - function fetchText(filePath) { - return fetchResource(filePath).then(function(response) { - return response.text(); - }); - } + if (!config.preRun) + config.preRun = []; + config.preRun.push(qtPreRun); - function fetchThenCompileWasm(response) { - return response.arrayBuffer().then(function(data) { - self.loaderSubState = "Compiling"; - setStatus("Loading") // trigger loaderSubState update - return WebAssembly.compile(data); - }); + const originalOnRuntimeInitialized = config.onRuntimeInitialized; + config.onRuntimeInitialized = () => { + originalOnRuntimeInitialized?.(); + config.qt.onLoaded?.(); } - function fetchCompileWasm(filePath) { - return fetchResource(filePath).then(function(response) { - if (typeof WebAssembly.compileStreaming !== "undefined") { - self.loaderSubState = "Downloading/Compiling"; - setStatus("Loading"); - return WebAssembly.compileStreaming(response).catch(function(error) { - // compileStreaming may/will fail if the server does not set the correct - // mime type (application/wasm) for the wasm file. Fall back to fetch, - // then compile in this case. - return fetchThenCompileWasm(response); - }); - } else { - // Fall back to fetch, then compile if compileStreaming is not supported - return fetchThenCompileWasm(response); - } - }); + const originalLocateFile = config.locateFile; + config.locateFile = filename => { + const originalLocatedFilename = originalLocateFile ? originalLocateFile(filename) : filename; + if (originalLocatedFilename.startsWith('libQt6')) + return `${config.qt.qtdir}/lib/${originalLocatedFilename}`; + return originalLocatedFilename; } - function loadEmscriptenModule(applicationName) { + let onExitCalled = false; + const originalOnExit = config.onExit; + config.onExit = code => { + originalOnExit?.(); - // Loading in qtloader.js goes through four steps: - // 1) Check prerequisites - // 2) Download resources - // 3) Configure the emscripten Module object - // 4) Start the emcripten runtime, after which emscripten takes over - - // Check for Wasm & WebGL support; set error and return before downloading resources if missing - if (!webAssemblySupported()) { - handleError("Error: WebAssembly is not supported"); - return; - } - if (!webGLSupported()) { - handleError("Error: WebGL is not supported"); - return; - } - - // Continue waiting if loadEmscriptenModule() is called again - if (publicAPI.status == "Loading") - return; - self.loaderSubState = "Downloading"; - setStatus("Loading"); - - // Fetch emscripten generated javascript runtime - var emscriptenModuleSource = undefined - var emscriptenModuleSourcePromise = fetchText(applicationName + ".js").then(function(source) { - emscriptenModuleSource = source - }); - - // Fetch and compile wasm module - var wasmModule = undefined; - var wasmModulePromise = fetchCompileWasm(applicationName + ".wasm").then(function (module) { - wasmModule = module; - }); - - // Wait for all resources ready - Promise.all([emscriptenModuleSourcePromise, wasmModulePromise]).then(function(){ - completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule); - }).catch(function(error) { - handleError(error); - // An error here is fatal, abort - self.moduleConfig.onAbort(error) - }); - } - - function completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule) { - - // The wasm binary has been compiled into a module during resource download, - // and is ready to be instantiated. Define the instantiateWasm callback which - // emscripten will call to create the instance. - self.moduleConfig.instantiateWasm = function(imports, successCallback) { - WebAssembly.instantiate(wasmModule, imports).then(function(instance) { - successCallback(instance, wasmModule); - }, function(error) { - handleError(error) + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + code, + crashed: false }); - return {}; - }; - - self.moduleConfig.locateFile = self.moduleConfig.locateFile || function(filename) { - return config.path + filename; - }; - - // Attach status callbacks - self.moduleConfig.setStatus = self.moduleConfig.setStatus || function(text) { - // Currently the only usable status update from this function - // is "Running..." - if (text.startsWith("Running")) - setStatus("Running"); - }; - self.moduleConfig.monitorRunDependencies = self.moduleConfig.monitorRunDependencies || function(left) { - // console.log("monitorRunDependencies " + left) - }; - - // Attach standard out/err callbacks. - self.moduleConfig.print = self.moduleConfig.print || function(text) { - if (config.stdoutEnabled) - console.log(text) - }; - self.moduleConfig.printErr = self.moduleConfig.printErr || function(text) { - if (config.stderrEnabled) - console.warn(text) - }; - - // Error handling: set status to "Exited", update crashed and - // exitCode according to exit type. - // Emscripten will typically call printErr with the error text - // as well. Note that emscripten may also throw exceptions from - // async callbacks. These should be handled in window.onerror by user code. - self.moduleConfig.onAbort = self.moduleConfig.onAbort || function(text) { - publicAPI.crashed = true; - publicAPI.exitText = text; - setStatus("Exited"); - }; - self.moduleConfig.quit = self.moduleConfig.quit || function(code, exception) { - - // Emscripten (and Qt) supports exiting from main() while keeping the app - // running. Don't transition into the "Exited" state for clean exits. - if (code == 0) - return; - - if (exception.name == "ExitStatus") { - // Clean exit with code - publicAPI.exitText = undefined - publicAPI.exitCode = code; - } else { - publicAPI.exitText = exception.toString(); - publicAPI.crashed = true; - } - setStatus("Exited"); - }; - - self.moduleConfig.preRun = self.moduleConfig.preRun || [] - self.moduleConfig.preRun.push(function(module) { - // Set environment variables - for (var [key, value] of Object.entries(config.environment)) { - module.ENV[key.toUpperCase()] = value; - } - // Propagate Qt module properties - module.qtContainerElements = self.qtContainerElements; - module.qtFontDpi = self.qtFontDpi; - }); - - self.moduleConfig.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'}); - - self.qtContainerElements = config.canvasElements; - - config.restart = function() { - - // Restart by reloading the page. This will wipe all state which means - // reload loops can't be prevented. - if (config.restartType == "ReloadPage") { - location.reload(); - } - - // Restart by readling the emscripten app module. - ++self.restartCount; - if (self.restartCount > config.restartLimit) { - handleError("Error: This application has crashed too many times and has been disabled. Reload the page to try again."); - return; - } - loadEmscriptenModule(applicationName); - }; - - publicAPI.exitCode = undefined; - publicAPI.exitText = undefined; - publicAPI.crashed = false; - - // Load the Emscripten application module. This is done by eval()'ing the - // javascript runtime generated by Emscripten, and then calling - // createQtAppInstance(), which was added to the global scope. - eval(emscriptenModuleSource); - createQtAppInstance(self.moduleConfig).then(function(module) { - self.module = module; - }); - } - - function setErrorContent() { - if (config.containerElements === undefined) { - if (config.showError !== undefined) - config.showError(self.error); - return; - } - - for (container of config.containerElements) { - var errorElement = config.showError(self.error, container); - container.appendChild(errorElement); } } - function setLoaderContent() { - if (config.containerElements === undefined) { - if (config.showLoader !== undefined) - config.showLoader(self.loaderSubState); - return; - } - - for (container of config.containerElements) { - var loaderElement = config.showLoader(self.loaderSubState, container); - container.appendChild(loaderElement); + const originalOnAbort = config.onAbort; + config.onAbort = text => + { + originalOnAbort?.(); + + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text, + crashed: true + }); } - } - - function setCanvasContent() { - if (config.containerElements === undefined) { - if (config.showCanvas !== undefined) - config.showCanvas(); + }; + + // Call app/emscripten module entry function. It may either come from the emscripten + // runtime script or be customized as needed. + let instance; + try { + instance = await Promise.race( + [circuitBreaker, config.qt.entryFunction(config)]); + + // Call main after creating the instance. We've opted into manually + // calling main() by setting noInitialRun in the config. Thie Works around + // issue where Emscripten suppresses all exceptions thrown during main. + if (!originalNoInitialRun) + instance.callMain(originalArguments); + } catch (e) { + // If this is the exception thrown by app.exec() then that is a normal + // case and we suppress it. + if (e == "unwind") // not much to go on return; - } - for (var i = 0; i < config.containerElements.length; ++i) { - var container = config.containerElements[i]; - var canvas = config.canvasElements[i]; - config.showCanvas(canvas, container); - container.appendChild(canvas); + if (!onExitCalled) { + onExitCalled = true; + config.qt.onExit?.({ + text: e.message, + crashed: true + }); } + throw e; } - function setExitContent() { - - // publicAPI.crashed = true; - - if (publicAPI.status != "Exited") - return; - - if (config.containerElements === undefined) { - if (config.showExit !== undefined) - config.showExit(publicAPI.crashed, publicAPI.exitCode); - return; - } - - if (!publicAPI.crashed) - return; + return instance; +} - for (container of config.containerElements) { - var loaderElement = config.showExit(publicAPI.crashed, publicAPI.exitCode, container); - if (loaderElement !== undefined) - container.appendChild(loaderElement); - } +// Compatibility API. This API is deprecated, +// and will be removed in a future version of Qt. +function QtLoader(qtConfig) { + + const warning = 'Warning: The QtLoader API is deprecated and will be removed in ' + + 'a future version of Qt. Please port to the new qtLoad() API.'; + console.warn(warning); + + let emscriptenConfig = qtConfig.moduleConfig || {} + qtConfig.moduleConfig = undefined; + const showLoader = qtConfig.showLoader; + qtConfig.showLoader = undefined; + const showError = qtConfig.showError; + qtConfig.showError = undefined; + const showExit = qtConfig.showExit; + qtConfig.showExit = undefined; + const showCanvas = qtConfig.showCanvas; + qtConfig.showCanvas = undefined; + if (qtConfig.canvasElements) { + qtConfig.containerElements = qtConfig.canvasElements + qtConfig.canvasElements = undefined; + } else { + qtConfig.containerElements = qtConfig.containerElements; + qtConfig.containerElements = undefined; } - - var committedStatus = undefined; - function handleStatusChange() { - if (publicAPI.status != "Loading" && committedStatus == publicAPI.status) - return; - committedStatus = publicAPI.status; - - if (publicAPI.status == "Error") { - setErrorContent(); - } else if (publicAPI.status == "Loading") { - setLoaderContent(); - } else if (publicAPI.status == "Running") { - setCanvasContent(); - } else if (publicAPI.status == "Exited") { - if (config.restartMode == "RestartOnExit" || - config.restartMode == "RestartOnCrash" && publicAPI.crashed) { - committedStatus = undefined; - config.restart(); - } else { - setExitContent(); + emscriptenConfig.qt = qtConfig; + + let qtloader = { + exitCode: undefined, + exitText: "", + loadEmscriptenModule: _name => { + try { + qtLoad(emscriptenConfig); + } catch (e) { + showError?.(e.message); } } - - // Send status change notification - if (config.statusChanged) - config.statusChanged(publicAPI.status); } - function setStatus(status) { - if (status != "Loading" && publicAPI.status == status) - return; - publicAPI.status = status; - - window.setTimeout(function() { handleStatusChange(); }, 0); - } - - function addCanvasElement(element) { - if (publicAPI.status == "Running") - self.module.qtAddCanvasElement(element); - else - console.log("Error: addCanvasElement can only be called in the Running state"); + qtConfig.onLoaded = () => { + showCanvas?.(); } - function removeCanvasElement(element) { - if (publicAPI.status == "Running") - self.module.qtRemoveCanvasElement(element); - else - console.log("Error: removeCanvasElement can only be called in the Running state"); + qtConfig.onExit = exit => { + qtloader.exitCode = exit.code + qtloader.exitText = exit.text; + showExit?.(); } - function resizeCanvasElement(element) { - if (publicAPI.status == "Running") - self.module.qtResizeCanvasElement(element); - } - - function setFontDpi(dpi) { - self.qtFontDpi = dpi; - if (publicAPI.status == "Running") - self.qtSetFontDpi(dpi); - } + showLoader?.("Loading"); - function fontDpi() { - return self.qtFontDpi; - } - - function module() { - return self.module; - } - - setStatus("Created"); - - return publicAPI; -} + return qtloader; +}; diff --git a/src/plugins/platforms/wasm/qtlogo.svg b/src/plugins/platforms/wasm/qtlogo.svg deleted file mode 100644 index ad7c7776bf..0000000000 --- a/src/plugins/platforms/wasm/qtlogo.svg +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - width="462pt" - height="339pt" - viewBox="0 0 462 339" - version="1.1"> - <metadata - id="metadata20"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <path - fill="#41cd52" - d=" M 63.50 0.00 L 462.00 0.00 L 462.00 274.79 C 440.60 296.26 419.13 317.66 397.61 339.00 L 0.00 339.00 L 0.00 63.39 C 21.08 42.18 42.34 21.13 63.50 0.00 Z" - id="path6" /> - <path - d=" M 122.37 71.33 C 137.50 61.32 156.21 58.79 174.00 58.95 C 190.94 59.16 208.72 62.13 222.76 72.24 C 232.96 79.41 239.59 90.48 244.01 101.93 C 251.16 120.73 253.26 141.03 253.50 161.01 C 253.53 181.13 252.62 201.69 245.96 220.86 C 241.50 233.90 233.01 245.48 221.81 253.52 C 229.87 266.58 238.09 279.54 246.15 292.60 C 236.02 297.27 225.92 301.97 215.78 306.62 C 207.15 292.38 198.56 278.11 189.90 263.89 C 178.19 265.81 166.21 265.66 154.44 264.36 C 140.34 262.67 125.97 258.37 115.09 248.88 C 106.73 241.64 101.48 231.51 97.89 221.21 C 92.01 203.79 90.43 185.25 90.16 166.97 C 90.02 147.21 91.28 127.14 97.24 108.18 C 101.85 93.92 109.48 79.69 122.37 71.33 Z" - id="path8" - fill="#ffffff" /> - <path - d=" M 294.13 70.69 C 304.73 70.68 315.33 70.68 325.93 70.69 C 325.96 84.71 325.92 98.72 325.95 112.74 C 339.50 112.76 353.05 112.74 366.60 112.75 C 366.37 121.85 366.12 130.95 365.86 140.05 C 352.32 140.08 338.79 140.04 325.25 140.07 C 325.28 163.05 325.18 186.03 325.30 209.01 C 325.56 215.30 325.42 221.94 328.19 227.75 C 330.21 232.23 335.65 233.38 340.08 233.53 C 348.43 233.50 356.77 233.01 365.12 232.86 C 365.63 241.22 366.12 249.59 366.60 257.95 C 349.99 260.74 332.56 264.08 316.06 258.86 C 309.11 256.80 302.63 252.19 299.81 245.32 C 294.76 233.63 294.35 220.62 294.13 208.07 C 294.11 185.40 294.13 162.74 294.12 140.07 C 286.73 140.05 279.34 140.08 271.95 140.05 C 271.93 130.96 271.93 121.86 271.95 112.76 C 279.34 112.73 286.72 112.77 294.11 112.74 C 294.14 98.72 294.10 84.71 294.13 70.69 Z" - id="path10" - fill="#ffffff" /> - <path - fill="#41cd52" - d=" M 160.51 87.70 C 170.80 86.36 181.60 86.72 191.34 90.61 C 199.23 93.73 205.93 99.84 209.47 107.58 C 214.90 119.31 216.98 132.26 218.03 145.05 C 219.17 162.07 219.01 179.25 216.66 196.17 C 215.01 206.24 212.66 216.85 205.84 224.79 C 198.92 232.76 188.25 236.18 178.01 236.98 C 167.21 237.77 155.82 236.98 146.07 231.87 C 140.38 228.84 135.55 224.09 132.73 218.27 C 129.31 211.30 127.43 203.69 126.11 196.07 C 122.13 171.91 121.17 146.91 126.61 122.89 C 128.85 113.83 132.11 104.53 138.73 97.70 C 144.49 91.85 152.51 88.83 160.51 87.70 Z" - id="path12" /> -</svg> diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.cpp b/src/plugins/platforms/wasm/qwasmaccessibility.cpp new file mode 100644 index 0000000000..4c3cb46ba3 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmaccessibility.cpp @@ -0,0 +1,789 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmaccessibility.h" +#include "qwasmscreen.h" +#include "qwasmwindow.h" +#include "qwasmintegration.h" +#include <QtGui/qwindow.h> + +#if QT_CONFIG(accessibility) + +#include <QtGui/private/qaccessiblebridgeutils_p.h> + +Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") + +// Qt WebAssembly a11y backend +// +// This backend implements accessibility support by creating "shadowing" html +// elements for each Qt UI element. We access the DOM by using Emscripten's +// val.h API. +// +// Currently, html elements are created in response to notifyAccessibilityUpdate +// events. In addition or alternatively, we could also walk the accessibility tree +// from setRootObject(). + +QWasmAccessibility::QWasmAccessibility() +{ + + s_instance = this; +} + +QWasmAccessibility::~QWasmAccessibility() +{ + s_instance = nullptr; +} + +QWasmAccessibility *QWasmAccessibility::s_instance = nullptr; + +QWasmAccessibility* QWasmAccessibility::get() +{ + return s_instance; +} + +void QWasmAccessibility::addAccessibilityEnableButton(QWindow *window) +{ + get()->addAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::removeAccessibilityEnableButton(QWindow *window) +{ + get()->removeAccessibilityEnableButtonImpl(window); +} + +void QWasmAccessibility::addAccessibilityEnableButtonImpl(QWindow *window) +{ + if (m_accessibilityEnabled) + return; + + emscripten::val container = getContainer(window); + emscripten::val document = getDocument(container); + emscripten::val button = document.call<emscripten::val>("createElement", std::string("button")); + button.set("innerText", std::string("Enable Screen Reader")); + button["classList"].call<void>("add", emscripten::val("hidden-visually-read-by-screen-reader")); + container.call<void>("appendChild", button); + + auto enableContext = std::make_tuple(button, std::make_unique<qstdweb::EventCallback> + (button, std::string("click"), [this](emscripten::val) { enableAccessibility(); })); + m_enableButtons.insert(std::make_pair(window, std::move(enableContext))); +} + +void QWasmAccessibility::removeAccessibilityEnableButtonImpl(QWindow *window) +{ + auto it = m_enableButtons.find(window); + if (it == m_enableButtons.end()) + return; + + // Remove button + auto [element, callback] = it->second; + Q_UNUSED(callback); + element["parentElement"].call<void>("removeChild", element); + m_enableButtons.erase(it); +} + +void QWasmAccessibility::enableAccessibility() +{ + // Enable accessibility globally for the applicaton. Remove all "enable" + // buttons and populate the accessibility tree, starting from the root object. + + Q_ASSERT(!m_accessibilityEnabled); + m_accessibilityEnabled = true; + for (const auto& [key, value] : m_enableButtons) { + const auto &[element, callback] = value; + Q_UNUSED(key); + Q_UNUSED(callback); + element["parentElement"].call<void>("removeChild", element); + } + m_enableButtons.clear(); + populateAccessibilityTree(QAccessible::queryAccessibleInterface(m_rootObject)); +} + +emscripten::val QWasmAccessibility::getContainer(QWindow *window) +{ + return window ? static_cast<QWasmWindow *>(window->handle())->a11yContainer() + : emscripten::val::undefined(); +} + +emscripten::val QWasmAccessibility::getContainer(QAccessibleInterface *iface) +{ + if (!iface) + return emscripten::val::undefined(); + return getContainer(getWindow(iface)); +} + +QWindow *QWasmAccessibility::getWindow(QAccessibleInterface *iface) +{ + QWindow *window = iface->window(); + // this is needed to add tabs as the window is not available + if (!window && iface->parent()) + window = iface->parent()->window(); + return window; +} + +emscripten::val QWasmAccessibility::getDocument(const emscripten::val &container) +{ + if (container.isUndefined()) + return emscripten::val::global("document"); + return container["ownerDocument"]; +} + +emscripten::val QWasmAccessibility::getDocument(QAccessibleInterface *iface) +{ + return getDocument(getContainer(iface)); +} + +emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *iface) +{ + // Get the html container element for the interface; this depends on which + // QScreen it is on. If the interface is not on a screen yet we get an undefined + // container, and the code below handles that case as well. + emscripten::val container = getContainer(iface); + + // Get the correct html document for the container, or fall back + // to the global document. TODO: Does using the correct document actually matter? + emscripten::val document = getDocument(container); + + // Translate the Qt a11y elemen role into html element type + ARIA role. + // Here we can either create <div> elements with a spesific ARIA role, + // or create e.g. <button> elements which should have built-in accessibility. + emscripten::val element = [this, iface, document] { + + emscripten::val element = emscripten::val::undefined(); + + switch (iface->role()) { + + case QAccessible::Button: { + element = document.call<emscripten::val>("createElement", std::string("button")); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + case QAccessible::CheckBox: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("checkbox")); + if (iface->state().checked) { + element.call<void>("setAttribute", std::string("checked"), std::string("true")); + } + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + + } break; + + case QAccessible::RadioButton: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("radio")); + if (iface->state().checked) { + element.call<void>("setAttribute", std::string("checked"), std::string("true")); + } + element.set(std::string("name"), std::string("buttonGroup")); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::SpinBox: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("number")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::Slider: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"), std::string("range")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::PageTabList:{ + element = document.call<emscripten::val>("createElement", std::string("div")); + element.call<void>("setAttribute", std::string("role"), std::string("tablist")); + QString idName = iface->text(QAccessible::Name).replace(" ", "_"); + idName += "_tabList"; + element.call<void>("setAttribute", std::string("id"), idName.toStdString()); + + for (int i = 0; i < iface->childCount(); ++i) { + if (iface->child(i)->role() == QAccessible::PageTab){ + emscripten::val elementTab = emscripten::val::undefined(); + elementTab = ensureHtmlElement(iface->child(i)); + elementTab.call<void>("setAttribute", std::string("aria-owns"), idName.toStdString()); + setHtmlElementGeometry(iface->child(i)); + } + } + } break; + + case QAccessible::PageTab:{ + element = document.call<emscripten::val>("createElement", std::string("button")); + element.call<void>("setAttribute", std::string("role"), std::string("tab")); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::ScrollBar: { + element = document.call<emscripten::val>("createElement", std::string("div")); + element.call<void>("setAttribute", std::string("role"), std::string("scrollbar")); + std::string valueString = iface->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("aria-valuenow"), valueString); + element.call<void>("addEventListener", emscripten::val("change"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + + case QAccessible::StaticText: { + element = document.call<emscripten::val>("createElement", std::string("textarea")); + element.call<void>("setAttribute", std::string("readonly"), std::string("true")); + + } break; + case QAccessible::Dialog: { + element = document.call<emscripten::val>("createElement", std::string("dialog")); + }break; + case QAccessible::ToolBar:{ + element = document.call<emscripten::val>("createElement", std::string("div")); + QString text = iface->text(QAccessible::Name); + + element.call<void>("setAttribute", std::string("role"), std::string("toolbar")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + }break; + case QAccessible::MenuItem: + case QAccessible::ButtonMenu: { + element = document.call<emscripten::val>("createElement", std::string("button")); + QString text = iface->text(QAccessible::Name); + + element.call<void>("setAttribute", std::string("role"), std::string("menuitem")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + element.call<void>("addEventListener", emscripten::val("click"), + emscripten::val::module_property("qtEventReceived"), true); + }break; + case QAccessible::MenuBar: + case QAccessible::PopupMenu: { + element = document.call<emscripten::val>("createElement",std::string("div")); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("role"), std::string("menubar")); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + for (int i = 0; i < iface->childCount(); ++i) { + emscripten::val childElement = emscripten::val::undefined(); + childElement= ensureHtmlElement(iface->child(i)); + childElement.call<void>("setAttribute", std::string("aria-owns"), text.toStdString()); + setHtmlElementTextName(iface->child(i)); + setHtmlElementGeometry(iface->child(i)); + } + }break; + case QAccessible::EditableText: { + element = document.call<emscripten::val>("createElement", std::string("input")); + element.call<void>("setAttribute", std::string("type"),std::string("text")); + element.call<void>("addEventListener", emscripten::val("input"), + emscripten::val::module_property("qtEventReceived"), true); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: createHtmlElement() handle" << iface->role(); + element = document.call<emscripten::val>("createElement", std::string("div")); + } + + return element; + + }(); + + // Add the html element to the container if we have one. If not there + // is a second chance when handling the ObjectShow event. + if (!container.isUndefined()) + container.call<void>("appendChild", element); + + return element; +} + +void QWasmAccessibility::destroyHtmlElement(QAccessibleInterface *iface) +{ + Q_UNUSED(iface); + qCDebug(lcQpaAccessibility) << "TODO destroyHtmlElement"; +} + +emscripten::val QWasmAccessibility::ensureHtmlElement(QAccessibleInterface *iface) +{ + auto it = m_elements.find(iface); + if (it != m_elements.end()) + return it.value(); + + emscripten::val element = createHtmlElement(iface); + m_elements.insert(iface, element); + + return element; +} + +void QWasmAccessibility::setHtmlElementVisibility(QAccessibleInterface *iface, bool visible) +{ + emscripten::val element = ensureHtmlElement(iface); + emscripten::val container = getContainer(iface); + + if (container.isUndefined()) { + qCDebug(lcQpaAccessibility) << "TODO: setHtmlElementVisibility: unable to find html container for element" << iface; + return; + } + + container.call<void>("appendChild", element); + + element.set("ariaHidden", !visible); // ariaHidden mean completely hidden; maybe some sort of soft-hidden should be used. +} + +void QWasmAccessibility::setHtmlElementGeometry(QAccessibleInterface *iface) +{ + emscripten::val element = ensureHtmlElement(iface); + + // QAccessibleInterface gives us the geometry in global (screen) coordinates. Translate that + // to window geometry in order to position elements relative to window origin. + QWindow *window = getWindow(iface); + if (!window) + qCWarning(lcQpaAccessibility) << "Unable to find window for" << iface << "setting null geometry"; + QRect screenGeometry = iface->rect(); + QPoint windowPos = window ? window->mapFromGlobal(screenGeometry.topLeft()) : QPoint(); + QRect windowGeometry(windowPos, screenGeometry.size()); + + setHtmlElementGeometry(element, windowGeometry); +} + +void QWasmAccessibility::setHtmlElementGeometry(emscripten::val element, QRect geometry) +{ + // Position the element using "position: absolute" in order to place + // it under the corresponding Qt element in the screen. + emscripten::val style = element["style"]; + style.set("position", std::string("absolute")); + style.set("z-index", std::string("-1")); // FIXME: "0" should be sufficient to order beheind the + // screen element, but isn't + style.set("left", std::to_string(geometry.x()) + "px"); + style.set("top", std::to_string(geometry.y()) + "px"); + style.set("width", std::to_string(geometry.width()) + "px"); + style.set("height", std::to_string(geometry.height()) + "px"); +} + +void QWasmAccessibility::setHtmlElementTextName(QAccessibleInterface *iface) +{ + emscripten::val element = ensureHtmlElement(iface); + QString text = iface->text(QAccessible::Name); + element.set("innerHTML", text.toStdString()); // FIXME: use something else than innerHTML +} + +void QWasmAccessibility::setHtmlElementTextNameLE(QAccessibleInterface *iface) { + emscripten::val element = ensureHtmlElement(iface); + QString text = iface->text(QAccessible::Name); + element.call<void>("setAttribute", std::string("name"), text.toStdString()); + QString value = iface->text(QAccessible::Value); + element.set("innerHTML", value.toStdString()); +} + +void QWasmAccessibility::setHtmlElementDescription(QAccessibleInterface *iface) { + emscripten::val element = ensureHtmlElement(iface); + QString desc = iface->text(QAccessible::Description); + element.call<void>("setAttribute", std::string("aria-description"), desc.toStdString()); +} + +void QWasmAccessibility::handleStaticTextUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleStaticTextUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event) { + + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: + case QAccessible::TextRemoved: + case QAccessible::TextInserted: + case QAccessible::TextCaretMoved: { + setHtmlElementTextNameLE(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event) +{ + + QAccessibleInterface *iface = m_elements.key(event["target"]); + if (iface == nullptr) { + return; + } else { + QString eventType = QString::fromStdString(event["type"].as<std::string>()); + const auto& actionNames = QAccessibleBridgeUtils::effectiveActionNames(iface); + if (actionNames.contains(QAccessibleActionInterface::pressAction())) { + + iface->actionInterface()->doAction(QAccessibleActionInterface::pressAction()); + + } else if (actionNames.contains(QAccessibleActionInterface::toggleAction())) { + + iface->actionInterface()->doAction(QAccessibleActionInterface::toggleAction()); + + } else if (actionNames.contains(QAccessibleActionInterface::increaseAction()) || + actionNames.contains(QAccessibleActionInterface::decreaseAction())) { + + QString val = QString::fromStdString(event["target"]["value"].as<std::string>()); + + iface->valueInterface()->setCurrentValue(val.toInt()); + + } else if (eventType == "input") { + + // as EditableTextInterface is not implemented in qml accessibility + // so we need to check the role for text to update in the textbox during accessibility + + if (iface->editableTextInterface() || iface->role() == QAccessible::EditableText) { + std::string insertText = event["target"]["value"].as<std::string>(); + iface->setText(QAccessible::Value, QString::fromStdString(insertText)); + } + } + } +} + +void QWasmAccessibility::handleButtonUpdate(QAccessibleEvent *event) +{ + qCDebug(lcQpaAccessibility) << "TODO: implement handleButtonUpdate for event" << event->type(); +} + +void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::StateChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + bool checkedString = accessible->state().checked ? true : false; + element.call<void>("setAttribute", std::string("checked"), checkedString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleCheckBoxUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event) +{ + QAccessibleInterface *iface = event->accessibleInterface(); + QString text = iface->text(QAccessible::Name); + QString desc = iface->text(QAccessible::Description); + switch (event->type()) { + case QAccessible::NameChanged: + case QAccessible::StateChanged:{ + emscripten::val element = ensureHtmlElement(iface); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleToolUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event) +{ + QAccessibleInterface *iface = event->accessibleInterface(); + QString text = iface->text(QAccessible::Name); + QString desc = iface->text(QAccessible::Description); + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: + case QAccessible::MenuStart ://"TODO: To implement later + case QAccessible::PopupMenuStart://"TODO: To implement later + case QAccessible::StateChanged:{ + emscripten::val element = ensureHtmlElement(iface); + element.call<void>("setAttribute", std::string("title"), text.toStdString()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleMenuUpdate for event" << event->type(); + break; + } +} +void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) { + + switch (event->type()) { + case QAccessible::NameChanged: + case QAccessible::Focus: + case QAccessible::DialogStart: + case QAccessible::StateChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement handleLineEditUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface) +{ + if (!iface) + return; + + // Create html element for the interface, sync up properties. + ensureHtmlElement(iface); + const bool visible = !iface->state().invisible; + setHtmlElementVisibility(iface, visible); + setHtmlElementGeometry(iface); + setHtmlElementTextName(iface); + setHtmlElementDescription(iface); + + for (int i = 0; i < iface->childCount(); ++i) + populateAccessibilityTree(iface->child(i)); +} + +void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::StateChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string checkedString = accessible->state().checked ? "true" : "false"; + element.call<void>("setAttribute", std::string("checked"), checkedString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleRadioButtonUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSpinBoxUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("value"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSliderUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::Focus: + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::ValueChanged: { + QAccessibleInterface *accessible = event->accessibleInterface(); + emscripten::val element = ensureHtmlElement(accessible); + std::string valueString = accessible->valueInterface()->currentValue().toString().toStdString(); + element.call<void>("setAttribute", std::string("aria-valuenow"), valueString); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handleSliderUpdate for event" << event->type(); + break; + } + +} + +void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event) +{ + switch (event->type()) { + case QAccessible::NameChanged: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::Focus: { + setHtmlElementTextName(event->accessibleInterface()); + } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; + default: + qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); + break; + } +} + +void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) +{ + if (!m_accessibilityEnabled) + return; + + QAccessibleInterface *iface = event->accessibleInterface(); + if (!iface) { + qWarning() << "notifyAccessibilityUpdate with null a11y interface" ; + return; + } + + // Handle some common event types. See + // https://doc.qt.io/qt-5/qaccessible.html#Event-enum + switch (event->type()) { + case QAccessible::ObjectShow: + setHtmlElementVisibility(iface, true); + + // Sync up properties on show; + setHtmlElementGeometry(iface); + setHtmlElementTextName(iface); + setHtmlElementDescription(iface); + + return; + break; + case QAccessible::ObjectHide: + setHtmlElementVisibility(iface, false); + return; + break; + // TODO: maybe handle more types here + default: + break; + }; + + // Switch on interface role, see + // https://doc.qt.io/qt-5/qaccessibleinterface.html#role + switch (iface->role()) { + case QAccessible::StaticText: + handleStaticTextUpdate(event); + break; + case QAccessible::Button: + handleStaticTextUpdate(event); + break; + case QAccessible::CheckBox: + handleCheckBoxUpdate(event); + break; + case QAccessible::EditableText: + handleLineEditUpdate(event); + break; + case QAccessible::Dialog: + handleDialogUpdate(event); + break; + case QAccessible::MenuItem: + case QAccessible::MenuBar: + case QAccessible::PopupMenu: + handleMenuUpdate(event); + break; + case QAccessible::ToolBar: + case QAccessible::ButtonMenu: + handleToolUpdate(event); + case QAccessible::RadioButton: + handleRadioButtonUpdate(event); + break; + case QAccessible::SpinBox: + handleSpinBoxUpdate(event); + break; + case QAccessible::Slider: + handleSliderUpdate(event); + break; + case QAccessible::PageTab: + handlePageTabUpdate(event); + break; + case QAccessible::PageTabList: + handlePageTabListUpdate(event); + break; + case QAccessible::ScrollBar: + handleScrollBarUpdate(event); + break; + default: + qCDebug(lcQpaAccessibility) << "TODO: implement notifyAccessibilityUpdate for role" << iface->role(); + }; +} + +void QWasmAccessibility::setRootObject(QObject *root) +{ + m_rootObject = root; +} + +void QWasmAccessibility::initialize() +{ + +} + +void QWasmAccessibility::cleanup() +{ + +} + +void QWasmAccessibility::onHtmlEventReceived(emscripten::val event) +{ + static_cast<QWasmAccessibility *>(QWasmIntegration::get()->accessibility())->handleEventFromHtmlElement(event); +} + +EMSCRIPTEN_BINDINGS(qtButtonEvent) { + function("qtEventReceived", &QWasmAccessibility::onHtmlEventReceived); +} + +#endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/wasm/qwasmaccessibility.h b/src/plugins/platforms/wasm/qwasmaccessibility.h new file mode 100644 index 0000000000..c4be7f0d72 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmaccessibility.h @@ -0,0 +1,92 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMACCESIBILITY_H +#define QWASMACCESIBILITY_H + +#include <QtCore/qtconfigmacros.h> +#include <QtGui/qtguiglobal.h> + +#if QT_CONFIG(accessibility) + +#include <QtCore/qhash.h> +#include <private/qstdweb_p.h> +#include <qpa/qplatformaccessibility.h> + +#include <emscripten/val.h> +#include <QLoggingCategory> + +#include <map> +#include <emscripten/bind.h> + +Q_DECLARE_LOGGING_CATEGORY(lcQpaAccessibility) + +class QWasmAccessibility : public QPlatformAccessibility +{ +public: + QWasmAccessibility(); + ~QWasmAccessibility(); + + static QWasmAccessibility* get(); + + static void addAccessibilityEnableButton(QWindow *window); + static void removeAccessibilityEnableButton(QWindow *window); + +private: + void addAccessibilityEnableButtonImpl(QWindow *window); + void removeAccessibilityEnableButtonImpl(QWindow *window); + void enableAccessibility(); + + static emscripten::val getContainer(QWindow *window); + static emscripten::val getContainer(QAccessibleInterface *iface); + static emscripten::val getDocument(const emscripten::val &container); + static emscripten::val getDocument(QAccessibleInterface *iface); + static QWindow *getWindow(QAccessibleInterface *iface); + + emscripten::val createHtmlElement(QAccessibleInterface *iface); + void destroyHtmlElement(QAccessibleInterface *iface); + emscripten::val ensureHtmlElement(QAccessibleInterface *iface); + void setHtmlElementVisibility(QAccessibleInterface *iface, bool visible); + void setHtmlElementGeometry(QAccessibleInterface *iface); + void setHtmlElementGeometry(emscripten::val element, QRect geometry); + void setHtmlElementTextName(QAccessibleInterface *iface); + void setHtmlElementTextNameLE(QAccessibleInterface *iface); + void setHtmlElementDescription(QAccessibleInterface *iface); + + void handleStaticTextUpdate(QAccessibleEvent *event); + void handleButtonUpdate(QAccessibleEvent *event); + void handleCheckBoxUpdate(QAccessibleEvent *event); + void handleDialogUpdate(QAccessibleEvent *event); + void handleMenuUpdate(QAccessibleEvent *event); + void handleToolUpdate(QAccessibleEvent *event); + void handleLineEditUpdate(QAccessibleEvent *event); + void handleRadioButtonUpdate(QAccessibleEvent *event); + void handleSpinBoxUpdate(QAccessibleEvent *event); + void handlePageTabUpdate(QAccessibleEvent *event); + void handleSliderUpdate(QAccessibleEvent *event); + void handleScrollBarUpdate(QAccessibleEvent *event); + void handlePageTabListUpdate(QAccessibleEvent *event); + + void handleEventFromHtmlElement(const emscripten::val event); + + void populateAccessibilityTree(QAccessibleInterface *iface); + void notifyAccessibilityUpdate(QAccessibleEvent *event) override; + void setRootObject(QObject *o) override; + void initialize() override; + void cleanup() override; + +public: // public for EMSCRIPTEN_BINDINGS + static void onHtmlEventReceived(emscripten::val event); + +private: + static QWasmAccessibility *s_instance; + QObject *m_rootObject = nullptr; + bool m_accessibilityEnabled = false; + std::map<QWindow *, std::tuple<emscripten::val, std::shared_ptr<qstdweb::EventCallback>>> m_enableButtons; + QHash<QAccessibleInterface *, emscripten::val> m_elements; + +}; + +#endif // QT_CONFIG(accessibility) + +#endif diff --git a/src/plugins/platforms/wasm/qwasmbackingstore.cpp b/src/plugins/platforms/wasm/qwasmbackingstore.cpp index 594dcaa812..a3c1ae8a50 100644 --- a/src/plugins/platforms/wasm/qwasmbackingstore.cpp +++ b/src/plugins/platforms/wasm/qwasmbackingstore.cpp @@ -4,21 +4,18 @@ #include "qwasmbackingstore.h" #include "qwasmwindow.h" #include "qwasmcompositor.h" +#include "qwasmdom.h" -#include <QtOpenGL/qopengltexture.h> -#include <QtGui/qmatrix4x4.h> #include <QtGui/qpainter.h> -#include <private/qguiapplication_p.h> -#include <qpa/qplatformscreen.h> -#include <QtGui/qoffscreensurface.h> #include <QtGui/qbackingstore.h> +#include <emscripten.h> +#include <emscripten/wire.h> + QT_BEGIN_NAMESPACE QWasmBackingStore::QWasmBackingStore(QWasmCompositor *compositor, QWindow *window) - : QPlatformBackingStore(window) - , m_compositor(compositor) - , m_texture(new QOpenGLTexture(QOpenGLTexture::Target2D)) + : QPlatformBackingStore(window), m_compositor(compositor) { QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle()); if (wasmWindow) @@ -29,29 +26,11 @@ QWasmBackingStore::~QWasmBackingStore() { auto window = this->window(); QWasmIntegration::get()->removeBackingStore(window); - destroy(); QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle()); if (wasmWindow) wasmWindow->setBackingStore(nullptr); } -void QWasmBackingStore::destroy() -{ - if (m_texture->isCreated()) { - auto context = m_compositor->context(); - auto currentContext = QOpenGLContext::currentContext(); - if (!currentContext || !QOpenGLContext::areSharing(context, currentContext)) { - QOffscreenSurface offScreenSurface(m_compositor->screen()->screen()); - offScreenSurface.setFormat(context->format()); - offScreenSurface.create(); - context->makeCurrent(&offScreenSurface); - m_texture->destroy(); - } else { - m_texture->destroy(); - } - } -} - QPaintDevice *QWasmBackingStore::paintDevice() { return &m_image; @@ -64,33 +43,24 @@ void QWasmBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoi Q_UNUSED(offset); m_dirty |= region; - m_compositor->handleBackingStoreFlush(); + m_compositor->handleBackingStoreFlush(window); } -void QWasmBackingStore::updateTexture() +void QWasmBackingStore::updateTexture(QWasmWindow *window) { if (m_dirty.isNull()) return; - if (m_recreateTexture) { - m_recreateTexture = false; - destroy(); + if (m_webImageDataArray.isUndefined()) { + m_webImageDataArray = window->context2d().call<emscripten::val>( + "createImageData", emscripten::val(m_image.width()), + emscripten::val(m_image.height())); } - if (!m_texture->isCreated()) { - m_texture->setMinificationFilter(QOpenGLTexture::Nearest); - m_texture->setMagnificationFilter(QOpenGLTexture::Nearest); - m_texture->setWrapMode(QOpenGLTexture::ClampToEdge); - m_texture->setData(m_image, QOpenGLTexture::DontGenerateMipMaps); - m_texture->create(); - } - m_texture->bind(); - - QRegion fixed; + QRegion clippedDpiScaledRegion; QRect imageRect = m_image.rect(); for (const QRect &rect : m_dirty) { - // Convert device-independent dirty region to device region qreal dpr = m_image.devicePixelRatio(); QRect deviceRect = QRect(rect.topLeft() * dpr, rect.size() * dpr); @@ -103,21 +73,11 @@ void QWasmBackingStore::updateTexture() r.setWidth(imageRect.width()); } - fixed |= r; + clippedDpiScaledRegion |= r; } - for (const QRect &rect : fixed) { - // if the sub-rect is full-width we can pass the image data directly to - // OpenGL instead of copying, since there is no gap between scanlines - if (rect.width() == imageRect.width()) { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, rect.y(), rect.width(), rect.height(), GL_RGBA, GL_UNSIGNED_BYTE, - m_image.constScanLine(rect.y())); - } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, rect.x(), rect.y(), rect.width(), rect.height(), GL_RGBA, GL_UNSIGNED_BYTE, - m_image.copy(rect).constBits()); - } - } - /* End of code taken from QEGLPlatformBackingStore */ + for (const QRect &dirtyRect : clippedDpiScaledRegion) + dom::drawImageToWebImageDataArray(m_image, m_webImageDataArray, dirtyRect); m_dirty = QRegion(); } @@ -126,7 +86,7 @@ void QWasmBackingStore::beginPaint(const QRegion ®ion) { m_dirty |= region; // Keep backing store device pixel ratio in sync with window - if (m_image.devicePixelRatio() != window()->devicePixelRatio()) + if (m_image.devicePixelRatio() != window()->handle()->devicePixelRatio()) resize(backingStore()->size(), backingStore()->staticContents()); QPainter painter(&m_image); @@ -143,11 +103,11 @@ void QWasmBackingStore::resize(const QSize &size, const QRegion &staticContents) { Q_UNUSED(staticContents); - QImage::Format format = window()->format().hasAlpha() ? - QImage::Format_ARGB32_Premultiplied : QImage::Format_RGB32; - m_image = QImage(size * window()->devicePixelRatio(), format); - m_image.setDevicePixelRatio(window()->devicePixelRatio()); - m_recreateTexture = true; + QImage::Format format = QImage::Format_RGBA8888; + const auto platformScreenDPR = window()->handle()->devicePixelRatio(); + m_image = QImage(size * platformScreenDPR, format); + m_image.setDevicePixelRatio(platformScreenDPR); + m_webImageDataArray = emscripten::val::undefined(); } QImage QWasmBackingStore::toImage() const @@ -161,10 +121,10 @@ const QImage &QWasmBackingStore::getImageRef() const return m_image; } -const QOpenGLTexture *QWasmBackingStore::getUpdatedTexture() +emscripten::val QWasmBackingStore::getUpdatedWebImage(QWasmWindow *window) { - updateTexture(); - return m_texture.data(); + updateTexture(window); + return m_webImageDataArray; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbackingstore.h b/src/plugins/platforms/wasm/qwasmbackingstore.h index b5c5e2e406..54e9fe4cb3 100644 --- a/src/plugins/platforms/wasm/qwasmbackingstore.h +++ b/src/plugins/platforms/wasm/qwasmbackingstore.h @@ -7,18 +7,20 @@ #include <qpa/qplatformbackingstore.h> #include <QtGui/qimage.h> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE class QOpenGLTexture; class QRegion; class QWasmCompositor; +class QWasmWindow; class QWasmBackingStore : public QPlatformBackingStore { public: QWasmBackingStore(QWasmCompositor *compositor, QWindow *window); ~QWasmBackingStore(); - void destroy(); QPaintDevice *paintDevice() override; @@ -28,17 +30,16 @@ public: QImage toImage() const override; const QImage &getImageRef() const; - const QOpenGLTexture *getUpdatedTexture(); + emscripten::val getUpdatedWebImage(QWasmWindow *window); protected: - void updateTexture(); + void updateTexture(QWasmWindow *window); private: QWasmCompositor *m_compositor; QImage m_image; - QScopedPointer<QOpenGLTexture> m_texture; QRegion m_dirty; - bool m_recreateTexture = false; + emscripten::val m_webImageDataArray = emscripten::val::undefined(); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp b/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp new file mode 100644 index 0000000000..8f05f082ea --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.cpp @@ -0,0 +1,40 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmbase64iconstore.h" + +#include <QtCore/qfile.h> + +QT_BEGIN_NAMESPACE + +Q_GLOBAL_STATIC(Base64IconStore, globalWasmWindowIconStore); + +Base64IconStore::Base64IconStore() +{ + QString iconSources[static_cast<size_t>(IconType::Size)] = { + QStringLiteral(":/wasm-window/maximize.svg"), QStringLiteral(":/wasm-window/qtlogo.svg"), + QStringLiteral(":/wasm-window/restore.svg"), QStringLiteral(":/wasm-window/x.svg") + }; + + for (size_t iconType = static_cast<size_t>(IconType::First); + iconType < static_cast<size_t>(IconType::Size); ++iconType) { + QFile svgFile(iconSources[static_cast<size_t>(iconType)]); + if (!svgFile.open(QIODevice::ReadOnly)) + Q_ASSERT(false); // A resource should always be opened. + m_storage[static_cast<size_t>(iconType)] = svgFile.readAll().toBase64(); + } +} + +Base64IconStore::~Base64IconStore() = default; + +Base64IconStore *Base64IconStore::get() +{ + return globalWasmWindowIconStore(); +} + +std::string_view Base64IconStore::getIcon(IconType type) const +{ + return m_storage[static_cast<size_t>(type)]; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.h b/src/plugins/platforms/wasm/qwasmbase64iconstore.h new file mode 100644 index 0000000000..89704f2d2c --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.h @@ -0,0 +1,37 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMBASE64IMAGESTORE_H +#define QWASMBASE64IMAGESTORE_H + +#include <string> +#include <string_view> + +#include <QtCore/qtconfigmacros.h> + +QT_BEGIN_NAMESPACE +class Base64IconStore +{ +public: + enum class IconType { + Maximize, + First = Maximize, + QtLogo, + Restore, + X, + Size, + }; + + Base64IconStore(); + ~Base64IconStore(); + + static Base64IconStore *get(); + + std::string_view getIcon(IconType type) const; + +private: + std::string m_storage[static_cast<size_t>(IconType::Size)]; +}; + +QT_END_NAMESPACE +#endif // QWASMBASE64IMAGESTORE_H diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 99f3e61155..1aa3ffa5b3 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -2,20 +2,19 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmclipboard.h" +#include "qwasmdom.h" +#include "qwasmevent.h" #include "qwasmwindow.h" -#include "qwasmstring.h" -#include <private/qstdweb_p.h> -#include <emscripten.h> -#include <emscripten/html5.h> -#include <emscripten/bind.h> -#include <emscripten/val.h> +#include <private/qstdweb_p.h> #include <QCoreApplication> #include <qpa/qwindowsysteminterface.h> #include <QBuffer> #include <QString> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE using namespace emscripten; @@ -27,12 +26,11 @@ static void commonCopyEvent(val event) // doing it this way seems to sanitize the text better that calling data() like down below if (_mimes->hasText()) { - event["clipboardData"].call<void>("setData", val("text/plain") - , QWasmString::fromQString(_mimes->text())); + event["clipboardData"].call<void>("setData", val("text/plain"), + _mimes->text().toEcmaString()); } if (_mimes->hasHtml()) { - event["clipboardData"].call<void>("setData", val("text/html") - , QWasmString::fromQString(_mimes->html())); + event["clipboardData"].call<void>("setData", val("text/html"), _mimes->html().toEcmaString()); } for (auto mimetype : _mimes->formats()) { @@ -40,21 +38,19 @@ static void commonCopyEvent(val event) continue; QByteArray ba = _mimes->data(mimetype); if (!ba.isEmpty()) - event["clipboardData"].call<void>("setData", QWasmString::fromQString(mimetype) - , val(ba.constData())); + event["clipboardData"].call<void>("setData", mimetype.toEcmaString(), + val(ba.constData())); } event.call<void>("preventDefault"); - QWasmIntegration::get()->getWasmClipboard()->m_isListener = false; } static void qClipboardCutTo(val event) { - QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { + if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+X to make the app cut data to Qt's clipboard - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "X"); + QWindowSystemInterface::handleKeyEvent( + 0, QEvent::KeyPress, Qt::Key_X, Qt::ControlModifier, "X"); } commonCopyEvent(event); @@ -62,80 +58,19 @@ static void qClipboardCutTo(val event) static void qClipboardCopyTo(val event) { - QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; - - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) { + if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi()) { // Send synthetic Ctrl+C to make the app copy data to Qt's clipboard - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( + QWindowSystemInterface::handleKeyEvent( 0, QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier, "C"); } commonCopyEvent(event); } -static void qClipboardPasteTo(val dataTransfer) +static void qClipboardPasteTo(val event) { - QWasmIntegration::get()->getWasmClipboard()->m_isListener = true; - val clipboardData = dataTransfer["clipboardData"]; - val types = clipboardData["types"]; - int typesCount = types["length"].as<int>(); - std::string stdMimeFormat; - QMimeData *mMimeData = new QMimeData; - for (int i = 0; i < typesCount; i++) { - stdMimeFormat = types[i].as<std::string>(); - QString mimeFormat = QString::fromStdString(stdMimeFormat); - if (mimeFormat.contains("STRING", Qt::CaseSensitive) || mimeFormat.contains("TEXT", Qt::CaseSensitive)) - continue; - - if (mimeFormat.contains("text")) { -// also "text/plain;charset=utf-8" -// "UTF8_STRING" "MULTIPLE" - val mimeData = clipboardData.call<val>("getData", val(stdMimeFormat)); // as DataTransfer - - const QString qstr = QWasmString::toQString(mimeData); + event.call<void>("preventDefault"); // prevent browser from handling drop event - if (qstr.length() > 0) { - if (mimeFormat.contains("text/html")) { - mMimeData->setHtml(qstr); - } else if (mimeFormat.isEmpty() || mimeFormat.contains("text/plain")) { - mMimeData->setText(qstr); // the type can be empty - } else { - mMimeData->setData(mimeFormat, qstr.toLocal8Bit());} - } - } else { - val items = clipboardData["items"]; - - int itemsCount = items["length"].as<int>(); - // handle data - for (int i = 0; i < itemsCount; i++) { - val item = items[i]; - val clipboardFile = item.call<emscripten::val>("getAsFile"); // string kind is handled above - if (clipboardFile.isUndefined() || item["kind"].as<std::string>() == "string" ) { - continue; - } - qstdweb::File file(clipboardFile); - - mimeFormat = QString::fromStdString(file.type()); - QByteArray fileContent; - fileContent.resize(file.size()); - - file.stream(fileContent.data(), [=]() { - if (!fileContent.isEmpty()) { - - if (mimeFormat.contains("image")) { - QImage image; - image.loadFromData(fileContent, nullptr); - mMimeData->setImageData(image); - } else { - mMimeData->setData(mimeFormat,fileContent.data()); - } - QWasmClipboard::qWasmClipboardPaste(mMimeData); - } - }); - } // next item - } - } - QWasmClipboard::qWasmClipboardPaste(mMimeData); - QWasmIntegration::get()->getWasmClipboard()->m_isListener = false; + QWasmIntegration::get()->getWasmClipboard()->sendClipboardData(event); } EMSCRIPTEN_BINDINGS(qtClipboardModule) { @@ -144,26 +79,14 @@ EMSCRIPTEN_BINDINGS(qtClipboardModule) { function("qtClipboardPasteTo", &qClipboardPasteTo); } -QWasmClipboard::QWasmClipboard() : - isPaste(false), - m_isListener(false) +QWasmClipboard::QWasmClipboard() { val clipboard = val::global("navigator")["clipboard"]; - val permissions = val::global("navigator")["permissions"]; - val hasInstallTrigger = val::global("window")["InstallTrigger"]; - - hasPermissionsApi = !permissions.isUndefined(); - hasClipboardApi = (!clipboard.isUndefined() && !clipboard["readText"].isUndefined()); - bool isFirefox = !hasInstallTrigger.isUndefined(); - isSafari = !emscripten::val::global("window")["safari"].isUndefined(); - - // firefox has clipboard API if user sets these config tweaks: - // dom.events.asyncClipboard.clipboardItem true - // dom.events.asyncClipboard.read true - // dom.events.testing.asyncClipboard - // and permissions API, but does not currently support - // the clipboardRead and clipboardWrite permissions - if (hasClipboardApi && hasPermissionsApi && !isFirefox) + + const bool hasPermissionsApi = !val::global("navigator")["permissions"].isUndefined(); + m_hasClipboardApi = !clipboard.isUndefined() && !clipboard["readText"].isUndefined(); + + if (m_hasClipboardApi && hasPermissionsApi) initClipboardPermissions(); } @@ -181,16 +104,27 @@ QMimeData *QWasmClipboard::mimeData(QClipboard::Mode mode) void QWasmClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) { - QPlatformClipboard::setMimeData(mimeData, mode); // handle setText/ setData programmatically - if (!isPaste) { - if (hasClipboardApi) { - writeToClipboardApi(); - } else if (!m_isListener) { - writeToClipboard(mimeData); - } - } - isPaste = false; + QPlatformClipboard::setMimeData(mimeData, mode); + if (m_hasClipboardApi) + writeToClipboardApi(); + else + writeToClipboard(); +} + +QWasmClipboard::ProcessKeyboardResult QWasmClipboard::processKeyboard(const KeyEvent &event) +{ + if (event.type != EventType::KeyDown || !event.modifiers.testFlag(Qt::ControlModifier)) + return ProcessKeyboardResult::Ignored; + + if (event.key != Qt::Key_C && event.key != Qt::Key_V && event.key != Qt::Key_X) + return ProcessKeyboardResult::Ignored; + + const bool isPaste = event.key == Qt::Key_V; + + return m_hasClipboardApi && !isPaste + ? ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded + : ProcessKeyboardResult::NativeClipboardEventNeeded; } bool QWasmClipboard::supportsMode(QClipboard::Mode mode) const @@ -204,38 +138,31 @@ bool QWasmClipboard::ownsMode(QClipboard::Mode mode) const return false; } -void QWasmClipboard::qWasmClipboardPaste(QMimeData *mData) -{ - QWasmIntegration::get()->clipboard()->setMimeData(mData, QClipboard::Clipboard); - - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier, "V"); -} - void QWasmClipboard::initClipboardPermissions() { - if (!hasClipboardApi) - return; - val permissions = val::global("navigator")["permissions"]; - val readPermissionsMap = val::object(); - readPermissionsMap.set("name", val("clipboard-read")); - permissions.call<val>("query", readPermissionsMap); - val writePermissionsMap = val::object(); - writePermissionsMap.set("name", val("clipboard-write")); - permissions.call<val>("query", writePermissionsMap); + qstdweb::Promise::make(permissions, "query", { .catchFunc = [](emscripten::val) {} }, ([]() { + val readPermissionsMap = val::object(); + readPermissionsMap.set("name", val("clipboard-read")); + return readPermissionsMap; + })()); + qstdweb::Promise::make(permissions, "query", { .catchFunc = [](emscripten::val) {} }, ([]() { + val readPermissionsMap = val::object(); + readPermissionsMap.set("name", val("clipboard-write")); + return readPermissionsMap; + })()); } -void QWasmClipboard::installEventHandlers(const emscripten::val &canvas) +void QWasmClipboard::installEventHandlers(const emscripten::val &target) { emscripten::val cContext = val::undefined(); emscripten::val isChromium = val::global("window")["chrome"]; - if (!isChromium.isUndefined()) { + if (!isChromium.isUndefined()) { cContext = val::global("document"); - } else { - cContext = canvas; - } + } else { + cContext = target; + } // Fallback path for browsers which do not support direct clipboard access cContext.call<void>("addEventListener", val("cut"), val::module_property("qtClipboardCutTo"), true); @@ -245,16 +172,20 @@ void QWasmClipboard::installEventHandlers(const emscripten::val &canvas) val::module_property("qtClipboardPasteTo"), true); } +bool QWasmClipboard::hasClipboardApi() +{ + return m_hasClipboardApi; +} + void QWasmClipboard::writeToClipboardApi() { - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi) - return; + Q_ASSERT(m_hasClipboardApi); // copy event // browser event handler detected ctrl c if clipboard API // or Qt call from keyboard event handler - QMimeData *_mimes = QWasmIntegration::get()->getWasmClipboard()->mimeData(QClipboard::Clipboard); + QMimeData *_mimes = mimeData(QClipboard::Clipboard); if (!_mimes) return; @@ -312,12 +243,12 @@ void QWasmClipboard::writeToClipboardApi() // we have a blob, now create a ClipboardItem emscripten::val type = emscripten::val::array(); - type.set("type", val(QWasmString::fromQString(mimetype))); + type.set("type", mimetype.toEcmaString()); emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type); emscripten::val clipboardItemObject = emscripten::val::object(); - clipboardItemObject.set(val(QWasmString::fromQString(mimetype)), contentBlob); + clipboardItemObject.set(mimetype.toEcmaString(), contentBlob); val clipboardItemData = val::global("ClipboardItem").new_(clipboardItemObject); @@ -342,9 +273,8 @@ void QWasmClipboard::writeToClipboardApi() clipboardWriteArray); } -void QWasmClipboard::writeToClipboard(const QMimeData *data) +void QWasmClipboard::writeToClipboard() { - Q_UNUSED(data) // this works for firefox, chrome by generating // copy event, but not safari // execCommand has been deemed deprecated in the docs, but browsers do not seem @@ -352,4 +282,23 @@ void QWasmClipboard::writeToClipboard(const QMimeData *data) val document = val::global("document"); document.call<val>("execCommand", val("copy")); } + +void QWasmClipboard::sendClipboardData(emscripten::val event) +{ + qDebug() << "sendClipboardData"; + + dom::DataTransfer *transfer = new dom::DataTransfer(event["clipboardData"]); + const auto mimeCallback = std::function([transfer](QMimeData *data) { + + // Persist clipboard data so that the app can read it when handling the CTRL+V + QWasmIntegration::get()->clipboard()->QPlatformClipboard::setMimeData(data, QClipboard::Clipboard); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_V, + Qt::ControlModifier, "V"); + QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyRelease, Qt::Key_V, + Qt::ControlModifier, "V"); + delete transfer; + }); + + transfer->toMimeDataWithFile(mimeCallback); +} QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmclipboard.h b/src/plugins/platforms/wasm/qwasmclipboard.h index ef11fd2e49..86618dd560 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.h +++ b/src/plugins/platforms/wasm/qwasmclipboard.h @@ -7,6 +7,7 @@ #include <QObject> #include <qpa/qplatformclipboard.h> +#include <private/qstdweb_p.h> #include <QMimeData> #include <emscripten/bind.h> @@ -14,9 +15,17 @@ QT_BEGIN_NAMESPACE +struct KeyEvent; + class QWasmClipboard : public QObject, public QPlatformClipboard { public: + enum class ProcessKeyboardResult { + Ignored, + NativeClipboardEventNeeded, + NativeClipboardEventAndCopiedDataNeeded, + }; + QWasmClipboard(); virtual ~QWasmClipboard(); @@ -26,16 +35,17 @@ public: bool supportsMode(QClipboard::Mode mode) const override; bool ownsMode(QClipboard::Mode mode) const override; - static void qWasmClipboardPaste(QMimeData *mData); + ProcessKeyboardResult processKeyboard(const KeyEvent &event); + static void installEventHandlers(const emscripten::val &target); + bool hasClipboardApi(); + void sendClipboardData(emscripten::val event); + +private: void initClipboardPermissions(); - void installEventHandlers(const emscripten::val &canvas); - bool hasClipboardApi; - bool hasPermissionsApi; void writeToClipboardApi(); - void writeToClipboard(const QMimeData *data); - bool isPaste; - bool m_isListener; - bool isSafari; + void writeToClipboard(); + + bool m_hasClipboardApi = false; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp index 7787367e01..ef460f666f 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.cpp +++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp @@ -2,176 +2,42 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmcompositor.h" -#include "qwasmstylepixmaps_p.h" #include "qwasmwindow.h" -#include "qwasmeventtranslator.h" -#include "qwasmeventdispatcher.h" -#include "qwasmclipboard.h" -#include <QtOpenGL/qopengltexture.h> - -#include <QtGui/private/qwindow_p.h> -#include <QtGui/qopenglcontext.h> -#include <QtGui/qopenglfunctions.h> -#include <QtGui/qoffscreensurface.h> -#include <QtGui/qpainter.h> -#include <private/qpixmapcache_p.h> - -#include <private/qguiapplication_p.h> +#include <private/qeventdispatcher_wasm_p.h> #include <qpa/qwindowsysteminterface.h> -#include <QtCore/qcoreapplication.h> -#include <QtGui/qguiapplication.h> -#include <emscripten/bind.h> +#include <emscripten/html5.h> using namespace emscripten; -Q_GUI_EXPORT int qt_defaultDpiX(); - -QWasmCompositedWindow::QWasmCompositedWindow() - : window(nullptr) - , parentWindow(nullptr) - , flushPending(false) - , visible(false) -{ -} - -bool g_scrollingInvertedFromDevice = false; +bool QWasmCompositor::m_requestUpdateHoldEnabled = true; -static void mouseWheelEvent(emscripten::val event) +QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) { - emscripten::val wheelInverted = event["webkitDirectionInvertedFromDevice"]; - if (wheelInverted.as<bool>()) - g_scrollingInvertedFromDevice = true; -} - -EMSCRIPTEN_BINDINGS(qtMouseModule) { - function("qtMouseWheelEvent", &mouseWheelEvent); -} - -QWasmCompositor::QWasmCompositor(QWasmScreen *screen) - :QObject(screen) - , m_blitter(new QOpenGLTextureBlitter) - , m_needComposit(false) - , m_inFlush(false) - , m_inResize(false) - , m_isEnabled(true) - , m_targetDevicePixelRatio(1) - , draggedWindow(nullptr) - , lastWindow(nullptr) - , pressedButtons(Qt::NoButton) - , resizeMode(QWasmCompositor::ResizeNone) - , eventTranslator(new QWasmEventTranslator()) - , mouseInCanvas(false) -{ - touchDevice = new QPointingDevice( - "touchscreen", 1, QInputDevice::DeviceType::TouchScreen, - QPointingDevice::PointerType::Finger, - QPointingDevice::Capability::Position | QPointingDevice::Capability::Area - | QPointingDevice::Capability::NormalizedPosition, - 10, 0); - QWindowSystemInterface::registerInputDevice(touchDevice); + QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } QWasmCompositor::~QWasmCompositor() { - windowUnderMouse.clear(); - if (m_requestAnimationFrameId != -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); - deregisterEventHandlers(); - destroy(); -} - -void QWasmCompositor::deregisterEventHandlers() -{ - QByteArray canvasSelector = screen()->canvasTargetId().toUtf8(); - emscripten_set_keydown_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_keyup_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_mousedown_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_mouseup_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_mousemove_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_mouseenter_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_mouseleave_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_focus_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_wheel_callback(canvasSelector.constData(), 0, 0, NULL); - - emscripten_set_touchstart_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_touchend_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_touchmove_callback(canvasSelector.constData(), 0, 0, NULL); - emscripten_set_touchcancel_callback(canvasSelector.constData(), 0, 0, NULL); - - val canvas = screen()->canvas(); - canvas.call<void>("removeEventListener", - std::string("drop"), - val::module_property("qtDrop"), val(true)); -} - -void QWasmCompositor::destroy() -{ - // Destroy OpenGL resources. This is done here in a separate function - // which can be called while screen() still returns a valid screen - // (which it might not, during destruction). A valid QScreen is - // a requirement for QOffscreenSurface on Wasm since the native - // context is tied to a single canvas. - if (m_context) { - QOffscreenSurface offScreenSurface(screen()->screen()); - offScreenSurface.setFormat(m_context->format()); - offScreenSurface.create(); - m_context->makeCurrent(&offScreenSurface); - for (QWasmWindow *window : m_windowStack) - window->destroy(); - m_blitter.reset(nullptr); - m_context.reset(nullptr); - } - + // TODO(mikolaj.boc): Investigate if m_isEnabled is needed at all. It seems like a frame should + // not be generated after this instead. m_isEnabled = false; // prevent frame() from creating a new m_context } -void QWasmCompositor::initEventHandlers() +void QWasmCompositor::onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindow *window) { - QByteArray canvasSelector = screen()->canvasTargetId().toUtf8(); - - eventTranslator->g_usePlatformMacSpecifics - = (QWasmIntegration::get()->platform == QWasmIntegration::MacOSPlatform); - if (QWasmIntegration::get()->platform == QWasmIntegration::MacOSPlatform) { - if (!emscripten::val::global("window")["safari"].isUndefined()) { - val canvas = screen()->canvas(); - canvas.call<void>("addEventListener", - val("wheel"), - val::module_property("qtMouseWheelEvent")); - } - } - - emscripten_set_keydown_callback(canvasSelector.constData(), (void *)this, 1, &keyboard_cb); - emscripten_set_keyup_callback(canvasSelector.constData(), (void *)this, 1, &keyboard_cb); - - emscripten_set_mousedown_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - emscripten_set_mouseup_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - emscripten_set_mousemove_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - emscripten_set_mouseenter_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - emscripten_set_mouseleave_callback(canvasSelector.constData(), (void *)this, 1, &mouse_cb); - - emscripten_set_focus_callback(canvasSelector.constData(), (void *)this, 1, &focus_cb); - - emscripten_set_wheel_callback(canvasSelector.constData(), (void *)this, 1, &wheel_cb); - - emscripten_set_touchstart_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - emscripten_set_touchend_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - emscripten_set_touchmove_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - emscripten_set_touchcancel_callback(canvasSelector.constData(), (void *)this, 1, &touchCallback); - - val canvas = screen()->canvas(); - canvas.call<void>("addEventListener", - std::string("drop"), - val::module_property("qtDrop"), val(true)); - canvas.set("data-qtdropcontext", // ? unique - emscripten::val(quintptr(reinterpret_cast<void *>(screen())))); + auto allWindows = screen()->allWindows(); + setEnabled(std::any_of(allWindows.begin(), allWindows.end(), [](QWasmWindow *element) { + return !element->context2d().isUndefined(); + })); + if (changeType == QWasmWindowTreeNodeChangeType::NodeRemoval) + m_requestUpdateWindows.remove(window); } void QWasmCompositor::setEnabled(bool enabled) @@ -179,233 +45,14 @@ void QWasmCompositor::setEnabled(bool enabled) m_isEnabled = enabled; } -void QWasmCompositor::addWindow(QWasmWindow *window, QWasmWindow *parentWindow) -{ - QWasmCompositedWindow compositedWindow; - compositedWindow.window = window; - compositedWindow.parentWindow = parentWindow; - m_compositedWindows.insert(window, compositedWindow); - - if (parentWindow == 0) - m_windowStack.append(window); - else - m_compositedWindows[parentWindow].childWindows.append(window); - - notifyTopWindowChanged(window); -} - -void QWasmCompositor::removeWindow(QWasmWindow *window) -{ - QWasmWindow *platformWindow = m_compositedWindows[window].parentWindow; - - if (platformWindow) { - QWasmWindow *parentWindow = window; - m_compositedWindows[parentWindow].childWindows.removeAll(window); - } - - m_windowStack.removeAll(window); - m_compositedWindows.remove(window); - m_requestUpdateWindows.remove(window); - - if (!m_windowStack.isEmpty() && !QGuiApplication::focusWindow()) { - auto lastWindow = m_windowStack.last(); - lastWindow->requestActivateWindow(); - notifyTopWindowChanged(lastWindow); - } -} - -void QWasmCompositor::setVisible(QWasmWindow *window, bool visible) +// requestUpdate delivery is initially disabled at startup, while Qt completes +// startup tasks such as font loading. This function enables requestUpdate delivery +// again. +bool QWasmCompositor::releaseRequestUpdateHold() { - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - if (compositedWindow.visible == visible) - return; - - compositedWindow.visible = visible; - compositedWindow.flushPending = true; - if (visible) - compositedWindow.damage = compositedWindow.window->geometry(); - else - m_globalDamage = compositedWindow.window->geometry(); // repaint previously covered area. - - requestUpdateWindow(window, QWasmCompositor::ExposeEventDelivery); -} - -void QWasmCompositor::raise(QWasmWindow *window) -{ - if (m_compositedWindows.size() <= 1) - return; - - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - compositedWindow.damage = compositedWindow.window->geometry(); - m_windowStack.removeAll(window); - m_windowStack.append(window); - - notifyTopWindowChanged(window); -} - -void QWasmCompositor::lower(QWasmWindow *window) -{ - if (m_compositedWindows.size() <= 1) - return; - - m_windowStack.removeAll(window); - m_windowStack.prepend(window); - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - m_globalDamage = compositedWindow.window->geometry(); // repaint previously covered area. - - notifyTopWindowChanged(window); -} - -void QWasmCompositor::setParent(QWasmWindow *window, QWasmWindow *parent) -{ - m_compositedWindows[window].parentWindow = parent; - - requestUpdate(); -} - -int QWasmCompositor::windowCount() const -{ - return m_windowStack.count(); -} - -QWindow *QWasmCompositor::windowAt(QPoint globalPoint, int padding) const -{ - int index = m_windowStack.count() - 1; - // qDebug() << "window at" << "point" << p << "window count" << index; - - while (index >= 0) { - const QWasmCompositedWindow &compositedWindow = m_compositedWindows[m_windowStack.at(index)]; - //qDebug() << "windwAt testing" << compositedWindow.window << - - QRect geometry = compositedWindow.window->windowFrameGeometry() - .adjusted(-padding, -padding, padding, padding); - - if (compositedWindow.visible && geometry.contains(globalPoint)) - return m_windowStack.at(index)->window(); - --index; - } - - return 0; -} - -QWindow *QWasmCompositor::keyWindow() const -{ - return m_windowStack.at(m_windowStack.count() - 1)->window(); -} - -void QWasmCompositor::blit(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, const QOpenGLTexture *texture, QRect targetGeometry) -{ - QMatrix4x4 m; - m.translate(-1.0f, -1.0f); - - m.scale(2.0f / (float)screen->geometry().width(), - 2.0f / (float)screen->geometry().height()); - - m.translate((float)targetGeometry.width() / 2.0f, - (float)-targetGeometry.height() / 2.0f); - - m.translate(targetGeometry.x(), screen->geometry().height() - targetGeometry.y()); - - m.scale(0.5f * (float)targetGeometry.width(), - 0.5f * (float)targetGeometry.height()); - - blitter->blit(texture->textureId(), m, QOpenGLTextureBlitter::OriginTopLeft); -} - -void QWasmCompositor::drawWindowContent(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window) -{ - QWasmBackingStore *backingStore = window->backingStore(); - if (!backingStore) - return; - - QOpenGLTexture const *texture = backingStore->getUpdatedTexture(); - QRect windowCanvasGeometry = window->geometry().translated(-screen->geometry().topLeft()); - blit(blitter, screen, texture, windowCanvasGeometry); -} - -QPalette QWasmCompositor::makeWindowPalette() -{ - QPalette palette; - palette.setColor(QPalette::Active, QPalette::Highlight, - palette.color(QPalette::Active, QPalette::Highlight)); - palette.setColor(QPalette::Active, QPalette::Base, - palette.color(QPalette::Active, QPalette::Highlight)); - palette.setColor(QPalette::Inactive, QPalette::Highlight, - palette.color(QPalette::Inactive, QPalette::Dark)); - palette.setColor(QPalette::Inactive, QPalette::Base, - palette.color(QPalette::Inactive, QPalette::Dark)); - palette.setColor(QPalette::Inactive, QPalette::HighlightedText, - palette.color(QPalette::Inactive, QPalette::Window)); - - return palette; -} - -QRect QWasmCompositor::titlebarRect(QWasmTitleBarOptions tb, QWasmCompositor::SubControls subcontrol) -{ - QRect ret; - const int controlMargin = 2; - const int controlHeight = tb.rect.height() - controlMargin *2; - const int delta = controlHeight + controlMargin; - int offset = 0; - - bool isMinimized = tb.state & Qt::WindowMinimized; - bool isMaximized = tb.state & Qt::WindowMaximized; - - ret = tb.rect; - switch (subcontrol) { - case SC_TitleBarLabel: - if (tb.flags & Qt::WindowSystemMenuHint) - ret.adjust(delta, 0, -delta, 0); - break; - case SC_TitleBarCloseButton: - if (tb.flags & Qt::WindowSystemMenuHint) { - ret.adjust(0, 0, -delta, 0); - offset += delta; - } - break; - case SC_TitleBarMaxButton: - if (!isMaximized && tb.flags & Qt::WindowMaximizeButtonHint) { - ret.adjust(0, 0, -delta*2, 0); - offset += (delta +delta); - } - break; - case SC_TitleBarNormalButton: - if (isMinimized && (tb.flags & Qt::WindowMinimizeButtonHint)) { - offset += delta; - } else if (isMaximized && (tb.flags & Qt::WindowMaximizeButtonHint)) { - ret.adjust(0, 0, -delta*2, 0); - offset += (delta +delta); - } - break; - case SC_TitleBarSysMenu: - if (tb.flags & Qt::WindowSystemMenuHint) { - ret.setRect(tb.rect.left() + controlMargin, tb.rect.top() + controlMargin, - controlHeight, controlHeight); - } - break; - default: - break; - }; - - if (subcontrol != SC_TitleBarLabel && subcontrol != SC_TitleBarSysMenu) { - ret.setRect(tb.rect.right() - offset, tb.rect.top() + controlMargin, - controlHeight, controlHeight); - } - - if (qApp->layoutDirection() == Qt::LeftToRight) - return ret; - - QRect rect = ret; - rect.translate(2 * (tb.rect.right() - ret.right()) + - ret.width() - tb.rect.width(), 0); - - return rect; -} - -void QWasmCompositor::requestUpdateAllWindows() -{ - m_requestUpdateAllWindows = true; - requestUpdate(); + const bool wasEnabled = m_requestUpdateHoldEnabled; + m_requestUpdateHoldEnabled = false; + return wasEnabled; } void QWasmCompositor::requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType) @@ -429,11 +76,17 @@ void QWasmCompositor::requestUpdate() if (m_requestAnimationFrameId != -1) return; + if (m_requestUpdateHoldEnabled) + return; + static auto frame = [](double frameTime, void *context) -> int { Q_UNUSED(frameTime); + QWasmCompositor *compositor = reinterpret_cast<QWasmCompositor *>(context); + compositor->m_requestAnimationFrameId = -1; compositor->deliverUpdateRequests(); + return 0; }; m_requestAnimationFrameId = emscripten_request_animation_frame(frame, this); @@ -446,899 +99,61 @@ void QWasmCompositor::deliverUpdateRequests() // update set. auto requestUpdateWindows = m_requestUpdateWindows; m_requestUpdateWindows.clear(); - bool requestUpdateAllWindows = m_requestUpdateAllWindows; - m_requestUpdateAllWindows = false; - // Update window content, either all windows or a spesific set of windows. Use the correct update - // type: QWindow subclasses expect that requested and delivered updateRequests matches exactly. + // Update window content, either all windows or a spesific set of windows. Use the correct + // update type: QWindow subclasses expect that requested and delivered updateRequests matches + // exactly. m_inDeliverUpdateRequest = true; - if (requestUpdateAllWindows) { - for (QWasmWindow *window : m_windowStack) { - auto it = requestUpdateWindows.find(window); - UpdateRequestDeliveryType updateType = - (it == m_requestUpdateWindows.end() ? ExposeEventDelivery : it.value()); - deliverUpdateRequest(window, updateType); - } - } else { - for (auto it = requestUpdateWindows.constBegin(); it != requestUpdateWindows.constEnd(); ++it) { - auto *window = it.key(); - UpdateRequestDeliveryType updateType = it.value(); - deliverUpdateRequest(window, updateType); - } + for (auto it = requestUpdateWindows.constBegin(); it != requestUpdateWindows.constEnd(); ++it) { + auto *window = it.key(); + UpdateRequestDeliveryType updateType = it.value(); + deliverUpdateRequest(window, updateType); } - m_inDeliverUpdateRequest = false; - // Compose window content - frame(); + m_inDeliverUpdateRequest = false; + frame(requestUpdateWindows.keys()); } void QWasmCompositor::deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType) { - // update by deliverUpdateRequest and expose event accordingly. + QWindow *qwindow = window->window(); + + // Make sure the DPR value for the window is up to date on expose/repaint. + // FIXME: listen to native DPR change events instead, if/when available. + QWindowSystemInterface::handleWindowDevicePixelRatioChanged(qwindow); + + // Update by deliverUpdateRequest and expose event according to requested update + // type. If the window has not yet been exposed then we must expose it first regardless + // of update type. The deliverUpdateRequest must still be sent in this case in order + // to maintain correct window update state. + QRect updateRect(QPoint(0, 0), qwindow->geometry().size()); if (updateType == UpdateRequestDelivery) { - window->QPlatformWindow::deliverUpdateRequest(); + if (qwindow->isExposed() == false) + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); + window->deliverUpdateRequest(); } else { - QWindow *qwindow = window->window(); - QWindowSystemInterface::handleExposeEvent<QWindowSystemInterface::SynchronousDelivery>( - qwindow, QRect(QPoint(0, 0), qwindow->geometry().size())); + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); } } -void QWasmCompositor::handleBackingStoreFlush() +void QWasmCompositor::handleBackingStoreFlush(QWindow *window) { - // Request update to flush the updated backing store content, - // unless we are currently processing an update, in which case - // the new content will flushed as a part of that update. + // Request update to flush the updated backing store content, unless we are currently + // processing an update, in which case the new content will flushed as a part of that update. if (!m_inDeliverUpdateRequest) - requestUpdate(); -} - -int dpiScaled(qreal value) -{ - return value * (qreal(qt_defaultDpiX()) / 96.0); -} - -QWasmCompositor::QWasmTitleBarOptions QWasmCompositor::makeTitleBarOptions(const QWasmWindow *window) -{ - int width = window->windowFrameGeometry().width(); - int border = window->borderWidth(); - - QWasmTitleBarOptions titleBarOptions; - - titleBarOptions.rect = QRect(border, border, width - 2 * border, window->titleHeight()); - titleBarOptions.flags = window->window()->flags(); - titleBarOptions.state = window->window()->windowState(); - - bool isMaximized = titleBarOptions.state & Qt::WindowMaximized; // this gets reset when maximized - - if (titleBarOptions.flags & (Qt::WindowTitleHint)) - titleBarOptions.subControls |= SC_TitleBarLabel; - if (titleBarOptions.flags & Qt::WindowMaximizeButtonHint) { - if (isMaximized) - titleBarOptions.subControls |= SC_TitleBarNormalButton; - else - titleBarOptions.subControls |= SC_TitleBarMaxButton; - } - if (titleBarOptions.flags & Qt::WindowSystemMenuHint) { - titleBarOptions.subControls |= SC_TitleBarCloseButton; - titleBarOptions.subControls |= SC_TitleBarSysMenu; - } - - - titleBarOptions.palette = QWasmCompositor::makeWindowPalette(); - - if (window->window()->isActive()) - titleBarOptions.palette.setCurrentColorGroup(QPalette::Active); - else - titleBarOptions.palette.setCurrentColorGroup(QPalette::Inactive); - - if (window->activeSubControl() != QWasmCompositor::SC_None) - titleBarOptions.subControls = window->activeSubControl(); - - if (!window->window()->title().isEmpty()) - titleBarOptions.titleBarOptionsString = window->window()->title(); - - titleBarOptions.windowIcon = window->window()->icon(); - - return titleBarOptions; -} - -void QWasmCompositor::drawWindowDecorations(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window) -{ - int width = window->windowFrameGeometry().width(); - int height = window->windowFrameGeometry().height(); - qreal dpr = window->devicePixelRatio(); - - QImage image(QSize(width * dpr, height * dpr), QImage::Format_RGB32); - image.setDevicePixelRatio(dpr); - QPainter painter(&image); - painter.fillRect(QRect(0, 0, width, height), painter.background()); - - QWasmTitleBarOptions titleBarOptions = makeTitleBarOptions(window); - - drawTitlebarWindow(titleBarOptions, &painter); - - QWasmFrameOptions frameOptions; - frameOptions.rect = QRect(0, 0, width, height); - frameOptions.lineWidth = dpiScaled(4.); - - drawFrameWindow(frameOptions, &painter); - - painter.end(); - - QOpenGLTexture texture(QOpenGLTexture::Target2D); - texture.setMinificationFilter(QOpenGLTexture::Nearest); - texture.setMagnificationFilter(QOpenGLTexture::Nearest); - texture.setWrapMode(QOpenGLTexture::ClampToEdge); - texture.setData(image, QOpenGLTexture::DontGenerateMipMaps); - texture.create(); - texture.bind(); - - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.width(), image.height(), GL_RGBA, GL_UNSIGNED_BYTE, - image.constScanLine(0)); - - QRect windowCanvasGeometry = window->windowFrameGeometry().translated(-screen->geometry().topLeft()); - blit(blitter, screen, &texture, windowCanvasGeometry); -} - -void QWasmCompositor::drawFrameWindow(QWasmFrameOptions options, QPainter *painter) -{ - int x = options.rect.x(); - int y = options.rect.y(); - int w = options.rect.width(); - int h = options.rect.height(); - const QColor &c1 = options.palette.light().color(); - const QColor &c2 = options.palette.shadow().color(); - const QColor &c3 = options.palette.midlight().color(); - const QColor &c4 = options.palette.dark().color(); - const QBrush *fill = nullptr; - - const qreal devicePixelRatio = painter->device()->devicePixelRatio(); - if (!qFuzzyCompare(devicePixelRatio, qreal(1))) { - const qreal inverseScale = qreal(1) / devicePixelRatio; - painter->scale(inverseScale, inverseScale); - x = qRound(devicePixelRatio * x); - y = qRound(devicePixelRatio * y); - w = qRound(devicePixelRatio * w); - h = qRound(devicePixelRatio * h); - } - - QPen oldPen = painter->pen(); - QPoint a[3] = { QPoint(x, y+h-2), QPoint(x, y), QPoint(x+w-2, y) }; - painter->setPen(c1); - painter->drawPolyline(a, 3); - QPoint b[3] = { QPoint(x, y+h-1), QPoint(x+w-1, y+h-1), QPoint(x+w-1, y) }; - painter->setPen(c2); - painter->drawPolyline(b, 3); - if (w > 4 && h > 4) { - QPoint c[3] = { QPoint(x+1, y+h-3), QPoint(x+1, y+1), QPoint(x+w-3, y+1) }; - painter->setPen(c3); - painter->drawPolyline(c, 3); - QPoint d[3] = { QPoint(x+1, y+h-2), QPoint(x+w-2, y+h-2), QPoint(x+w-2, y+1) }; - painter->setPen(c4); - painter->drawPolyline(d, 3); - if (fill) - painter->fillRect(QRect(x+2, y+2, w-4, h-4), *fill); - } - painter->setPen(oldPen); -} - -//from commonstyle.cpp -static QPixmap cachedPixmapFromXPM(const char * const *xpm) -{ - QPixmap result; - const QString tag = QString::asprintf("xpm:0x%p", static_cast<const void*>(xpm)); - if (!QPixmapCache::find(tag, &result)) { - result = QPixmap(xpm); - QPixmapCache::insert(tag, result); - } - return result; -} - -void QWasmCompositor::drawItemPixmap(QPainter *painter, const QRect &rect, int alignment, - const QPixmap &pixmap) const -{ - qreal scale = pixmap.devicePixelRatio(); - QSize size = pixmap.size() / scale; - int x = rect.x(); - int y = rect.y(); - int w = size.width(); - int h = size.height(); - if ((alignment & Qt::AlignVCenter) == Qt::AlignVCenter) - y += rect.size().height()/2 - h/2; - else if ((alignment & Qt::AlignBottom) == Qt::AlignBottom) - y += rect.size().height() - h; - if ((alignment & Qt::AlignRight) == Qt::AlignRight) - x += rect.size().width() - w; - else if ((alignment & Qt::AlignHCenter) == Qt::AlignHCenter) - x += rect.size().width()/2 - w/2; - - QRect aligned = QRect(x, y, w, h); - QRect inter = aligned.intersected(rect); - - painter->drawPixmap(inter.x(), inter.y(), pixmap, inter.x() - aligned.x(), - inter.y() - aligned.y(), inter.width() * scale, inter.height() *scale); -} - - -void QWasmCompositor::drawTitlebarWindow(QWasmTitleBarOptions tb, QPainter *painter) -{ - QRect ir; - if (tb.subControls.testFlag(SC_TitleBarLabel)) { - QColor left = tb.palette.highlight().color(); - QColor right = tb.palette.base().color(); - - QBrush fillBrush(left); - if (left != right) { - QPoint p1(tb.rect.x(), tb.rect.top() + tb.rect.height()/2); - QPoint p2(tb.rect.right(), tb.rect.top() + tb.rect.height()/2); - QLinearGradient lg(p1, p2); - lg.setColorAt(0, left); - lg.setColorAt(1, right); - fillBrush = lg; - } - - painter->fillRect(tb.rect, fillBrush); - ir = titlebarRect(tb, SC_TitleBarLabel); - painter->setPen(tb.palette.highlightedText().color()); - painter->drawText(ir.x() + 2, ir.y(), ir.width() - 2, ir.height(), - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, tb.titleBarOptionsString); - } // SC_TitleBarLabel - - QPixmap pixmap; - - if (tb.subControls.testFlag(SC_TitleBarCloseButton) - && tb.flags & Qt::WindowSystemMenuHint) { - ir = titlebarRect(tb, SC_TitleBarCloseButton); - pixmap = cachedPixmapFromXPM(qt_close_xpm).scaled(QSize(10, 10)); - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } //SC_TitleBarCloseButton - - if (tb.subControls.testFlag(SC_TitleBarMaxButton) - && tb.flags & Qt::WindowMaximizeButtonHint - && !(tb.state & Qt::WindowMaximized)) { - ir = titlebarRect(tb, SC_TitleBarMaxButton); - pixmap = cachedPixmapFromXPM(qt_maximize_xpm).scaled(QSize(10, 10)); - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } //SC_TitleBarMaxButton - - bool drawNormalButton = (tb.subControls & SC_TitleBarNormalButton) - && (((tb.flags & Qt::WindowMinimizeButtonHint) - && (tb.flags & Qt::WindowMinimized)) - || ((tb.flags & Qt::WindowMaximizeButtonHint) - && (tb.flags & Qt::WindowMaximized))); - - if (drawNormalButton) { - ir = titlebarRect(tb, SC_TitleBarNormalButton); - pixmap = cachedPixmapFromXPM(qt_normalizeup_xpm).scaled( QSize(10, 10)); - - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } // SC_TitleBarNormalButton - - if (tb.subControls & SC_TitleBarSysMenu && tb.flags & Qt::WindowSystemMenuHint) { - ir = titlebarRect(tb, SC_TitleBarSysMenu); - if (!tb.windowIcon.isNull()) { - tb.windowIcon.paint(painter, ir, Qt::AlignCenter); - } else { - pixmap = cachedPixmapFromXPM(qt_menu_xpm).scaled(QSize(10, 10)); - drawItemPixmap(painter, ir, Qt::AlignCenter, pixmap); - } - } + requestUpdateWindow(static_cast<QWasmWindow *>(window->handle())); } -void QWasmCompositor::drawShadePanel(QWasmTitleBarOptions options, QPainter *painter) +void QWasmCompositor::frame(const QList<QWasmWindow *> &windows) { - int lineWidth = 1; - QPalette palette = options.palette; - const QBrush *fill = &options.palette.brush(QPalette::Button); - - int x = options.rect.x(); - int y = options.rect.y(); - int w = options.rect.width(); - int h = options.rect.height(); - - const qreal devicePixelRatio = painter->device()->devicePixelRatio(); - if (!qFuzzyCompare(devicePixelRatio, qreal(1))) { - const qreal inverseScale = qreal(1) / devicePixelRatio; - painter->scale(inverseScale, inverseScale); - - x = qRound(devicePixelRatio * x); - y = qRound(devicePixelRatio * y); - w = qRound(devicePixelRatio * w); - h = qRound(devicePixelRatio * h); - lineWidth = qRound(devicePixelRatio * lineWidth); - } - - QColor shade = palette.dark().color(); - QColor light = palette.light().color(); - - if (fill) { - if (fill->color() == shade) - shade = palette.shadow().color(); - if (fill->color() == light) - light = palette.midlight().color(); - } - QPen oldPen = painter->pen(); - QList<QLineF> lines; - lines.reserve(2*lineWidth); - - painter->setPen(light); - int x1, y1, x2, y2; - int i; - x1 = x; - y1 = y2 = y; - x2 = x + w - 2; - for (i = 0; i < lineWidth; i++) // top shadow - lines << QLineF(x1, y1++, x2--, y2++); - - x2 = x1; - y1 = y + h - 2; - for (i = 0; i < lineWidth; i++) // left shado - lines << QLineF(x1++, y1, x2++, y2--); - - painter->drawLines(lines); - lines.clear(); - painter->setPen(shade); - x1 = x; - y1 = y2 = y+h-1; - x2 = x+w-1; - for (i=0; i<lineWidth; i++) { // bottom shadow - lines << QLineF(x1++, y1--, x2, y2--); - } - x1 = x2; - y1 = y; - y2 = y + h - lineWidth - 1; - for (i = 0; i < lineWidth; i++) // right shadow - lines << QLineF(x1--, y1++, x2--, y2); - - painter->drawLines(lines); - if (fill) // fill with fill color - painter->fillRect(x+lineWidth, y+lineWidth, w-lineWidth*2, h-lineWidth*2, *fill); - painter->setPen(oldPen); // restore pen - -} - -void QWasmCompositor::drawWindow(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window) -{ - if (window->window()->type() != Qt::Popup && !(window->m_windowState & Qt::WindowFullScreen)) - drawWindowDecorations(blitter, screen, window); - drawWindowContent(blitter, screen, window); -} - -void QWasmCompositor::frame() -{ - if (!m_isEnabled || m_windowStack.empty() || !screen()) - return; - - QWasmWindow *someWindow = nullptr; - - for (QWasmWindow *window : qAsConst(m_windowStack)) { - if (window->window()->surfaceClass() == QSurface::Window - && qt_window_private(static_cast<QWindow *>(window->window()))->receivedExpose) { - someWindow = window; - break; - } - } - - if (!someWindow) - return; - - if (m_context.isNull()) { - m_context.reset(new QOpenGLContext()); - m_context->setFormat(someWindow->window()->requestedFormat()); - m_context->setScreen(screen()->screen()); - m_context->create(); - } - - bool ok = m_context->makeCurrent(someWindow->window()); - if (!ok) + if (!m_isEnabled || !screen()) return; - if (!m_blitter->isCreated()) - m_blitter->create(); - - qreal dpr = screen()->devicePixelRatio(); - glViewport(0, 0, screen()->geometry().width() * dpr, screen()->geometry().height() * dpr); - - m_context->functions()->glClearColor(0.2, 0.2, 0.2, 1.0); - m_context->functions()->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - - m_blitter->bind(); - m_blitter->setRedBlueSwizzle(true); - - for (QWasmWindow *window : qAsConst(m_windowStack)) { - QWasmCompositedWindow &compositedWindow = m_compositedWindows[window]; - - if (!compositedWindow.visible) - continue; - - drawWindow(m_blitter.data(), screen(), window); - } - - m_blitter->release(); - - if (someWindow && someWindow->window()->surfaceType() == QSurface::OpenGLSurface) - m_context->swapBuffers(someWindow->window()); -} - -void QWasmCompositor::resizeWindow(QWindow *window, QWasmCompositor::ResizeMode mode, - QRect startRect, QPoint amount) -{ - if (mode == QWasmCompositor::ResizeNone) - return; - - bool top = mode == QWasmCompositor::ResizeTopLeft || - mode == QWasmCompositor::ResizeTop || - mode == QWasmCompositor::ResizeTopRight; - - bool bottom = mode == QWasmCompositor::ResizeBottomLeft || - mode == QWasmCompositor::ResizeBottom || - mode == QWasmCompositor::ResizeBottomRight; - - bool left = mode == QWasmCompositor::ResizeLeft || - mode == QWasmCompositor::ResizeTopLeft || - mode == QWasmCompositor::ResizeBottomLeft; - - bool right = mode == QWasmCompositor::ResizeRight || - mode == QWasmCompositor::ResizeTopRight || - mode == QWasmCompositor::ResizeBottomRight; - - int x1 = startRect.left(); - int y1 = startRect.top(); - int x2 = startRect.right(); - int y2 = startRect.bottom(); - - if (left) - x1 += amount.x(); - if (top) - y1 += amount.y(); - if (right) - x2 += amount.x(); - if (bottom) - y2 += amount.y(); - - int w = x2-x1; - int h = y2-y1; - - if (w < window->minimumWidth()) { - if (left) - x1 -= window->minimumWidth() - w; - - w = window->minimumWidth(); - } - - if (h < window->minimumHeight()) { - if (top) - y1 -= window->minimumHeight() - h; - - h = window->minimumHeight(); - } - - window->setGeometry(x1, y1, w, h); -} - -void QWasmCompositor::notifyTopWindowChanged(QWasmWindow *window) -{ - QWindow *modalWindow; - bool blocked = QGuiApplicationPrivate::instance()->isWindowBlocked(window->window(), &modalWindow); - - if (blocked) { - modalWindow->requestActivate(); - raise(static_cast<QWasmWindow*>(modalWindow->handle())); - return; - } - - requestUpdate(); + for (QWasmWindow *window : windows) + window->paint(); } QWasmScreen *QWasmCompositor::screen() { return static_cast<QWasmScreen *>(parent()); } - -QOpenGLContext *QWasmCompositor::context() -{ - return m_context.data(); -} - -int QWasmCompositor::keyboard_cb(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData) -{ - QWasmCompositor *wasmCompositor = reinterpret_cast<QWasmCompositor *>(userData); - return static_cast<int>(wasmCompositor->processKeyboard(eventType, keyEvent)); -} - -int QWasmCompositor::mouse_cb(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) -{ - QWasmCompositor *compositor = (QWasmCompositor*)userData; - return static_cast<int>(compositor->processMouse(eventType, mouseEvent)); -} - -int QWasmCompositor::focus_cb(int eventType, const EmscriptenFocusEvent *focusEvent, void *userData) -{ - Q_UNUSED(eventType) - Q_UNUSED(focusEvent) - Q_UNUSED(userData) - - return 0; -} - -int QWasmCompositor::wheel_cb(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData) -{ - QWasmCompositor *compositor = (QWasmCompositor *) userData; - return static_cast<int>(compositor->processWheel(eventType, wheelEvent)); -} - -int QWasmCompositor::touchCallback(int eventType, const EmscriptenTouchEvent *touchEvent, void *userData) -{ - auto compositor = reinterpret_cast<QWasmCompositor*>(userData); - return static_cast<int>(compositor->handleTouch(eventType, touchEvent)); -} - -bool QWasmCompositor::processMouse(int eventType, const EmscriptenMouseEvent *mouseEvent) -{ - QPoint targetPoint(mouseEvent->targetX, mouseEvent->targetY); - QPoint globalPoint = screen()->geometry().topLeft() + targetPoint; - - QEvent::Type buttonEventType = QEvent::None; - Qt::MouseButton button = Qt::NoButton; - Qt::KeyboardModifiers modifiers = eventTranslator->translateMouseEventModifier(mouseEvent); - - QWindow *window2 = nullptr; - if (resizeMode == QWasmCompositor::ResizeNone) - window2 = screen()->compositor()->windowAt(globalPoint, 5); - - if (window2 == nullptr) { - window2 = lastWindow; - } else { - lastWindow = window2; - } - - QPoint localPoint = window2->mapFromGlobal(globalPoint); - bool interior = window2->geometry().contains(globalPoint); - bool blocked = QGuiApplicationPrivate::instance()->isWindowBlocked(window2); - - if (mouseInCanvas) { - if (windowUnderMouse != window2 && interior) { - // delayed mouse enter - enterWindow(window2, localPoint, globalPoint); - windowUnderMouse = window2; - } - } - - QWasmWindow *htmlWindow = static_cast<QWasmWindow*>(window2->handle()); - Qt::WindowStates windowState = htmlWindow->window()->windowState(); - bool isResizable = !(windowState.testFlag(Qt::WindowMaximized) || - windowState.testFlag(Qt::WindowFullScreen)); - - switch (eventType) { - case EMSCRIPTEN_EVENT_MOUSEDOWN: - { - button = QWasmEventTranslator::translateMouseButton(mouseEvent->button); - - if (window2) - window2->requestActivate(); - - pressedButtons.setFlag(button); - - pressedWindow = window2; - buttonEventType = QEvent::MouseButtonPress; - - // button overview: - // 0 = primary mouse button, usually left click - // 1 = middle mouse button, usually mouse wheel - // 2 = right mouse button, usually right click - // from: https://w3c.github.io/uievents/#dom-mouseevent-button - if (mouseEvent->button == 0) { - if (!blocked && !(htmlWindow->m_windowState & Qt::WindowFullScreen) - && !(htmlWindow->m_windowState & Qt::WindowMaximized)) { - if (htmlWindow && window2->flags().testFlag(Qt::WindowTitleHint) - && htmlWindow->isPointOnTitle(globalPoint)) - draggedWindow = window2; - else if (htmlWindow && htmlWindow->isPointOnResizeRegion(globalPoint)) { - draggedWindow = window2; - resizeMode = htmlWindow->resizeModeAtPoint(globalPoint); - resizePoint = globalPoint; - resizeStartRect = window2->geometry(); - } - } - } - - htmlWindow->injectMousePressed(localPoint, globalPoint, button, modifiers); - break; - } - case EMSCRIPTEN_EVENT_MOUSEUP: - { - button = QWasmEventTranslator::translateMouseButton(mouseEvent->button); - pressedButtons.setFlag(button, false); - buttonEventType = QEvent::MouseButtonRelease; - QWasmWindow *oldWindow = nullptr; - - if (mouseEvent->button == 0 && pressedWindow) { - oldWindow = static_cast<QWasmWindow*>(pressedWindow->handle()); - pressedWindow = nullptr; - } - - if (draggedWindow && pressedButtons.testFlag(Qt::NoButton)) { - draggedWindow = nullptr; - resizeMode = QWasmCompositor::ResizeNone; - } - - if (oldWindow) - oldWindow->injectMouseReleased(localPoint, globalPoint, button, modifiers); - else - htmlWindow->injectMouseReleased(localPoint, globalPoint, button, modifiers); - break; - } - case EMSCRIPTEN_EVENT_MOUSEMOVE: // drag event - { - buttonEventType = QEvent::MouseMove; - - if (htmlWindow && pressedButtons.testFlag(Qt::NoButton)) { - - bool isOnResizeRegion = htmlWindow->isPointOnResizeRegion(globalPoint); - - if (isResizable && isOnResizeRegion && !blocked) { - QCursor resizingCursor = eventTranslator->cursorForMode(htmlWindow->resizeModeAtPoint(globalPoint)); - - if (resizingCursor != window2->cursor()) { - isCursorOverridden = true; - QWasmCursor::setOverrideWasmCursor(&resizingCursor, window2->screen()); - } - } else { // off resizing area - if (isCursorOverridden) { - isCursorOverridden = false; - QWasmCursor::clearOverrideWasmCursor(window2->screen()); - } - } - } - - if (!(htmlWindow->m_windowState & Qt::WindowFullScreen) && !(htmlWindow->m_windowState & Qt::WindowMaximized)) { - if (resizeMode == QWasmCompositor::ResizeNone && draggedWindow) { - draggedWindow->setX(draggedWindow->x() + mouseEvent->movementX); - draggedWindow->setY(draggedWindow->y() + mouseEvent->movementY); - } - - if (resizeMode != QWasmCompositor::ResizeNone && !(htmlWindow->m_windowState & Qt::WindowFullScreen)) { - QPoint delta = QPoint(mouseEvent->targetX, mouseEvent->targetY) - resizePoint; - resizeWindow(draggedWindow, resizeMode, resizeStartRect, delta); - } - } - break; - } - case EMSCRIPTEN_EVENT_MOUSEENTER: - processMouseEnter(mouseEvent); - break; - case EMSCRIPTEN_EVENT_MOUSELEAVE: - processMouseLeave(); - break; - default: break; - }; - - if (!interior && pressedButtons.testFlag(Qt::NoButton)) { - leaveWindow(lastWindow); - } - - if (!window2 && buttonEventType == QEvent::MouseButtonRelease) { - window2 = lastWindow; - lastWindow = nullptr; - interior = true; - } - bool accepted = false; - if (window2 && interior) { - accepted = QWindowSystemInterface::handleMouseEvent<QWindowSystemInterface::SynchronousDelivery>( - window2, QWasmIntegration::getTimestamp(), localPoint, globalPoint, pressedButtons, button, buttonEventType, modifiers); - } - - if (eventType == EMSCRIPTEN_EVENT_MOUSEDOWN && !accepted) - QGuiApplicationPrivate::instance()->closeAllPopups(); - return accepted; -} - -bool QWasmCompositor::processKeyboard(int eventType, const EmscriptenKeyboardEvent *keyEvent) -{ - Qt::Key qtKey; - QString keyText; - QEvent::Type keyType = QEvent::None; - switch (eventType) { - case EMSCRIPTEN_EVENT_KEYPRESS: - case EMSCRIPTEN_EVENT_KEYDOWN: // down - keyType = QEvent::KeyPress; - qtKey = this->eventTranslator->getKey(keyEvent); - keyText = this->eventTranslator->getKeyText(keyEvent, qtKey); - break; - case EMSCRIPTEN_EVENT_KEYUP: // up - keyType = QEvent::KeyRelease; - this->eventTranslator->setStickyDeadKey(keyEvent); - break; - default: - break; - }; - - if (keyType == QEvent::None) - return 0; - - QFlags<Qt::KeyboardModifier> modifiers = eventTranslator->translateKeyboardEventModifier(keyEvent); - - // Clipboard fallback path: cut/copy/paste are handled by clipboard event - // handlers if direct clipboard access is not available. - if (!QWasmIntegration::get()->getWasmClipboard()->hasClipboardApi && modifiers & Qt::ControlModifier && - (qtKey == Qt::Key_X || qtKey == Qt::Key_C || qtKey == Qt::Key_V)) { - if (qtKey == Qt::Key_V) { - QWasmIntegration::get()->getWasmClipboard()->isPaste = true; - } - return false; - } - - bool accepted = false; - - if (keyType == QEvent::KeyPress && - modifiers.testFlag(Qt::ControlModifier) - && qtKey == Qt::Key_V) { - QWasmIntegration::get()->getWasmClipboard()->isPaste = true; - accepted = false; // continue on to event - } else { - if (keyText.isEmpty()) - keyText = QString(keyEvent->key); - if (keyText.size() > 1) - keyText.clear(); - accepted = QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, keyType, qtKey, modifiers, keyText); - } - if (keyType == QEvent::KeyPress && - modifiers.testFlag(Qt::ControlModifier) - && qtKey == Qt::Key_C) { - QWasmIntegration::get()->getWasmClipboard()->isPaste = false; - accepted = false; // continue on to event - } - - return accepted; -} - -bool QWasmCompositor::processWheel(int eventType, const EmscriptenWheelEvent *wheelEvent) -{ - Q_UNUSED(eventType); - - EmscriptenMouseEvent mouseEvent = wheelEvent->mouse; - - int scrollFactor = 0; - switch (wheelEvent->deltaMode) { - case DOM_DELTA_PIXEL: - scrollFactor = 1; - break; - case DOM_DELTA_LINE: - scrollFactor = 12; - break; - case DOM_DELTA_PAGE: - scrollFactor = 20; - break; - }; - - scrollFactor = -scrollFactor; // Web scroll deltas are inverted from Qt deltas. - - Qt::KeyboardModifiers modifiers = eventTranslator->translateMouseEventModifier(&mouseEvent); - QPoint targetPoint(mouseEvent.targetX, mouseEvent.targetY); - QPoint globalPoint = screen()->geometry().topLeft() + targetPoint; - - QWindow *window2 = screen()->compositor()->windowAt(globalPoint, 5); - if (!window2) - return 0; - QPoint localPoint = window2->mapFromGlobal(globalPoint); - - QPoint pixelDelta; - - if (wheelEvent->deltaY != 0) pixelDelta.setY(wheelEvent->deltaY * scrollFactor); - if (wheelEvent->deltaX != 0) pixelDelta.setX(wheelEvent->deltaX * scrollFactor); - - QPoint angleDelta = pixelDelta; // FIXME: convert from pixels? - - bool accepted = QWindowSystemInterface::handleWheelEvent( - window2, QWasmIntegration::getTimestamp(), localPoint, - globalPoint, pixelDelta, angleDelta, modifiers, - Qt::NoScrollPhase, Qt::MouseEventNotSynthesized, - g_scrollingInvertedFromDevice); - return accepted; -} - -int QWasmCompositor::handleTouch(int eventType, const EmscriptenTouchEvent *touchEvent) -{ - QList<QWindowSystemInterface::TouchPoint> touchPointList; - touchPointList.reserve(touchEvent->numTouches); - QWindow *window2; - - for (int i = 0; i < touchEvent->numTouches; i++) { - - const EmscriptenTouchPoint *touches = &touchEvent->touches[i]; - - QPoint targetPoint(touches->targetX, touches->targetY); - QPoint globalPoint = screen()->geometry().topLeft() + targetPoint; - - window2 = this->screen()->compositor()->windowAt(globalPoint, 5); - if (window2 == nullptr) - continue; - - QWindowSystemInterface::TouchPoint touchPoint; - - touchPoint.area = QRect(0, 0, 8, 8); - touchPoint.id = touches->identifier; - touchPoint.pressure = 1.0; - - touchPoint.area.moveCenter(globalPoint); - - const auto tp = pressedTouchIds.constFind(touchPoint.id); - if (tp != pressedTouchIds.constEnd()) - touchPoint.normalPosition = tp.value(); - - QPointF localPoint = QPointF(window2->mapFromGlobal(globalPoint)); - QPointF normalPosition(localPoint.x() / window2->width(), - localPoint.y() / window2->height()); - - const bool stationaryTouchPoint = (normalPosition == touchPoint.normalPosition); - touchPoint.normalPosition = normalPosition; - - switch (eventType) { - case EMSCRIPTEN_EVENT_TOUCHSTART: - if (tp != pressedTouchIds.constEnd()) { - touchPoint.state = (stationaryTouchPoint - ? QEventPoint::State::Stationary - : QEventPoint::State::Updated); - } else { - touchPoint.state = QEventPoint::State::Pressed; - } - pressedTouchIds.insert(touchPoint.id, touchPoint.normalPosition); - - break; - case EMSCRIPTEN_EVENT_TOUCHEND: - touchPoint.state = QEventPoint::State::Released; - pressedTouchIds.remove(touchPoint.id); - break; - case EMSCRIPTEN_EVENT_TOUCHMOVE: - touchPoint.state = (stationaryTouchPoint - ? QEventPoint::State::Stationary - : QEventPoint::State::Updated); - - pressedTouchIds.insert(touchPoint.id, touchPoint.normalPosition); - break; - default: - break; - } - - touchPointList.append(touchPoint); - } - - QFlags<Qt::KeyboardModifier> keyModifier = eventTranslator->translateTouchEventModifier(touchEvent); - - bool accepted = false; - - if (eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) - accepted = QWindowSystemInterface::handleTouchCancelEvent(window2, QWasmIntegration::getTimestamp(), touchDevice, keyModifier); - else - accepted = QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( - window2, QWasmIntegration::getTimestamp(), touchDevice, touchPointList, keyModifier); - - return static_cast<int>(accepted); -} - -void QWasmCompositor::leaveWindow(QWindow *window) -{ - windowUnderMouse = nullptr; - QWindowSystemInterface::handleLeaveEvent<QWindowSystemInterface::SynchronousDelivery>(window); -} - -void QWasmCompositor::enterWindow(QWindow *window, const QPoint &localPoint, const QPoint &globalPoint) -{ - QWindowSystemInterface::handleEnterEvent<QWindowSystemInterface::SynchronousDelivery>(window, localPoint, globalPoint); -} - -bool QWasmCompositor::processMouseEnter(const EmscriptenMouseEvent *mouseEvent) -{ - Q_UNUSED(mouseEvent) - // mouse has entered the canvas area - mouseInCanvas = true; - return true; -} - -bool QWasmCompositor::processMouseLeave() -{ - mouseInCanvas = false; - return true; -} diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h index 29a0f02267..4953d65233 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.h +++ b/src/plugins/platforms/wasm/qwasmcompositor.h @@ -4,203 +4,56 @@ #ifndef QWASMCOMPOSITOR_H #define QWASMCOMPOSITOR_H -#include <QtGui/qregion.h> -#include <qpa/qplatformwindow.h> - -#include <QtOpenGL/qopengltextureblitter.h> -#include <QtGui/qpalette.h> -#include <QtGui/qpainter.h> -#include <QtGui/qinputdevice.h> +#include "qwasmwindowstack.h" -#include <QPointer> -#include <QPointingDevice> +#include <qpa/qplatformwindow.h> -#include <emscripten/html5.h> -#include <emscripten/emscripten.h> -#include <emscripten/bind.h> +#include <QMap> QT_BEGIN_NAMESPACE class QWasmWindow; class QWasmScreen; -class QOpenGLContext; -class QOpenGLTexture; -class QWasmEventTranslator; -class QWasmCompositedWindow -{ -public: - QWasmCompositedWindow(); - - QWasmWindow *window; - QWasmWindow *parentWindow; - QRegion damage; - bool flushPending; - bool visible; - QList<QWasmWindow *> childWindows; -}; +enum class QWasmWindowTreeNodeChangeType; -class QWasmCompositor : public QObject +class QWasmCompositor final : public QObject { Q_OBJECT public: QWasmCompositor(QWasmScreen *screen); - ~QWasmCompositor(); - void initEventHandlers(); - void deregisterEventHandlers(); - void destroy(); - - enum QWasmSubControl { - SC_None = 0x00000000, - SC_TitleBarSysMenu = 0x00000001, - SC_TitleBarMinButton = 0x00000002, - SC_TitleBarMaxButton = 0x00000004, - SC_TitleBarCloseButton = 0x00000008, - SC_TitleBarNormalButton = 0x00000010, - SC_TitleBarLabel = 0x00000100 - }; - Q_DECLARE_FLAGS(SubControls, QWasmSubControl) - - enum QWasmStateFlag { - State_None = 0x00000000, - State_Enabled = 0x00000001, - State_Raised = 0x00000002, - State_Sunken = 0x00000004 - }; - Q_DECLARE_FLAGS(StateFlags, QWasmStateFlag) - - enum ResizeMode { - ResizeNone, - ResizeTopLeft, - ResizeTop, - ResizeTopRight, - ResizeRight, - ResizeBottomRight, - ResizeBottom, - ResizeBottomLeft, - ResizeLeft - }; - - struct QWasmTitleBarOptions { - QRect rect; - Qt::WindowFlags flags; - int state; - QPalette palette; - QString titleBarOptionsString; - QWasmCompositor::SubControls subControls; - QIcon windowIcon; - }; - - struct QWasmFrameOptions { - QRect rect; - int lineWidth; - QPalette palette; - }; - - void setEnabled(bool enabled); - - void addWindow(QWasmWindow *window, QWasmWindow *parentWindow = nullptr); - void removeWindow(QWasmWindow *window); + ~QWasmCompositor() final; void setVisible(QWasmWindow *window, bool visible); - void raise(QWasmWindow *window); - void lower(QWasmWindow *window); - void setParent(QWasmWindow *window, QWasmWindow *parent); - - int windowCount() const; - - QWindow *windowAt(QPoint globalPoint, int padding = 0) const; - QWindow *keyWindow() const; - static QWasmTitleBarOptions makeTitleBarOptions(const QWasmWindow *window); - static QRect titlebarRect(QWasmTitleBarOptions tb, QWasmCompositor::SubControls subcontrol); + void onScreenDeleting(); QWasmScreen *screen(); - QOpenGLContext *context(); + void setEnabled(bool enabled); + + static bool releaseRequestUpdateHold(); + void requestUpdate(); enum UpdateRequestDeliveryType { ExposeEventDelivery, UpdateRequestDelivery }; - void requestUpdateAllWindows(); void requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType = ExposeEventDelivery); - void requestUpdate(); - void deliverUpdateRequests(); - void deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType); - void handleBackingStoreFlush(); - bool processMouse(int eventType, const EmscriptenMouseEvent *mouseEvent); - bool processKeyboard(int eventType, const EmscriptenKeyboardEvent *keyEvent); - bool processWheel(int eventType, const EmscriptenWheelEvent *wheelEvent); - int handleTouch(int eventType, const EmscriptenTouchEvent *touchEvent); - void resizeWindow(QWindow *window, QWasmCompositor::ResizeMode mode, QRect startRect, QPoint amount); - bool processMouseEnter(const EmscriptenMouseEvent *mouseEvent); - bool processMouseLeave(); - void enterWindow(QWindow* window, const QPoint &localPoint, const QPoint &globalPoint); - void leaveWindow(QWindow* window); - -private slots: - void frame(); + void handleBackingStoreFlush(QWindow *window); + void onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindow *window); private: - void notifyTopWindowChanged(QWasmWindow *window); - void drawWindow(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window); - void drawWindowContent(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window); - void blit(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, const QOpenGLTexture *texture, QRect targetGeometry); - - void drawWindowDecorations(QOpenGLTextureBlitter *blitter, QWasmScreen *screen, QWasmWindow *window); - void drwPanelButton(); - - QScopedPointer<QOpenGLContext> m_context; - QScopedPointer<QOpenGLTextureBlitter> m_blitter; - - QHash<QWasmWindow *, QWasmCompositedWindow> m_compositedWindows; - QList<QWasmWindow *> m_windowStack; - QRegion m_globalDamage; // damage caused by expose, window close, etc. - bool m_needComposit; - bool m_inFlush; - bool m_inResize; - bool m_isEnabled; - QSize m_targetSize; - qreal m_targetDevicePixelRatio; - QMap<QWasmWindow *, UpdateRequestDeliveryType> m_requestUpdateWindows; - bool m_requestUpdateAllWindows = false; - int m_requestAnimationFrameId = -1; - bool m_inDeliverUpdateRequest = false; - - QPointer<QWindow> draggedWindow; - QPointer<QWindow> pressedWindow; - QPointer<QWindow> lastWindow; - Qt::MouseButtons pressedButtons; - - QWasmCompositor::ResizeMode resizeMode; - QPoint resizePoint; - QRect resizeStartRect; - QPointingDevice *touchDevice; - - QMap <int, QPointF> pressedTouchIds; - - QCursor overriddenCursor; - bool isCursorOverridden = false; - - static QPalette makeWindowPalette(); + void frame(const QList<QWasmWindow *> &windows); - void drawFrameWindow(QWasmFrameOptions options, QPainter *painter); - void drawTitlebarWindow(QWasmTitleBarOptions options, QPainter *painter); - void drawShadePanel(QWasmTitleBarOptions options, QPainter *painter); - void drawItemPixmap(QPainter *painter, const QRect &rect, - int alignment, const QPixmap &pixmap) const; - - static int keyboard_cb(int eventType, const EmscriptenKeyboardEvent *keyEvent, void *userData); - static int mouse_cb(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); - static int focus_cb(int eventType, const EmscriptenFocusEvent *focusEvent, void *userData); - static int wheel_cb(int eventType, const EmscriptenWheelEvent *wheelEvent, void *userData); - - static int touchCallback(int eventType, const EmscriptenTouchEvent *ev, void *userData); + void deregisterEventHandlers(); - QWasmEventTranslator *eventTranslator; + void deliverUpdateRequests(); + void deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType); - bool mouseInCanvas; - QPointer<QWindow> windowUnderMouse; + bool m_isEnabled = true; + QMap<QWasmWindow *, UpdateRequestDeliveryType> m_requestUpdateWindows; + int m_requestAnimationFrameId = -1; + bool m_inDeliverUpdateRequest = false; + static bool m_requestUpdateHoldEnabled; }; -Q_DECLARE_OPERATORS_FOR_FLAGS(QWasmCompositor::SubControls) QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.cpp b/src/plugins/platforms/wasm/qwasmcssstyle.cpp new file mode 100644 index 0000000000..e0e1a99f48 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmcssstyle.cpp @@ -0,0 +1,248 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmcssstyle.h" + +#include "qwasmbase64iconstore.h" + +#include <QtCore/qstring.h> +#include <QtCore/qfile.h> + +QT_BEGIN_NAMESPACE + +namespace { +const char *Style = R"css( +.qt-screen { + --border-width: 4px; + --resize-outline-width: 8px; + --resize-outline-half-width: var(--resize-outline-width) / 2; + + position: relative; + border: none; + caret-color: transparent; + cursor: default; + width: 100%; + height: 100%; + overflow: hidden; +} + +.qt-screen div { + touch-action: none; +} + +.qt-window { + position: absolute; + background-color: lightgray; +} + +.qt-window-contents { + overflow: hidden; + position: relative; +} + +.qt-window.transparent-for-input { + pointer-events: none; +} + +.qt-window.has-shadow { + box-shadow: rgb(0 0 0 / 20%) 0px 10px 16px 0px, rgb(0 0 0 / 19%) 0px 6px 20px 0px; +} + +.qt-window.has-border { + border: var(--border-width) solid lightgray; + caret-color: transparent; +} + +.qt-window.frameless { + background-color: transparent; +} + +.resize-outline { + position: absolute; + display: none; +} + +.qt-window.no-resize > .resize-outline { display: none; } + +.qt-window.has-border:not(.maximized):not(.no-resize) .resize-outline { + display: block; +} + +.resize-outline.nw { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nwse-resize; +} + +.resize-outline.n { + left: var(--resize-outline-half-width); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + height: var(--resize-outline-width); + width: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + cursor: ns-resize; +} + +.resize-outline.ne { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nesw-resize; +} + +.resize-outline.w { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: 0; + height: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + width: var(--resize-outline-width); + cursor: ew-resize; +} + +.resize-outline.e { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: 0; + height: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + width: var(--resize-outline-width); + cursor: ew-resize; +} + +.resize-outline.sw { + left: calc(-1 * var(--resize-outline-half-width) - var(--border-width)); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nesw-resize; +} + +.resize-outline.s { + left: var(--resize-outline-half-width); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + height: var(--resize-outline-width); + width: calc(100% + 2 * var(--border-width) - var(--resize-outline-width)); + cursor: ns-resize; +} + +.resize-outline.se { + left: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + top: calc(100% + var(--border-width) - var(--resize-outline-half-width)); + width: var(--resize-outline-width); + height: var(--resize-outline-width); + cursor: nwse-resize; +} + +.title-bar { + display: none; + align-items: center; + overflow: hidden; + height: 18px; + padding-bottom: 4px; +} + +.qt-window.has-border > .title-bar { + display: flex; +} + +.title-bar .window-name { + display: none; + font-family: 'Lucida Grande'; + white-space: nowrap; + user-select: none; + overflow: hidden; +} + + +.qt-window.has-title .title-bar .window-name { + display: block; +} + +.title-bar .spacer { + flex-grow: 1 +} + +.qt-window.inactive .title-bar { + opacity: 0.35; +} + +.qt-window-canvas-container { + display: flex; + pointer-events: none; +} + +.title-bar div { + pointer-events: none; +} + +.qt-window-a11y-container { + position: absolute; + z-index: -1; +} + +.title-bar .image-button { + width: 18px; + height: 18px; + display: flex; + justify-content: center; + user-select: none; + align-items: center; +} + +.title-bar .image-button img { + width: 10px; + height: 10px; + user-select: none; + pointer-events: none; + -webkit-user-drag: none; + background-size: 10px 10px; +} + +.title-bar .action-button { + pointer-events: all; +} + +.qt-window.blocked div { + pointer-events: none; +} + +.title-bar .action-button img { + transition: filter 0.08s ease-out; +} + +.title-bar .action-button:hover img { + filter: invert(0.45); +} + +.title-bar .action-button:active img { + filter: invert(0.6); +} + +/* This will clip the content within 50% frame in 1x1 pixel area, preventing it + from being rendered on the page, but it should still be read by modern + screen readers */ +.hidden-visually-read-by-screen-reader { + visibility: visible; + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + +)css"; + +} // namespace + +emscripten::val QWasmCSSStyle::createStyleElement(emscripten::val parent) +{ + auto document = parent["ownerDocument"]; + auto screenStyle = document.call<emscripten::val>("createElement", emscripten::val("style")); + + screenStyle.set("textContent", std::string(Style)); + return screenStyle; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.h b/src/plugins/platforms/wasm/qwasmcssstyle.h new file mode 100644 index 0000000000..fc4cc2d54c --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmcssstyle.h @@ -0,0 +1,18 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMCSSSTYLE_H +#define QWASMCSSSTYLE_H + +#include <QtCore/qglobal.h> + +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + +namespace QWasmCSSStyle { +emscripten::val createStyleElement(emscripten::val parent); +} + +QT_END_NAMESPACE +#endif // QWASMINLINESTYLEREGISTRY_H diff --git a/src/plugins/platforms/wasm/qwasmcursor.cpp b/src/plugins/platforms/wasm/qwasmcursor.cpp index f1f5e77bcb..c258befa77 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.cpp +++ b/src/plugins/platforms/wasm/qwasmcursor.cpp @@ -3,9 +3,11 @@ #include "qwasmcursor.h" #include "qwasmscreen.h" -#include "qwasmstring.h" +#include "qwasmwindow.h" +#include <QtCore/qbuffer.h> #include <QtCore/qdebug.h> +#include <QtCore/qstring.h> #include <QtGui/qwindow.h> #include <emscripten/emscripten.h> @@ -14,125 +16,86 @@ QT_BEGIN_NAMESPACE using namespace emscripten; -void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) -{ - if (!window) - return; - QScreen *screen = window->screen(); - if (!screen) - return; - - if (windowCursor) { - - // Bitmap and custom cursors are not implemented (will fall back to "auto") - if (windowCursor->shape() == Qt::BitmapCursor || windowCursor->shape() >= Qt::CustomCursor) - qWarning() << "QWasmCursor: bitmap and custom cursors are not supported"; - - - htmlCursorName = cursorShapeToHtml(windowCursor->shape()); - } - if (htmlCursorName.isEmpty()) - htmlCursorName = "default"; - - setWasmCursor(screen, htmlCursorName); -} - -QByteArray QWasmCursor::cursorShapeToHtml(Qt::CursorShape shape) +namespace { +QByteArray cursorToCss(const QCursor *cursor) { - QByteArray cursorName; - + auto shape = cursor->shape(); switch (shape) { case Qt::ArrowCursor: - cursorName = "default"; - break; + return "default"; case Qt::UpArrowCursor: - cursorName = "n-resize"; - break; + return "n-resize"; case Qt::CrossCursor: - cursorName = "crosshair"; - break; + return "crosshair"; case Qt::WaitCursor: - cursorName = "wait"; - break; + return "wait"; case Qt::IBeamCursor: - cursorName = "text"; - break; + return "text"; case Qt::SizeVerCursor: - cursorName = "ns-resize"; - break; + return "ns-resize"; case Qt::SizeHorCursor: - cursorName = "ew-resize"; - break; + return "ew-resize"; case Qt::SizeBDiagCursor: - cursorName = "nesw-resize"; - break; + return "nesw-resize"; case Qt::SizeFDiagCursor: - cursorName = "nwse-resize"; - break; + return "nwse-resize"; case Qt::SizeAllCursor: - cursorName = "move"; - break; + return "move"; case Qt::BlankCursor: - cursorName = "none"; - break; + return "none"; case Qt::SplitVCursor: - cursorName = "row-resize"; - break; + return "row-resize"; case Qt::SplitHCursor: - cursorName = "col-resize"; - break; + return "col-resize"; case Qt::PointingHandCursor: - cursorName = "pointer"; - break; + return "pointer"; case Qt::ForbiddenCursor: - cursorName = "not-allowed"; - break; + return "not-allowed"; case Qt::WhatsThisCursor: - cursorName = "help"; - break; + return "help"; case Qt::BusyCursor: - cursorName = "progress"; - break; + return "progress"; case Qt::OpenHandCursor: - cursorName = "grab"; - break; + return "grab"; case Qt::ClosedHandCursor: - cursorName = "grabbing"; - break; + return "grabbing"; case Qt::DragCopyCursor: - cursorName = "copy"; - break; + return "copy"; case Qt::DragMoveCursor: - cursorName = "default"; - break; + return "default"; case Qt::DragLinkCursor: - cursorName = "alias"; - break; + return "alias"; + case Qt::BitmapCursor: { + auto pixmap = cursor->pixmap(); + QByteArray cursorAsPng; + QBuffer buffer(&cursorAsPng); + buffer.open(QBuffer::WriteOnly); + pixmap.save(&buffer, "PNG"); + buffer.close(); + auto cursorAsBase64 = cursorAsPng.toBase64(); + auto hotSpot = cursor->hotSpot(); + auto encodedCursor = + QString("url(data:image/png;base64,%1) %2 %3, auto") + .arg(QString::fromUtf8(cursorAsBase64), + QString::number(hotSpot.x()), + QString::number(hotSpot.y())); + return encodedCursor.toUtf8(); + } default: - break; + static_assert(Qt::CustomCursor == 25, + "New cursor type added, handle it"); + qWarning() << "QWasmCursor: " << shape << " unsupported"; + return "default"; } - - return cursorName; -} - -void QWasmCursor::setWasmCursor(QScreen *screen, const QByteArray &name) -{ - // Set cursor on the canvas - val canvas = QWasmScreen::get(screen)->canvas(); - val canvasStyle = canvas["style"]; - canvasStyle.set("cursor", val(name.constData())); } +} // namespace -void QWasmCursor::setOverrideWasmCursor(QCursor *windowCursor, QScreen *screen) -{ - QWasmCursor *wCursor = static_cast<QWasmCursor *>(QWasmScreen::get(screen)->cursor()); - wCursor->setWasmCursor(screen, wCursor->cursorShapeToHtml(windowCursor->shape())); -} - -void QWasmCursor::clearOverrideWasmCursor(QScreen *screen) +void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) { - QWasmCursor *wCursor = static_cast<QWasmCursor *>(QWasmScreen::get(screen)->cursor()); - wCursor->setWasmCursor(screen, wCursor->htmlCursorName); + if (!window) + return; + if (QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle())) + wasmWindow->setWindowCursor(windowCursor ? cursorToCss(windowCursor) : "default"); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcursor.h b/src/plugins/platforms/wasm/qwasmcursor.h index 17ddd344b1..6873602caf 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.h +++ b/src/plugins/platforms/wasm/qwasmcursor.h @@ -5,19 +5,13 @@ #define QWASMCURSOR_H #include <qpa/qplatformcursor.h> + QT_BEGIN_NAMESPACE class QWasmCursor : public QPlatformCursor { public: void changeCursor(QCursor *windowCursor, QWindow *window) override; - - QByteArray cursorShapeToHtml(Qt::CursorShape shape); - static void setOverrideWasmCursor(QCursor *windowCursor, QScreen *screen); - static void clearOverrideWasmCursor(QScreen *screen); -private: - QByteArray htmlCursorName = "default"; - void setWasmCursor(QScreen *screen, const QByteArray &name); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdom.cpp b/src/plugins/platforms/wasm/qwasmdom.cpp new file mode 100644 index 0000000000..6b2b3d0933 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.cpp @@ -0,0 +1,303 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmdom.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/qpoint.h> +#include <QtCore/qrect.h> +#include <QtGui/qimage.h> +#include <private/qstdweb_p.h> +#include <QtCore/qurl.h> + +#include <utility> +#include <emscripten/wire.h> + +QT_BEGIN_NAMESPACE + +namespace dom { +namespace { +std::string dropActionToDropEffect(Qt::DropAction action) +{ + switch (action) { + case Qt::DropAction::CopyAction: + return "copy"; + case Qt::DropAction::IgnoreAction: + return "none"; + case Qt::DropAction::LinkAction: + return "link"; + case Qt::DropAction::MoveAction: + case Qt::DropAction::TargetMoveAction: + return "move"; + case Qt::DropAction::ActionMask: + Q_ASSERT(false); + return ""; + } +} +} // namespace + +DataTransfer::DataTransfer(emscripten::val webDataTransfer) + : webDataTransfer(webDataTransfer) { +} + +DataTransfer::~DataTransfer() = default; + +DataTransfer::DataTransfer(const DataTransfer &other) = default; + +DataTransfer::DataTransfer(DataTransfer &&other) = default; + +DataTransfer &DataTransfer::operator=(const DataTransfer &other) = default; + +DataTransfer &DataTransfer::operator=(DataTransfer &&other) = default; + +void DataTransfer::setDragImage(emscripten::val element, const QPoint &hotspot) +{ + webDataTransfer.call<void>("setDragImage", element, emscripten::val(hotspot.x()), + emscripten::val(hotspot.y())); +} + +void DataTransfer::setData(std::string format, std::string data) +{ + webDataTransfer.call<void>("setData", emscripten::val(std::move(format)), + emscripten::val(std::move(data))); +} + +void DataTransfer::setDropAction(Qt::DropAction action) +{ + webDataTransfer.set("dropEffect", emscripten::val(dropActionToDropEffect(action))); +} + +void DataTransfer::setDataFromMimeData(const QMimeData &mimeData) +{ + for (const auto &format : mimeData.formats()) { + auto data = mimeData.data(format); + + auto encoded = format.startsWith("text/") + ? QString::fromLocal8Bit(data).toStdString() + : "QB64" + QString::fromLocal8Bit(data.toBase64()).toStdString(); + + setData(format.toStdString(), std::move(encoded)); + } +} + +// Converts a DataTransfer instance to a QMimeData instance. Invokes the +// given callback when the conversion is complete. The callback takes ownership +// of the QMimeData. +void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback) +{ + enum class ItemKind { + File, + String, + }; + + class MimeContext { + + public: + MimeContext(int itemCount, std::function<void(QMimeData *)> callback) + :m_remainingItemCount(itemCount), m_callback(callback) + { + + } + + void deref() { + if (--m_remainingItemCount > 0) + return; + + mimeData->setUrls(fileUrls); + + m_callback(mimeData); + + // Delete files; we expect that the user callback reads/copies + // file content before returning. + // Fixme: tie file lifetime to lifetime of the QMimeData? + for (QUrl fileUrl: fileUrls) + QFile(fileUrl.toLocalFile()).remove(); + + delete this; + } + + QMimeData *mimeData = new QMimeData(); + QList<QUrl> fileUrls; + + private: + int m_remainingItemCount; + std::function<void(QMimeData *)> m_callback; + }; + + const auto items = webDataTransfer["items"]; + const int itemCount = items["length"].as<int>(); + const int fileCount = webDataTransfer["files"]["length"].as<int>(); + MimeContext *mimeContext = new MimeContext(itemCount, callback); + + for (int i = 0; i < itemCount; ++i) { + const auto item = items[i]; + const auto itemKind = + item["kind"].as<std::string>() == "string" ? ItemKind::String : ItemKind::File; + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + + switch (itemKind) { + case ItemKind::File: { + qstdweb::File webfile(item.call<emscripten::val>("getAsFile")); + + if (webfile.size() > 1e+9) { // limit file size to 1 GB + qWarning() << "File is too large (> 1GB) and will be skipped. File size is" << webfile.size(); + mimeContext->deref(); + continue; + } + + QString mimeFormat = QString::fromStdString(webfile.type()); + QString fileName = QString::fromStdString(webfile.name()); + + // there's a file, now read it + QByteArray fileContent(webfile.size(), Qt::Uninitialized); + webfile.stream(fileContent.data(), [=]() { + + // If we get a single file, and that file is an image, then + // try to decode the image data. This handles the case where + // image data (i.e. not an image file) is pasted. The browsers + // will then create a fake "image.png" file which has the image + // data. As a side effect Qt will also decode the image for + // single-image-file drops, since there is no way to differentiate + // the fake "image.png" from a real one. + if (fileCount == 1 && mimeFormat.contains("image/")) { + QImage image; + if (image.loadFromData(fileContent)) + mimeContext->mimeData->setImageData(image); + } + + QDir qtTmpDir("/qt/tmp/"); // "tmp": indicate that these files won't stay around + qtTmpDir.mkpath(qtTmpDir.path()); + + QUrl fileUrl = QUrl::fromLocalFile(qtTmpDir.filePath(QString::fromStdString(webfile.name()))); + mimeContext->fileUrls.append(fileUrl); + + QFile file(fileUrl.toLocalFile()); + if (!file.open(QFile::WriteOnly)) { + qWarning() << "File was not opened"; + mimeContext->deref(); + return; + } + if (file.write(fileContent) < 0) { + qWarning() << "Write failed"; + file.close(); + } + mimeContext->deref(); + }); + break; + } + case ItemKind::String: + if (itemMimeType.contains("STRING", Qt::CaseSensitive) + || itemMimeType.contains("TEXT", Qt::CaseSensitive)) { + mimeContext->deref(); + break; + } + QString a; + QString data = QString::fromEcmaString(webDataTransfer.call<emscripten::val>( + "getData", emscripten::val(itemMimeType.toStdString()))); + + if (!data.isEmpty()) { + if (itemMimeType == "text/html") + mimeContext->mimeData->setHtml(data); + else if (itemMimeType.isEmpty() || itemMimeType == "text/plain") + mimeContext->mimeData->setText(data); // the type can be empty + else { + // TODO improve encoding + if (data.startsWith("QB64")) { + data.remove(0, 4); + mimeContext->mimeData->setData(itemMimeType, + QByteArray::fromBase64(QByteArray::fromStdString( + data.toStdString()))); + } else { + mimeContext->mimeData->setData(itemMimeType, data.toLocal8Bit()); + } + } + } + mimeContext->deref(); + break; + } + } // for items +} + +QMimeData *DataTransfer::toMimeDataPreview() +{ + auto data = new QMimeData(); + + QList<QUrl> uriList; + for (int i = 0; i < webDataTransfer["items"]["length"].as<int>(); ++i) { + const auto item = webDataTransfer["items"][i]; + if (item["kind"].as<std::string>() == "file") { + uriList.append(QUrl("blob://placeholder")); + } else { + const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>()); + data->setData(itemMimeType, QByteArray()); + } + } + data->setUrls(uriList); + return data; +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag) +{ + if (flag) { + element["classList"].call<void>("add", emscripten::val(std::move(cssClassName))); + return; + } + + element["classList"].call<void>("remove", emscripten::val(std::move(cssClassName))); +} + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point) +{ + const auto sourceBoundingRect = + QRectF::fromDOMRect(source.call<emscripten::val>("getBoundingClientRect")); + const auto targetBoundingRect = + QRectF::fromDOMRect(target.call<emscripten::val>("getBoundingClientRect")); + + const auto offset = sourceBoundingRect.topLeft() - targetBoundingRect.topLeft(); + return point + offset; +} + +void drawImageToWebImageDataArray(const QImage &sourceImage, emscripten::val destinationImageData, + const QRect &sourceRect) +{ + Q_ASSERT_X(destinationImageData["constructor"]["name"].as<std::string>() == "ImageData", + Q_FUNC_INFO, "The destination should be an ImageData instance"); + + constexpr int BytesPerColor = 4; + if (sourceRect.width() == sourceImage.width()) { + // Copy a contiguous chunk of memory + // ............... + // OOOOOOOOOOOOOOO + // OOOOOOOOOOOOOOO -> image data + // OOOOOOOOOOOOOOO + // ............... + auto imageMemory = emscripten::typed_memory_view(sourceRect.width() * sourceRect.height() + * BytesPerColor, + sourceImage.constScanLine(sourceRect.y())); + destinationImageData["data"].call<void>( + "set", imageMemory, sourceRect.y() * sourceImage.width() * BytesPerColor); + } else { + // Go through the scanlines manually to set the individual lines in bulk. This is + // marginally less performant than the above. + // ............... + // ...OOOOOOOOO... r = 0 -> image data + // ...OOOOOOOOO... r = 1 -> image data + // ...OOOOOOOOO... r = 2 -> image data + // ............... + for (int row = 0; row < sourceRect.height(); ++row) { + auto scanlineMemory = + emscripten::typed_memory_view(sourceRect.width() * BytesPerColor, + sourceImage.constScanLine(row + sourceRect.y()) + + BytesPerColor * sourceRect.x()); + destinationImageData["data"].call<void>("set", scanlineMemory, + (sourceRect.y() + row) * sourceImage.width() + * BytesPerColor + + sourceRect.x() * BytesPerColor); + } + } +} + +} // namespace dom + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdom.h b/src/plugins/platforms/wasm/qwasmdom.h new file mode 100644 index 0000000000..0a520815a3 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdom.h @@ -0,0 +1,63 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMDOM_H +#define QWASMDOM_H + +#include <QtCore/qtconfigmacros.h> +#include <QtCore/QPointF> +#include <private/qstdweb_p.h> +#include <QtCore/qnamespace.h> + +#include <emscripten/val.h> + +#include <functional> +#include <memory> +#include <string> + +#include <QMimeData> +QT_BEGIN_NAMESPACE + +namespace qstdweb { + struct CancellationFlag; +} + + +class QPoint; +class QRect; + +namespace dom { +struct DataTransfer +{ + explicit DataTransfer(emscripten::val webDataTransfer); + ~DataTransfer(); + DataTransfer(const DataTransfer &other); + DataTransfer(DataTransfer &&other); + DataTransfer &operator=(const DataTransfer &other); + DataTransfer &operator=(DataTransfer &&other); + + void toMimeDataWithFile(std::function<void(QMimeData *)> callback); + QMimeData *toMimeDataPreview(); + void setDragImage(emscripten::val element, const QPoint &hotspot); + void setData(std::string format, std::string data); + void setDropAction(Qt::DropAction dropAction); + void setDataFromMimeData(const QMimeData &mimeData); + + emscripten::val webDataTransfer; +}; + +inline emscripten::val document() +{ + return emscripten::val::global("document"); +} + +void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag); + +QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point); + +void drawImageToWebImageDataArray(const QImage &source, emscripten::val destinationImageData, + const QRect &sourceRect); +} // namespace dom + +QT_END_NAMESPACE +#endif // QWASMDOM_H diff --git a/src/plugins/platforms/wasm/qwasmdrag.cpp b/src/plugins/platforms/wasm/qwasmdrag.cpp index c87e4ce7c2..d07a46618f 100644 --- a/src/plugins/platforms/wasm/qwasmdrag.cpp +++ b/src/plugins/platforms/wasm/qwasmdrag.cpp @@ -1,210 +1,291 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmdrag.h" -#include <iostream> -#include <QMimeDatabase> +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" -#include <emscripten.h> -#include <emscripten/html5.h> -#include <emscripten/val.h> -#include <emscripten/bind.h> -#include <private/qstdweb_p.h> #include <qpa/qwindowsysteminterface.h> -#include <private/qsimpledrag_p.h> -#include "qwasmcompositor.h" -#include "qwasmeventtranslator.h" -#include <QtCore/QEventLoop> -#include <QMimeData> -#include <private/qshapedpixmapdndwindow_p.h> + +#include <QtCore/private/qstdweb_p.h> +#include <QtCore/qeventloop.h> +#include <QtCore/qmimedata.h> +#include <QtCore/qtimer.h> +#include <QFile> + +#include <functional> +#include <string> +#include <utility> QT_BEGIN_NAMESPACE -using namespace emscripten; +namespace { + +QWindow *windowForDrag(QDrag *drag) +{ + QWindow *window = qobject_cast<QWindow *>(drag->source()); + if (window) + return window; + if (drag->source()->metaObject()->indexOfMethod("_q_closestWindowHandle()") == -1) + return nullptr; + + QMetaObject::invokeMethod(drag->source(), "_q_closestWindowHandle", + Q_RETURN_ARG(QWindow *, window)); + return window; +} + +} // namespace -static void getTextPlainCallback(val m_string) +struct QWasmDrag::DragState +{ + class DragImage + { + public: + DragImage(const QPixmap &pixmap, const QMimeData *mimeData, QWindow *window); + ~DragImage(); + + emscripten::val htmlElement(); + + private: + emscripten::val generateDragImage(const QPixmap &pixmap, const QMimeData *mimeData); + emscripten::val generateDragImageFromText(const QMimeData *mimeData); + emscripten::val generateDefaultDragImage(); + emscripten::val generateDragImageFromPixmap(const QPixmap &pixmap); + + emscripten::val m_imageDomElement; + emscripten::val m_temporaryImageElementParent; + }; + + DragState(QDrag *drag, QWindow *window, std::function<void()> quitEventLoopClosure); + ~DragState(); + DragState(const QWasmDrag &other) = delete; + DragState(QWasmDrag &&other) = delete; + DragState &operator=(const QWasmDrag &other) = delete; + DragState &operator=(QWasmDrag &&other) = delete; + + QDrag *drag; + QWindow *window; + std::function<void()> quitEventLoopClosure; + std::unique_ptr<DragImage> dragImage; + Qt::DropAction dropAction = Qt::DropAction::IgnoreAction; +}; + +QWasmDrag::QWasmDrag() = default; + +QWasmDrag::~QWasmDrag() = default; + +QWasmDrag *QWasmDrag::instance() { - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - thisDrag->m_mimeData->setText(QString::fromStdString(m_string.as<std::string>())); - thisDrag->qWasmDrop(); + return static_cast<QWasmDrag *>(QWasmIntegration::get()->drag()); } -static void getTextUrlCallback(val m_string) +Qt::DropAction QWasmDrag::drag(QDrag *drag) { - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - thisDrag->m_mimeData->setData(QStringLiteral("text/uri-list"), - QByteArray::fromStdString(m_string.as<std::string>())); + Q_ASSERT_X(!m_dragState, Q_FUNC_INFO, "Drag already in progress"); + + QWindow *window = windowForDrag(drag); + if (!window) + return Qt::IgnoreAction; + + Qt::DropAction dragResult = Qt::IgnoreAction; + if (qstdweb::haveJspi()) { + QEventLoop loop; + m_dragState = std::make_unique<DragState>(drag, window, [&loop]() { loop.quit(); }); + loop.exec(); + dragResult = m_dragState->dropAction; + m_dragState.reset(); + } - thisDrag->qWasmDrop(); + if (dragResult == Qt::IgnoreAction) + dragResult = QBasicDrag::drag(drag); + + return dragResult; } -static void getTextHtmlCallback(val m_string) +void QWasmDrag::onNativeDragStarted(DragEvent *event) { - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - thisDrag->m_mimeData->setHtml(QString::fromStdString(m_string.as<std::string>())); + Q_ASSERT_X(event->type == EventType::DragStart, Q_FUNC_INFO, + "The event is not a DragStart event"); + // It is possible for a drag start event to arrive from another window. + if (!m_dragState || m_dragState->window != event->targetWindow) { + event->cancelDragStart(); + return; + } - thisDrag->qWasmDrop(); + m_dragState->dragImage = std::make_unique<DragState::DragImage>( + m_dragState->drag->pixmap(), m_dragState->drag->mimeData(), event->targetWindow); + event->dataTransfer.setDragImage(m_dragState->dragImage->htmlElement(), + m_dragState->drag->hotSpot()); + event->dataTransfer.setDataFromMimeData(*m_dragState->drag->mimeData()); } -static void dropEvent(val event) -{ - // someone dropped a file into the browser window - // event is dataTransfer object - // if drop event from outside browser, we do not get any mouse release, maybe mouse move - // after the drop event - - // data-context thing was not working here :( - QWasmDrag *wasmDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - - wasmDrag->m_wasmScreen = - reinterpret_cast<QWasmScreen*>(event["target"]["data-qtdropcontext"].as<quintptr>()); - - wasmDrag->m_mouseDropPoint = QPoint(event["x"].as<int>(), event["y"].as<int>()); - if (wasmDrag->m_mimeData) - delete wasmDrag->m_mimeData; - wasmDrag->m_mimeData = new QMimeData; - int button = event["button"].as<int>(); - wasmDrag->m_qButton = QWasmEventTranslator::translateMouseButton(button); - - wasmDrag->m_keyModifiers = Qt::NoModifier; - if (event["altKey"].as<bool>()) - wasmDrag->m_keyModifiers |= Qt::AltModifier; - if (event["ctrlKey"].as<bool>()) - wasmDrag->m_keyModifiers |= Qt::ControlModifier; - if (event["metaKey"].as<bool>()) - wasmDrag->m_keyModifiers |= Qt::MetaModifier; - - event.call<void>("preventDefault"); // prevent browser from handling drop event - - std::string dEffect = event["dataTransfer"]["dropEffect"].as<std::string>(); - - wasmDrag->m_dropActions = Qt::IgnoreAction; - if (dEffect == "copy") - wasmDrag->m_dropActions = Qt::CopyAction; - if (dEffect == "move") - wasmDrag->m_dropActions = Qt::MoveAction; - if (dEffect == "link") - wasmDrag->m_dropActions = Qt::LinkAction; - - val dt = event["dataTransfer"]["items"]["length"]; - - val typesCount = event["dataTransfer"]["types"]["length"]; - - // handle mimedata - int count = dt.as<int>(); - wasmDrag->m_mimeTypesCount = count; - // kind is file type: file or string - for (int i=0; i < count; i++) { - val item = event["dataTransfer"]["items"][i]; - val kind = item["kind"]; - val fileType = item["type"]; - - if (kind.as<std::string>() == "file") { - val m_file = item.call<val>("getAsFile"); - if (m_file.isUndefined()) { - continue; - } - - qstdweb::File file(m_file); - - QString mimeFormat = QString::fromStdString(file.type()); - QByteArray fileContent; - fileContent.resize(file.size()); - - file.stream(fileContent.data(), [=]() { - if (!fileContent.isEmpty()) { - - if (mimeFormat.contains("image")) { - QImage image; - image.loadFromData(fileContent, nullptr); - wasmDrag->m_mimeData->setImageData(image); - } else { - wasmDrag->m_mimeData->setData(mimeFormat, fileContent.data()); - } - wasmDrag->qWasmDrop(); - } - }); - - } else { // string - - if (fileType.as<std::string>() == "text/uri-list" - || fileType.as<std::string>() == "text/x-moz-url") { - item.call<val>("getAsString", val::module_property("qtgetTextUrl")); - } else if (fileType.as<std::string>() == "text/html") { - item.call<val>("getAsString", val::module_property("qtgetTextHtml")); - } else { // handle everything else here as plain text - item.call<val>("getAsString", val::module_property("qtgetTextPlain")); - } - } +void QWasmDrag::onNativeDragOver(DragEvent *event) +{ + auto mimeDataPreview = event->dataTransfer.toMimeDataPreview(); + + const Qt::DropActions actions = m_dragState + ? m_dragState->drag->supportedActions() + : (Qt::DropAction::CopyAction | Qt::DropAction::MoveAction + | Qt::DropAction::LinkAction); + + const auto dragResponse = QWindowSystemInterface::handleDrag( + event->targetWindow, &*mimeDataPreview, event->pointInPage.toPoint(), actions, + event->mouseButton, event->modifiers); + event->acceptDragOver(); + if (dragResponse.isAccepted()) { + event->dataTransfer.setDropAction(dragResponse.acceptedAction()); + } else { + event->dataTransfer.setDropAction(Qt::DropAction::IgnoreAction); } } -EMSCRIPTEN_BINDINGS(drop_module) { - function("qtDrop", &dropEvent); - function("qtgetTextPlain", &getTextPlainCallback); - function("qtgetTextUrl", &getTextUrlCallback); - function("qtgetTextHtml", &getTextHtmlCallback); +void QWasmDrag::onNativeDrop(DragEvent *event) +{ + QWasmWindow *wasmWindow = QWasmWindow::fromWindow(event->targetWindow); + + const auto screenElementPos = dom::mapPoint( + event->target(), wasmWindow->platformScreen()->element(), event->localPoint); + const auto screenPos = + wasmWindow->platformScreen()->mapFromLocal(screenElementPos); + const QPoint targetWindowPos = event->targetWindow->mapFromGlobal(screenPos).toPoint(); + + const Qt::DropActions actions = m_dragState + ? m_dragState->drag->supportedActions() + : (Qt::DropAction::CopyAction | Qt::DropAction::MoveAction + | Qt::DropAction::LinkAction); + Qt::MouseButton mouseButton = event->mouseButton; + QFlags<Qt::KeyboardModifier> modifiers = event->modifiers; + + // Accept the native drop event: We are going to async read any dropped + // files, but the browser expects that accepted state is set before any + // async calls. + event->acceptDrop(); + + const auto dropCallback = [&m_dragState = m_dragState, wasmWindow, targetWindowPos, + actions, mouseButton, modifiers](QMimeData *mimeData) { + + auto dropResponse = std::make_shared<QPlatformDropQtResponse>(true, Qt::DropAction::CopyAction); + *dropResponse = QWindowSystemInterface::handleDrop(wasmWindow->window(), mimeData, + targetWindowPos, actions, + mouseButton, modifiers); + + if (dropResponse->isAccepted()) + m_dragState->dropAction = dropResponse->acceptedAction(); + + delete mimeData; + }; + + event->dataTransfer.toMimeDataWithFile(dropCallback); } +void QWasmDrag::onNativeDragFinished(DragEvent *event) +{ + m_dragState->dropAction = event->dropAction; + m_dragState->quitEventLoopClosure(); +} + +QWasmDrag::DragState::DragImage::DragImage(const QPixmap &pixmap, const QMimeData *mimeData, + QWindow *window) + : m_temporaryImageElementParent(QWasmWindow::fromWindow(window)->containerElement()) +{ + m_imageDomElement = generateDragImage(pixmap, mimeData); + + m_imageDomElement.set("className", "hidden-drag-image"); + m_temporaryImageElementParent.call<void>("appendChild", m_imageDomElement); +} + +QWasmDrag::DragState::DragImage::~DragImage() +{ + m_temporaryImageElementParent.call<void>("removeChild", m_imageDomElement); +} -QWasmDrag::QWasmDrag() +emscripten::val QWasmDrag::DragState::DragImage::generateDragImage(const QPixmap &pixmap, + const QMimeData *mimeData) { - init(); + if (!pixmap.isNull()) + return generateDragImageFromPixmap(pixmap); + if (mimeData->hasFormat("text/plain")) + return generateDragImageFromText(mimeData); + return generateDefaultDragImage(); } -QWasmDrag::~QWasmDrag() +emscripten::val +QWasmDrag::DragState::DragImage::generateDragImageFromText(const QMimeData *mimeData) { - if (m_mimeData) - delete m_mimeData; + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("span")); + + constexpr qsizetype MaxCharactersInDragImage = 100; + + const auto text = QString::fromUtf8(mimeData->data("text/plain")); + dragImageElement.set( + "innerText", + text.first(qMin(qsizetype(MaxCharactersInDragImage), text.length())).toStdString()); + return dragImageElement; } -void QWasmDrag::init() +emscripten::val QWasmDrag::DragState::DragImage::generateDefaultDragImage() { + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("div")); + + auto innerImgElement = emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("img")); + innerImgElement.set("src", + "data:image/" + std::string("svg+xml") + ";base64," + + std::string(Base64IconStore::get()->getIcon( + Base64IconStore::IconType::QtLogo))); + + constexpr char DragImageSize[] = "50px"; + + dragImageElement["style"].set("width", DragImageSize); + innerImgElement["style"].set("width", DragImageSize); + dragImageElement["style"].set("display", "flex"); + + dragImageElement.call<void>("appendChild", innerImgElement); + return dragImageElement; } -void QWasmDrag::drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) +emscripten::val QWasmDrag::DragState::DragImage::generateDragImageFromPixmap(const QPixmap &pixmap) { - QSimpleDrag::drop(globalPos, b, mods); + emscripten::val dragImageElement = + emscripten::val::global("document") + .call<emscripten::val>("createElement", emscripten::val("canvas")); + dragImageElement.set("width", pixmap.width()); + dragImageElement.set("height", pixmap.height()); + + dragImageElement["style"].set( + "width", std::to_string(pixmap.width() / pixmap.devicePixelRatio()) + "px"); + dragImageElement["style"].set( + "height", std::to_string(pixmap.height() / pixmap.devicePixelRatio()) + "px"); + + auto context2d = dragImageElement.call<emscripten::val>("getContext", emscripten::val("2d")); + auto imageData = context2d.call<emscripten::val>( + "createImageData", emscripten::val(pixmap.width()), emscripten::val(pixmap.height())); + + dom::drawImageToWebImageDataArray(pixmap.toImage().convertedTo(QImage::Format::Format_RGBA8888), + imageData, QRect(0, 0, pixmap.width(), pixmap.height())); + context2d.call<void>("putImageData", imageData, emscripten::val(0), emscripten::val(0)); + + return dragImageElement; } -void QWasmDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) +emscripten::val QWasmDrag::DragState::DragImage::htmlElement() { - QSimpleDrag::move(globalPos, b, mods); + return m_imageDomElement; } -void QWasmDrag::qWasmDrop() -{ - // collect mime - QWasmDrag *thisDrag = static_cast<QWasmDrag*>(QWasmIntegration::get()->drag()); - - if (thisDrag->m_mimeTypesCount != thisDrag->m_mimeData->formats().size()) - return; // keep collecting mimetypes - - // start drag enter - QWindowSystemInterface::handleDrag(thisDrag->m_wasmScreen->topLevelAt(thisDrag->m_mouseDropPoint), - thisDrag->m_mimeData, - thisDrag->m_mouseDropPoint, - thisDrag->m_dropActions, - thisDrag->m_qButton, - thisDrag->m_keyModifiers); - - // drag drop - QWindowSystemInterface::handleDrop(thisDrag->m_wasmScreen->topLevelAt(thisDrag->m_mouseDropPoint), - thisDrag->m_mimeData, - thisDrag->m_mouseDropPoint, - thisDrag->m_dropActions, - thisDrag->m_qButton, - thisDrag->m_keyModifiers); - - // drag leave - QWindowSystemInterface::handleDrag(thisDrag->m_wasmScreen->topLevelAt(thisDrag->m_mouseDropPoint), - nullptr, - QPoint(), - Qt::IgnoreAction, { }, { }); - - thisDrag->m_mimeData->clear(); - thisDrag->m_mimeTypesCount = 0; +QWasmDrag::DragState::DragState(QDrag *drag, QWindow *window, + std::function<void()> quitEventLoopClosure) + : drag(drag), window(window), quitEventLoopClosure(std::move(quitEventLoopClosure)) +{ } +QWasmDrag::DragState::~DragState() = default; + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdrag.h b/src/plugins/platforms/wasm/qwasmdrag.h index 6358b415c3..146a69ebe8 100644 --- a/src/plugins/platforms/wasm/qwasmdrag.h +++ b/src/plugins/platforms/wasm/qwasmdrag.h @@ -1,43 +1,47 @@ -// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only -#ifndef QWASMDRAG_H -#define QWASMDRAG_H +#ifndef QWINDOWSDRAG_H +#define QWINDOWSDRAG_H -#include <qpa/qplatformdrag.h> -#include <private/qsimpledrag_p.h> #include <private/qstdweb_p.h> -#include <QDrag> -#include "qwasmscreen.h" +#include <private/qsimpledrag_p.h> + +#include <qpa/qplatformdrag.h> +#include <QtGui/qdrag.h> -QT_REQUIRE_CONFIG(draganddrop); +#include <memory> QT_BEGIN_NAMESPACE -class QWasmDrag : public QSimpleDrag +struct DragEvent; + +class QWasmDrag final : public QSimpleDrag { public: - QWasmDrag(); - ~QWasmDrag(); + ~QWasmDrag() override; + QWasmDrag(const QWasmDrag &other) = delete; + QWasmDrag(QWasmDrag &&other) = delete; + QWasmDrag &operator=(const QWasmDrag &other) = delete; + QWasmDrag &operator=(QWasmDrag &&other) = delete; + + static QWasmDrag *instance(); - void drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) override; - void move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) override; + void onNativeDragOver(DragEvent *event); + void onNativeDrop(DragEvent *event); + void onNativeDragStarted(DragEvent *event); + void onNativeDragFinished(DragEvent *event); - Qt::MouseButton m_qButton; - QPoint m_mouseDropPoint; - QFlags<Qt::KeyboardModifier> m_keyModifiers; - Qt::DropActions m_dropActions; - QWasmScreen *m_wasmScreen = nullptr; - int m_mimeTypesCount = 0; - QMimeData *m_mimeData = nullptr; - void qWasmDrop(); + // QPlatformDrag: + Qt::DropAction drag(QDrag *drag) final; private: - void init(); -}; + struct DragState; + std::unique_ptr<DragState> m_dragState; +}; QT_END_NAMESPACE -#endif // QWASMDRAG_H +#endif // QWINDOWSDRAG_H diff --git a/src/plugins/platforms/wasm/qwasmevent.cpp b/src/plugins/platforms/wasm/qwasmevent.cpp new file mode 100644 index 0000000000..e418263655 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmevent.cpp @@ -0,0 +1,338 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmevent.h" + +#include "qwasmkeytranslator.h" + +#include <QtCore/private/qmakearray_p.h> +#include <QtCore/private/qstringiterator_p.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +namespace { +constexpr std::string_view WebDeadKeyValue = "Dead"; + +bool isDeadKeyEvent(const char *key) +{ + return qstrncmp(key, WebDeadKeyValue.data(), WebDeadKeyValue.size()) == 0; +} + +Qt::Key getKeyFromCode(const std::string &code) +{ + if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(code.c_str())) + return *mapping; + + static QRegularExpression regex(QString(QStringLiteral(R"re((?:Key|Digit)(\w))re"))); + const auto codeQString = QString::fromStdString(code); + const auto match = regex.match(codeQString); + + if (!match.hasMatch()) + return Qt::Key_unknown; + + constexpr size_t CharacterIndex = 1; + return static_cast<Qt::Key>(match.capturedView(CharacterIndex).at(0).toLatin1()); +} + +Qt::Key webKeyToQtKey(const std::string &code, const std::string &key, bool isDeadKey, + QFlags<Qt::KeyboardModifier> modifiers) +{ + if (isDeadKey) { + auto mapped = getKeyFromCode(code); + switch (mapped) { + case Qt::Key_U: + return Qt::Key_Dead_Diaeresis; + case Qt::Key_E: + return Qt::Key_Dead_Acute; + case Qt::Key_I: + return Qt::Key_Dead_Circumflex; + case Qt::Key_N: + return Qt::Key_Dead_Tilde; + case Qt::Key_QuoteLeft: + return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Tilde : Qt::Key_Dead_Grave; + case Qt::Key_6: + return Qt::Key_Dead_Circumflex; + case Qt::Key_Apostrophe: + return modifiers.testFlag(Qt::ShiftModifier) ? Qt::Key_Dead_Diaeresis + : Qt::Key_Dead_Acute; + case Qt::Key_AsciiTilde: + return Qt::Key_Dead_Tilde; + default: + return Qt::Key_unknown; + } + } else if (auto mapping = QWasmKeyTranslator::mapWebKeyTextToQtKey(key.c_str())) { + return *mapping; + } + + // cast to unicode key + QString str = QString::fromUtf8(key.c_str()).toUpper(); + if (str.length() > 1) + return Qt::Key_unknown; + + QStringIterator i(str); + return static_cast<Qt::Key>(i.next(0)); +} +} // namespace + +namespace KeyboardModifier +{ +template <> +QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( + const EmscriptenKeyboardEvent& event) +{ + return internal::Helper<EmscriptenKeyboardEvent>::getModifierForEvent(event) | + (event.location == DOM_KEY_LOCATION_NUMPAD ? Qt::KeypadModifier : Qt::NoModifier); +} +} // namespace KeyboardModifier + +Event::Event(EventType type, emscripten::val webEvent) + : webEvent(webEvent), type(type) +{ +} + +Event::~Event() = default; + +Event::Event(const Event &other) = default; + +Event::Event(Event &&other) = default; + +Event &Event::operator=(const Event &other) = default; + +Event &Event::operator=(Event &&other) = default; + +KeyEvent::KeyEvent(EventType type, emscripten::val event) : Event(type, event) +{ + const auto code = event["code"].as<std::string>(); + const auto webKey = event["key"].as<std::string>(); + deadKey = isDeadKeyEvent(webKey.c_str()); + autoRepeat = event["repeat"].as<bool>(); + modifiers = KeyboardModifier::getForEvent(event); + key = webKeyToQtKey(code, webKey, deadKey, modifiers); + + text = QString::fromUtf8(webKey); + if (text.size() > 1) + text.clear(); + + if (key == Qt::Key_Tab) + text = "\t"; +} + +KeyEvent::~KeyEvent() = default; + +KeyEvent::KeyEvent(const KeyEvent &other) = default; + +KeyEvent::KeyEvent(KeyEvent &&other) = default; + +KeyEvent &KeyEvent::operator=(const KeyEvent &other) = default; + +KeyEvent &KeyEvent::operator=(KeyEvent &&other) = default; + +std::optional<KeyEvent> KeyEvent::fromWebWithDeadKeyTranslation(emscripten::val event, + QWasmDeadKeySupport *deadKeySupport) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "keydown") + return EventType::KeyDown; + else if (eventTypeString == "keyup") + return EventType::KeyUp; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + + auto result = KeyEvent(*eventType, event); + deadKeySupport->applyDeadKeyTranslations(&result); + + return result; +} + +MouseEvent::MouseEvent(EventType type, emscripten::val event) : Event(type, event) +{ + mouseButton = MouseEvent::buttonFromWeb(event["button"].as<int>()); + mouseButtons = MouseEvent::buttonsFromWeb(event["buttons"].as<unsigned short>()); + // The current button state (event.buttons) may be out of sync for some PointerDown + // events where the "down" state is very brief, for example taps on Apple trackpads. + // Qt expects that the current button state is in sync with the event, so we sync + // it up here. + if (type == EventType::PointerDown) + mouseButtons |= mouseButton; + localPoint = QPointF(event["offsetX"].as<qreal>(), event["offsetY"].as<qreal>()); + pointInPage = QPointF(event["pageX"].as<qreal>(), event["pageY"].as<qreal>()); + pointInViewport = QPointF(event["clientX"].as<qreal>(), event["clientY"].as<qreal>()); + modifiers = KeyboardModifier::getForEvent(event); +} + +MouseEvent::~MouseEvent() = default; + +MouseEvent::MouseEvent(const MouseEvent &other) = default; + +MouseEvent::MouseEvent(MouseEvent &&other) = default; + +MouseEvent &MouseEvent::operator=(const MouseEvent &other) = default; + +MouseEvent &MouseEvent::operator=(MouseEvent &&other) = default; + +PointerEvent::PointerEvent(EventType type, emscripten::val event) : MouseEvent(type, event) +{ + pointerId = event["pointerId"].as<int>(); + pointerType = ([type = event["pointerType"].as<std::string>()]() { + if (type == "mouse") + return PointerType::Mouse; + if (type == "touch") + return PointerType::Touch; + if (type == "pen") + return PointerType::Pen; + return PointerType::Other; + })(); + width = event["width"].as<qreal>(); + height = event["height"].as<qreal>(); + pressure = event["pressure"].as<qreal>(); + tiltX = event["tiltX"].as<qreal>(); + tiltY = event["tiltY"].as<qreal>(); + tangentialPressure = event["tangentialPressure"].as<qreal>(); + twist = event["twist"].as<qreal>(); + isPrimary = event["isPrimary"].as<bool>(); +} + +PointerEvent::~PointerEvent() = default; + +PointerEvent::PointerEvent(const PointerEvent &other) = default; + +PointerEvent::PointerEvent(PointerEvent &&other) = default; + +PointerEvent &PointerEvent::operator=(const PointerEvent &other) = default; + +PointerEvent &PointerEvent::operator=(PointerEvent &&other) = default; + +std::optional<PointerEvent> PointerEvent::fromWeb(emscripten::val event) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "pointermove") + return EventType::PointerMove; + else if (eventTypeString == "pointerup") + return EventType::PointerUp; + else if (eventTypeString == "pointerdown") + return EventType::PointerDown; + else if (eventTypeString == "pointerenter") + return EventType::PointerEnter; + else if (eventTypeString == "pointerleave") + return EventType::PointerLeave; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + + return PointerEvent(*eventType, event); +} + +DragEvent::DragEvent(EventType type, emscripten::val event, QWindow *window) + : MouseEvent(type, event), dataTransfer(event["dataTransfer"]), targetWindow(window) +{ + dropAction = ([event]() { + const std::string effect = event["dataTransfer"]["dropEffect"].as<std::string>(); + + if (effect == "copy") + return Qt::CopyAction; + else if (effect == "move") + return Qt::MoveAction; + else if (effect == "link") + return Qt::LinkAction; + return Qt::IgnoreAction; + })(); +} + +DragEvent::~DragEvent() = default; + +DragEvent::DragEvent(const DragEvent &other) = default; + +DragEvent::DragEvent(DragEvent &&other) = default; + +DragEvent &DragEvent::operator=(const DragEvent &other) = default; + +DragEvent &DragEvent::operator=(DragEvent &&other) = default; + +std::optional<DragEvent> DragEvent::fromWeb(emscripten::val event, QWindow *targetWindow) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "dragend") + return EventType::DragEnd; + if (eventTypeString == "dragover") + return EventType::DragOver; + if (eventTypeString == "dragstart") + return EventType::DragStart; + if (eventTypeString == "drop") + return EventType::Drop; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + return DragEvent(*eventType, event, targetWindow); +} + +void DragEvent::cancelDragStart() +{ + Q_ASSERT_X(type == EventType::DragStart, Q_FUNC_INFO, "Only supported for DragStart"); + webEvent.call<void>("preventDefault"); +} + +void DragEvent::acceptDragOver() +{ + Q_ASSERT_X(type == EventType::DragOver, Q_FUNC_INFO, "Only supported for DragOver"); + webEvent.call<void>("preventDefault"); +} + +void DragEvent::acceptDrop() +{ + Q_ASSERT_X(type == EventType::Drop, Q_FUNC_INFO, "Only supported for Drop"); + webEvent.call<void>("preventDefault"); +} + +WheelEvent::WheelEvent(EventType type, emscripten::val event) : MouseEvent(type, event) +{ + deltaMode = ([event]() { + const int deltaMode = event["deltaMode"].as<int>(); + const auto jsWheelEventType = emscripten::val::global("WheelEvent"); + if (deltaMode == jsWheelEventType["DOM_DELTA_PIXEL"].as<int>()) + return DeltaMode::Pixel; + else if (deltaMode == jsWheelEventType["DOM_DELTA_LINE"].as<int>()) + return DeltaMode::Line; + return DeltaMode::Page; + })(); + + delta = QPointF(event["deltaX"].as<qreal>(), event["deltaY"].as<qreal>()); + + webkitDirectionInvertedFromDevice = event["webkitDirectionInvertedFromDevice"].as<bool>(); +} + +WheelEvent::~WheelEvent() = default; + +WheelEvent::WheelEvent(const WheelEvent &other) = default; + +WheelEvent::WheelEvent(WheelEvent &&other) = default; + +WheelEvent &WheelEvent::operator=(const WheelEvent &other) = default; + +WheelEvent &WheelEvent::operator=(WheelEvent &&other) = default; + +std::optional<WheelEvent> WheelEvent::fromWeb(emscripten::val event) +{ + const auto eventType = ([&event]() -> std::optional<EventType> { + const auto eventTypeString = event["type"].as<std::string>(); + + if (eventTypeString == "wheel") + return EventType::Wheel; + return std::nullopt; + })(); + if (!eventType) + return std::nullopt; + return WheelEvent(*eventType, event); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmevent.h b/src/plugins/platforms/wasm/qwasmevent.h new file mode 100644 index 0000000000..bd0fb39f11 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmevent.h @@ -0,0 +1,272 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMEVENT_H +#define QWASMEVENT_H + +#include "qwasmplatform.h" +#include "qwasmdom.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qnamespace.h> +#include <QtGui/qevent.h> +#include <private/qstdweb_p.h> +#include <QPoint> + +#include <emscripten/html5.h> +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + +class QWasmDeadKeySupport; +class QWindow; + +enum class EventType { + DragEnd, + DragOver, + DragStart, + Drop, + KeyDown, + KeyUp, + PointerDown, + PointerMove, + PointerUp, + PointerEnter, + PointerLeave, + PointerCancel, + Wheel, +}; + +enum class PointerType { + Mouse, + Touch, + Pen, + Other, +}; + +enum class WindowArea { + NonClient, + Client, +}; + +enum class DeltaMode { Pixel, Line, Page }; + +namespace KeyboardModifier { +namespace internal +{ + // Check for the existence of shiftKey, ctrlKey, altKey and metaKey in a type. + // Based on that, we can safely assume we are dealing with an emscripten event type. + template<typename T> + struct IsEmscriptenEvent + { + template<typename U, EM_BOOL U::*, EM_BOOL U::*, EM_BOOL U::*, EM_BOOL U::*> + struct SFINAE {}; + template<typename U> static char Test( + SFINAE<U, &U::shiftKey, &U::ctrlKey, &U::altKey, &U::metaKey>*); + template<typename U> static int Test(...); + static const bool value = sizeof(Test<T>(0)) == sizeof(char); + }; + + template<class T, typename Enable = void> + struct Helper; + + template<class T> + struct Helper<T, std::enable_if_t<IsEmscriptenEvent<T>::value>> + { + static QFlags<Qt::KeyboardModifier> getModifierForEvent(const T& event) { + QFlags<Qt::KeyboardModifier> keyModifier = Qt::NoModifier; + if (event.shiftKey) + keyModifier |= Qt::ShiftModifier; + if (event.ctrlKey) + keyModifier |= platform() == Platform::MacOS ? Qt::MetaModifier : Qt::ControlModifier; + if (event.altKey) + keyModifier |= Qt::AltModifier; + if (event.metaKey) + keyModifier |= platform() == Platform::MacOS ? Qt::ControlModifier : Qt::MetaModifier; + + return keyModifier; + } + }; + + template<> + struct Helper<emscripten::val> + { + static QFlags<Qt::KeyboardModifier> getModifierForEvent(const emscripten::val& event) { + QFlags<Qt::KeyboardModifier> keyModifier = Qt::NoModifier; + if (event["shiftKey"].as<bool>()) + keyModifier |= Qt::ShiftModifier; + if (event["ctrlKey"].as<bool>()) + keyModifier |= platform() == Platform::MacOS ? Qt::MetaModifier : Qt::ControlModifier; + if (event["altKey"].as<bool>()) + keyModifier |= Qt::AltModifier; + if (event["metaKey"].as<bool>()) + keyModifier |= platform() == Platform::MacOS ? Qt::ControlModifier : Qt::MetaModifier; + if (event["constructor"]["name"].as<std::string>() == "KeyboardEvent" && + event["location"].as<unsigned int>() == DOM_KEY_LOCATION_NUMPAD) { + keyModifier |= Qt::KeypadModifier; + } + + return keyModifier; + } + }; +} // namespace internal + +template <typename Event> +QFlags<Qt::KeyboardModifier> getForEvent(const Event& event) +{ + return internal::Helper<Event>::getModifierForEvent(event); +} + +template <> +QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( + const EmscriptenKeyboardEvent& event); + +} // namespace KeyboardModifier + +struct Event +{ + Event(EventType type, emscripten::val webEvent); + ~Event(); + Event(const Event &other); + Event(Event &&other); + Event &operator=(const Event &other); + Event &operator=(Event &&other); + + emscripten::val webEvent; + EventType type; + emscripten::val target() const { return webEvent["target"]; } +}; + +struct KeyEvent : public Event +{ + static std::optional<KeyEvent> + fromWebWithDeadKeyTranslation(emscripten::val webEvent, QWasmDeadKeySupport *deadKeySupport); + + KeyEvent(EventType type, emscripten::val webEvent); + ~KeyEvent(); + KeyEvent(const KeyEvent &other); + KeyEvent(KeyEvent &&other); + KeyEvent &operator=(const KeyEvent &other); + KeyEvent &operator=(KeyEvent &&other); + + Qt::Key key; + QFlags<Qt::KeyboardModifier> modifiers; + bool deadKey; + QString text; + bool autoRepeat; +}; + +struct MouseEvent : public Event +{ + MouseEvent(EventType type, emscripten::val webEvent); + ~MouseEvent(); + MouseEvent(const MouseEvent &other); + MouseEvent(MouseEvent &&other); + MouseEvent &operator=(const MouseEvent &other); + MouseEvent &operator=(MouseEvent &&other); + + static constexpr Qt::MouseButton buttonFromWeb(int webButton) { + switch (webButton) { + case 0: + return Qt::LeftButton; + case 1: + return Qt::MiddleButton; + case 2: + return Qt::RightButton; + default: + return Qt::NoButton; + } + } + + static constexpr Qt::MouseButtons buttonsFromWeb(unsigned short webButtons) { + // Coincidentally, Qt and web bitfields match. + return Qt::MouseButtons::fromInt(webButtons); + } + + static constexpr QEvent::Type mouseEventTypeFromEventType( + EventType eventType, WindowArea windowArea) { + switch (eventType) { + case EventType::PointerDown : + return windowArea == WindowArea::Client ? + QEvent::MouseButtonPress : QEvent::NonClientAreaMouseButtonPress; + case EventType::PointerUp : + return windowArea == WindowArea::Client ? + QEvent::MouseButtonRelease : QEvent::NonClientAreaMouseButtonRelease; + case EventType::PointerMove : + return windowArea == WindowArea::Client ? + QEvent::MouseMove : QEvent::NonClientAreaMouseMove; + default: + return QEvent::None; + } + } + + QPointF localPoint; + QPointF pointInPage; + QPointF pointInViewport; + Qt::MouseButton mouseButton; + Qt::MouseButtons mouseButtons; + QFlags<Qt::KeyboardModifier> modifiers; +}; + +struct PointerEvent : public MouseEvent +{ + static std::optional<PointerEvent> fromWeb(emscripten::val webEvent); + + PointerEvent(EventType type, emscripten::val webEvent); + ~PointerEvent(); + PointerEvent(const PointerEvent &other); + PointerEvent(PointerEvent &&other); + PointerEvent &operator=(const PointerEvent &other); + PointerEvent &operator=(PointerEvent &&other); + + PointerType pointerType; + int pointerId; + qreal pressure; + qreal tiltX; + qreal tiltY; + qreal tangentialPressure; + qreal twist; + qreal width; + qreal height; + bool isPrimary; +}; + +struct DragEvent : public MouseEvent +{ + static std::optional<DragEvent> fromWeb(emscripten::val webEvent, QWindow *targetQWindow); + + DragEvent(EventType type, emscripten::val webEvent, QWindow *targetQWindow); + ~DragEvent(); + DragEvent(const DragEvent &other); + DragEvent(DragEvent &&other); + DragEvent &operator=(const DragEvent &other); + DragEvent &operator=(DragEvent &&other); + + void cancelDragStart(); + void acceptDragOver(); + void acceptDrop(); + + Qt::DropAction dropAction; + dom::DataTransfer dataTransfer; + QWindow *targetWindow; +}; + +struct WheelEvent : public MouseEvent +{ + static std::optional<WheelEvent> fromWeb(emscripten::val webEvent); + + WheelEvent(EventType type, emscripten::val webEvent); + ~WheelEvent(); + WheelEvent(const WheelEvent &other); + WheelEvent(WheelEvent &&other); + WheelEvent &operator=(const WheelEvent &other); + WheelEvent &operator=(WheelEvent &&other); + + DeltaMode deltaMode; + bool webkitDirectionInvertedFromDevice; + QPointF delta; +}; + +QT_END_NAMESPACE + +#endif // QWASMEVENT_H diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp index 2fd1a30401..1f2d3095d6 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.cpp @@ -2,16 +2,34 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmeventdispatcher.h" +#include "qwasmintegration.h" #include <QtGui/qpa/qwindowsysteminterface.h> QT_BEGIN_NAMESPACE // Note: All event dispatcher functionality is implemented in QEventDispatcherWasm -// in QtCore, except for processWindowSystemEvents() below which uses API from QtGui. -void QWasmEventDispatcher::processWindowSystemEvents(QEventLoop::ProcessEventsFlags flags) +// in QtCore, except for processPostedEvents() below which uses API from QtGui. +bool QWasmEventDispatcher::processPostedEvents() { - QWindowSystemInterface::sendWindowSystemEvents(flags); + QEventDispatcherWasm::processPostedEvents(); + return QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::AllEvents); +} + +void QWasmEventDispatcher::onLoaded() +{ + // This function is called when the application is ready to paint + // the first frame. Send the qtlaoder onLoaded event first (via + // the base class implementation), and then enable/call requestUpdate + // to deliver a frame. + QEventDispatcherWasm::onLoaded(); + + // Make sure all screens have a defined size; and pick + // up size changes due to onLoaded event handling. + QWasmIntegration *wasmIntegration = QWasmIntegration::get(); + wasmIntegration->resizeAllScreens(); + + wasmIntegration->releaseRequesetUpdateHold(); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventdispatcher.h b/src/plugins/platforms/wasm/qwasmeventdispatcher.h index a28fa7263b..cbf10482e3 100644 --- a/src/plugins/platforms/wasm/qwasmeventdispatcher.h +++ b/src/plugins/platforms/wasm/qwasmeventdispatcher.h @@ -11,7 +11,8 @@ QT_BEGIN_NAMESPACE class QWasmEventDispatcher : public QEventDispatcherWasm { protected: - void processWindowSystemEvents(QEventLoop::ProcessEventsFlags flags) override; + bool processPostedEvents() override; + void onLoaded() override; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp b/src/plugins/platforms/wasm/qwasmeventtranslator.cpp deleted file mode 100644 index 8f9050ba5d..0000000000 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.cpp +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#include "qwasmeventtranslator.h" -#include "qwasmeventdispatcher.h" -#include "qwasmcompositor.h" -#include "qwasmintegration.h" -#include "qwasmclipboard.h" -#include "qwasmstring.h" -#include "qwasmcursor.h" -#include <QtGui/qevent.h> -#include <qpa/qwindowsysteminterface.h> -#include <QtCore/qcoreapplication.h> -#include <QtCore/qglobal.h> -#include <QtCore/qobject.h> - -#include <QtCore/qdeadlinetimer.h> -#include <private/qmakearray_p.h> -#include <QtCore/qnamespace.h> -#include <QCursor> -#include <QtCore/private/qstringiterator_p.h> - -#include <emscripten/bind.h> - -#include <iostream> - -QT_BEGIN_NAMESPACE - -using namespace emscripten; - -typedef struct emkb2qt { - const char *em; - unsigned int qt; - - constexpr bool operator <=(const emkb2qt &that) const noexcept - { - return !(strcmp(that) > 0); - } - - bool operator <(const emkb2qt &that) const noexcept - { - return ::strcmp(em, that.em) < 0; - } - constexpr int strcmp(const emkb2qt &that, const int i = 0) const - { - return em[i] == 0 && that.em[i] == 0 ? 0 - : em[i] == 0 ? -1 - : that.em[i] == 0 ? 1 - : em[i] < that.em[i] ? -1 - : em[i] > that.em[i] ? 1 - : strcmp(that, i + 1); - } -} emkb2qt_t; - -template<unsigned int Qt, char ... EmChar> -struct Emkb2Qt -{ - static constexpr const char storage[sizeof ... (EmChar) + 1] = {EmChar..., '\0'}; - using Type = emkb2qt_t; - static constexpr Type data() noexcept { return Type{storage, Qt}; } -}; - -template<unsigned int Qt, char ... EmChar> constexpr char Emkb2Qt<Qt, EmChar...>::storage[]; - -static constexpr const auto KeyTbl = qMakeArray( - QSortedData< - Emkb2Qt< Qt::Key_Escape, 'E','s','c','a','p','e' >, - Emkb2Qt< Qt::Key_Tab, 'T','a','b' >, - Emkb2Qt< Qt::Key_Backspace, 'B','a','c','k','s','p','a','c','e' >, - Emkb2Qt< Qt::Key_Return, 'E','n','t','e','r' >, - Emkb2Qt< Qt::Key_Insert, 'I','n','s','e','r','t' >, - Emkb2Qt< Qt::Key_Delete, 'D','e','l','e','t','e' >, - Emkb2Qt< Qt::Key_Pause, 'P','a','u','s','e' >, - Emkb2Qt< Qt::Key_Pause, 'C','l','e','a','r' >, - Emkb2Qt< Qt::Key_Home, 'H','o','m','e' >, - Emkb2Qt< Qt::Key_End, 'E','n','d' >, - Emkb2Qt< Qt::Key_Left, 'A','r','r','o','w','L','e','f','t' >, - Emkb2Qt< Qt::Key_Up, 'A','r','r','o','w','U','p' >, - Emkb2Qt< Qt::Key_Right, 'A','r','r','o','w','R','i','g','h','t' >, - Emkb2Qt< Qt::Key_Down, 'A','r','r','o','w','D','o','w','n' >, - Emkb2Qt< Qt::Key_PageUp, 'P','a','g','e','U','p' >, - Emkb2Qt< Qt::Key_PageDown, 'P','a','g','e','D','o','w','n' >, - Emkb2Qt< Qt::Key_Shift, 'S','h','i','f','t' >, - Emkb2Qt< Qt::Key_Control, 'C','o','n','t','r','o','l' >, - Emkb2Qt< Qt::Key_Meta, 'M','e','t','a'>, - Emkb2Qt< Qt::Key_Meta, 'O','S'>, - Emkb2Qt< Qt::Key_Alt, 'A','l','t','L','e','f','t' >, - Emkb2Qt< Qt::Key_Alt, 'A','l','t' >, - Emkb2Qt< Qt::Key_CapsLock, 'C','a','p','s','L','o','c','k' >, - Emkb2Qt< Qt::Key_NumLock, 'N','u','m','L','o','c','k' >, - Emkb2Qt< Qt::Key_ScrollLock, 'S','c','r','o','l','l','L','o','c','k' >, - Emkb2Qt< Qt::Key_F1, 'F','1' >, - Emkb2Qt< Qt::Key_F2, 'F','2' >, - Emkb2Qt< Qt::Key_F3, 'F','3' >, - Emkb2Qt< Qt::Key_F4, 'F','4' >, - Emkb2Qt< Qt::Key_F5, 'F','5' >, - Emkb2Qt< Qt::Key_F6, 'F','6' >, - Emkb2Qt< Qt::Key_F7, 'F','7' >, - Emkb2Qt< Qt::Key_F8, 'F','8' >, - Emkb2Qt< Qt::Key_F9, 'F','9' >, - Emkb2Qt< Qt::Key_F10, 'F','1','0' >, - Emkb2Qt< Qt::Key_F11, 'F','1','1' >, - Emkb2Qt< Qt::Key_F12, 'F','1','2' >, - Emkb2Qt< Qt::Key_F13, 'F','1','3' >, - Emkb2Qt< Qt::Key_F14, 'F','1','4' >, - Emkb2Qt< Qt::Key_F15, 'F','1','5' >, - Emkb2Qt< Qt::Key_F16, 'F','1','6' >, - Emkb2Qt< Qt::Key_F17, 'F','1','7' >, - Emkb2Qt< Qt::Key_F18, 'F','1','8' >, - Emkb2Qt< Qt::Key_F19, 'F','1','9' >, - Emkb2Qt< Qt::Key_F20, 'F','2','0' >, - Emkb2Qt< Qt::Key_F21, 'F','2','1' >, - Emkb2Qt< Qt::Key_F22, 'F','2','2' >, - Emkb2Qt< Qt::Key_F23, 'F','2','3' >, - Emkb2Qt< Qt::Key_Paste, 'P','a','s','t','e' >, - Emkb2Qt< Qt::Key_AltGr, 'A','l','t','R','i','g','h','t' >, - Emkb2Qt< Qt::Key_Help, 'H','e','l','p' >, - Emkb2Qt< Qt::Key_yen, 'I','n','t','l','Y','e','n' >, - Emkb2Qt< Qt::Key_Menu, 'C','o','n','t','e','x','t','M','e','n','u' > - >::Data{} - ); - -static constexpr const auto DeadKeyShiftTbl = qMakeArray( - QSortedData< - // shifted - Emkb2Qt< Qt::Key_Agrave, '\xc3','\x80' >, - Emkb2Qt< Qt::Key_Aacute, '\xc3','\x81' >, - Emkb2Qt< Qt::Key_Acircumflex, '\xc3','\x82' >, - Emkb2Qt< Qt::Key_Adiaeresis, '\xc3','\x84' >, - Emkb2Qt< Qt::Key_AE, '\xc3','\x86' >, - Emkb2Qt< Qt::Key_Atilde, '\xc3','\x83' >, - Emkb2Qt< Qt::Key_Aring, '\xc3','\x85' >, - Emkb2Qt< Qt::Key_Egrave, '\xc3','\x88' >, - Emkb2Qt< Qt::Key_Eacute, '\xc3','\x89' >, - Emkb2Qt< Qt::Key_Ecircumflex, '\xc3','\x8a' >, - Emkb2Qt< Qt::Key_Ediaeresis, '\xc3','\x8b' >, - Emkb2Qt< Qt::Key_Icircumflex, '\xc3','\x8e' >, - Emkb2Qt< Qt::Key_Idiaeresis, '\xc3','\x8f' >, - Emkb2Qt< Qt::Key_Ocircumflex, '\xc3','\x94' >, - Emkb2Qt< Qt::Key_Odiaeresis, '\xc3','\x96' >, - Emkb2Qt< Qt::Key_Ograve, '\xc3','\x92' >, - Emkb2Qt< Qt::Key_Oacute, '\xc3','\x93' >, - Emkb2Qt< Qt::Key_Ooblique, '\xc3','\x98' >, - Emkb2Qt< Qt::Key_Otilde, '\xc3','\x95' >, - Emkb2Qt< Qt::Key_Ucircumflex, '\xc3','\x9b' >, - Emkb2Qt< Qt::Key_Udiaeresis, '\xc3','\x9c' >, - Emkb2Qt< Qt::Key_Ugrave, '\xc3','\x99' >, - Emkb2Qt< Qt::Key_Uacute, '\xc3','\x9a' >, - Emkb2Qt< Qt::Key_Ntilde, '\xc3','\x91' >, - Emkb2Qt< Qt::Key_Ccedilla, '\xc3','\x87' >, - Emkb2Qt< Qt::Key_ydiaeresis, '\xc3','\x8f' > - >::Data{} -); - -QWasmEventTranslator::QWasmEventTranslator() : QObject() -{ -} - -QWasmEventTranslator::~QWasmEventTranslator() -{ -} - -template <typename Event> -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translatKeyModifier(const Event *event) -{ - // macOS CTRL <-> META switching. We most likely want to enable - // the existing switching code in QtGui, but for now do it here. - - QFlags<Qt::KeyboardModifier> keyModifier = Qt::NoModifier; - if (event->shiftKey) - keyModifier |= Qt::ShiftModifier; - if (event->ctrlKey) { - if (g_usePlatformMacSpecifics) - keyModifier |= Qt::MetaModifier; - else - keyModifier |= Qt::ControlModifier; - } - if (event->altKey) - keyModifier |= Qt::AltModifier; - if (event->metaKey) { - if (g_usePlatformMacSpecifics) - keyModifier |= Qt::ControlModifier; - else - keyModifier |= Qt::MetaModifier; - } - - return keyModifier; -} - -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translateKeyboardEventModifier(const EmscriptenKeyboardEvent *event) -{ - QFlags<Qt::KeyboardModifier> keyModifier = translatKeyModifier(event); - - if (event->location == DOM_KEY_LOCATION_NUMPAD) { - keyModifier |= Qt::KeypadModifier; - } - - return keyModifier; -} - -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translateMouseEventModifier(const EmscriptenMouseEvent *mouseEvent) -{ - return translatKeyModifier(mouseEvent); -} - -QFlags<Qt::KeyboardModifier> QWasmEventTranslator::translateTouchEventModifier(const EmscriptenTouchEvent *touchEvent) -{ - return translatKeyModifier(touchEvent); -} - -Qt::Key QWasmEventTranslator::translateEmscriptKey(const EmscriptenKeyboardEvent *emscriptKey) -{ - Qt::Key qtKey = Qt::Key_unknown; - - if (qstrncmp(emscriptKey->key, "Dead", 4) == 0 ) { - emkb2qt_t searchKey1{emscriptKey->code, 0}; - for (auto it1 = KeyTbl.cbegin(); it1 != KeyTbl.end(); ++it1) - if (it1 != KeyTbl.end() && (qstrcmp(searchKey1.em, it1->em) == 0)) { - qtKey = static_cast<Qt::Key>(it1->qt); - } - } - if (qtKey == Qt::Key_unknown) { - emkb2qt_t searchKey{emscriptKey->key, 0}; - // search key - auto it1 = std::lower_bound(KeyTbl.cbegin(), KeyTbl.cend(), searchKey); - if (it1 != KeyTbl.end() && !(searchKey < *it1)) { - qtKey = static_cast<Qt::Key>(it1->qt); - } - } - - if (qtKey == Qt::Key_unknown) { - // cast to unicode key - QString str = QString::fromUtf8(emscriptKey->key).toUpper(); - QStringIterator i(str); - qtKey = static_cast<Qt::Key>(i.next(0)); - } - - return qtKey; -} - -Qt::MouseButton QWasmEventTranslator::translateMouseButton(unsigned short button) -{ - if (button == 0) - return Qt::LeftButton; - else if (button == 1) - return Qt::MiddleButton; - else if (button == 2) - return Qt::RightButton; - - return Qt::NoButton; -} - -struct KeyMapping { Qt::Key from, to; }; - -constexpr KeyMapping tildeKeyTable[] = { // ~ - { Qt::Key_A, Qt::Key_Atilde }, - { Qt::Key_N, Qt::Key_Ntilde }, - { Qt::Key_O, Qt::Key_Otilde }, -}; -constexpr KeyMapping graveKeyTable[] = { // ` - { Qt::Key_A, Qt::Key_Agrave }, - { Qt::Key_E, Qt::Key_Egrave }, - { Qt::Key_I, Qt::Key_Igrave }, - { Qt::Key_O, Qt::Key_Ograve }, - { Qt::Key_U, Qt::Key_Ugrave }, -}; -constexpr KeyMapping acuteKeyTable[] = { // ' - { Qt::Key_A, Qt::Key_Aacute }, - { Qt::Key_E, Qt::Key_Eacute }, - { Qt::Key_I, Qt::Key_Iacute }, - { Qt::Key_O, Qt::Key_Oacute }, - { Qt::Key_U, Qt::Key_Uacute }, - { Qt::Key_Y, Qt::Key_Yacute }, -}; -constexpr KeyMapping diaeresisKeyTable[] = { // umlaut ¨ - { Qt::Key_A, Qt::Key_Adiaeresis }, - { Qt::Key_E, Qt::Key_Ediaeresis }, - { Qt::Key_I, Qt::Key_Idiaeresis }, - { Qt::Key_O, Qt::Key_Odiaeresis }, - { Qt::Key_U, Qt::Key_Udiaeresis }, - { Qt::Key_Y, Qt::Key_ydiaeresis }, -}; -constexpr KeyMapping circumflexKeyTable[] = { // ^ - { Qt::Key_A, Qt::Key_Acircumflex }, - { Qt::Key_E, Qt::Key_Ecircumflex }, - { Qt::Key_I, Qt::Key_Icircumflex }, - { Qt::Key_O, Qt::Key_Ocircumflex }, - { Qt::Key_U, Qt::Key_Ucircumflex }, -}; - -static Qt::Key find_impl(const KeyMapping *first, const KeyMapping *last, Qt::Key key) noexcept -{ - while (first != last) { - if (first->from == key) - return first->to; - ++first; - } - return Qt::Key_unknown; -} - -template <size_t N> -static Qt::Key find(const KeyMapping (&map)[N], Qt::Key key) noexcept -{ - return find_impl(map, map + N, key); -} - -Qt::Key QWasmEventTranslator::translateDeadKey(Qt::Key deadKey, Qt::Key accentBaseKey, bool is_mac) -{ - Qt::Key wasmKey = Qt::Key_unknown; - - if (deadKey == Qt::Key_QuoteLeft ) { - if (is_mac) { // ` macOS: Key_Dead_Grave - wasmKey = find(graveKeyTable, accentBaseKey); - } else { - wasmKey = find(diaeresisKeyTable, accentBaseKey); - } - return wasmKey; - } - - switch (deadKey) { - // case Qt::Key_QuoteLeft: - case Qt::Key_O: // ´ Key_Dead_Grave - wasmKey = find(graveKeyTable, accentBaseKey); - break; - case Qt::Key_E: // ´ Key_Dead_Acute - wasmKey = find(acuteKeyTable, accentBaseKey); - break; - case Qt::Key_AsciiTilde: - case Qt::Key_N:// Key_Dead_Tilde - wasmKey = find(tildeKeyTable, accentBaseKey); - break; - case Qt::Key_U:// ¨ Key_Dead_Diaeresis - wasmKey = find(diaeresisKeyTable, accentBaseKey); - break; - case Qt::Key_I:// macOS Key_Dead_Circumflex - case Qt::Key_6:// linux - case Qt::Key_Apostrophe:// linux - wasmKey = find(circumflexKeyTable, accentBaseKey); - break; - default: - break; - - }; - return wasmKey; -} - -QCursor QWasmEventTranslator::cursorForMode(QWasmCompositor::ResizeMode m) -{ - switch (m) { - case QWasmCompositor::ResizeTopLeft: - case QWasmCompositor::ResizeBottomRight: - return Qt::SizeFDiagCursor; - case QWasmCompositor::ResizeBottomLeft: - case QWasmCompositor::ResizeTopRight: - return Qt::SizeBDiagCursor; - case QWasmCompositor::ResizeTop: - case QWasmCompositor::ResizeBottom: - return Qt::SizeVerCursor; - case QWasmCompositor::ResizeLeft: - case QWasmCompositor::ResizeRight: - return Qt::SizeHorCursor; - case QWasmCompositor::ResizeNone: - return Qt::ArrowCursor; - } - return Qt::ArrowCursor; -} - -QString QWasmEventTranslator::getKeyText(const EmscriptenKeyboardEvent *keyEvent, Qt::Key qtKey) -{ - QString keyText; - - if (m_emDeadKey != Qt::Key_unknown) { - Qt::Key transformedKey = translateDeadKey(m_emDeadKey, qtKey); - - if (transformedKey != Qt::Key_unknown) - qtKey = transformedKey; - - if (keyEvent->shiftKey == 0) { - for (auto it = KeyTbl.cbegin(); it != KeyTbl.end(); ++it) { - if (it != KeyTbl.end() && (qtKey == static_cast<Qt::Key>(it->qt))) { - keyText = it->em; - m_emDeadKey = Qt::Key_unknown; - break; - } - } - } else { - for (auto it = DeadKeyShiftTbl.cbegin(); it != DeadKeyShiftTbl.end(); ++it) { - if (it != DeadKeyShiftTbl.end() && (qtKey == static_cast<Qt::Key>(it->qt))) { - keyText = it->em; - m_emDeadKey = Qt::Key_unknown; - break; - } - } - } - } - if (keyText.isEmpty()) - keyText = QString::fromUtf8(keyEvent->key); - return keyText; -} - -Qt::Key QWasmEventTranslator::getKey(const EmscriptenKeyboardEvent *keyEvent) -{ - Qt::Key qtKey = translateEmscriptKey(keyEvent); - - if (qstrncmp(keyEvent->key, "Dead", 4) == 0 || qtKey == Qt::Key_AltGr) { - qtKey = translateEmscriptKey(keyEvent); - m_emStickyDeadKey = true; - if (keyEvent->shiftKey == 1 && qtKey == Qt::Key_QuoteLeft) - qtKey = Qt::Key_AsciiTilde; - m_emDeadKey = qtKey; - } - - return qtKey; -} - -void QWasmEventTranslator::setStickyDeadKey(const EmscriptenKeyboardEvent *keyEvent) -{ - Qt::Key qtKey = translateEmscriptKey(keyEvent); - - if (m_emStickyDeadKey && qtKey != Qt::Key_Alt) { - m_emStickyDeadKey = false; - } -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmeventtranslator.h b/src/plugins/platforms/wasm/qwasmeventtranslator.h deleted file mode 100644 index d098c10b77..0000000000 --- a/src/plugins/platforms/wasm/qwasmeventtranslator.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#ifndef QWASMEVENTTRANSLATOR_H -#define QWASMEVENTTRANSLATOR_H - -#include <QtCore/qobject.h> -#include <QtCore/qrect.h> -#include <QtCore/qpoint.h> -#include <emscripten/html5.h> -#include "qwasmwindow.h" -#include <QtGui/qinputdevice.h> -#include <QHash> -#include <QCursor> - -QT_BEGIN_NAMESPACE - -class QWindow; - -class QWasmEventTranslator : public QObject -{ - Q_OBJECT - -public: - - explicit QWasmEventTranslator(); - ~QWasmEventTranslator(); - - template <typename Event> - QFlags<Qt::KeyboardModifier> translatKeyModifier(const Event *event); - - static Qt::Key translateEmscriptKey(const EmscriptenKeyboardEvent *emscriptKey); - QFlags<Qt::KeyboardModifier> translateKeyboardEventModifier(const EmscriptenKeyboardEvent *keyEvent); - QFlags<Qt::KeyboardModifier> translateMouseEventModifier(const EmscriptenMouseEvent *mouseEvent); - QFlags<Qt::KeyboardModifier> translateTouchEventModifier(const EmscriptenTouchEvent *touchEvent); - static Qt::MouseButton translateMouseButton(unsigned short button); - static QCursor cursorForMode(QWasmCompositor::ResizeMode mode); - - QString getKeyText(const EmscriptenKeyboardEvent *keyEvent, Qt::Key key); - Qt::Key getKey(const EmscriptenKeyboardEvent *keyEvent); - void setStickyDeadKey(const EmscriptenKeyboardEvent *keyEvent); - - void setIsMac(bool is_mac) {g_usePlatformMacSpecifics = is_mac;}; - bool g_usePlatformMacSpecifics = false; - -Q_SIGNALS: - void getWindowAt(const QPoint &point, QWindow **window); -private: - static Qt::Key translateDeadKey(Qt::Key deadKey, Qt::Key accentBaseKey, bool is_mac = false); - -private: - static quint64 getTimestamp(); - - Qt::Key m_emDeadKey = Qt::Key_unknown; - bool m_emStickyDeadKey = false; - -}; - -QT_END_NAMESPACE -#endif // QWASMEVENTTRANSLATOR_H diff --git a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp index 7f90986121..3f3dc10f71 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp @@ -2,22 +2,271 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmfontdatabase.h" +#include "qwasmintegration.h" #include <QtCore/qfile.h> +#include <QtCore/private/qstdweb_p.h> +#include <QtCore/private/qeventdispatcher_wasm_p.h> +#include <QtGui/private/qguiapplication_p.h> + +#include <emscripten.h> +#include <emscripten/val.h> +#include <emscripten/bind.h> + +#include <map> +#include <array> QT_BEGIN_NAMESPACE +using namespace emscripten; using namespace Qt::StringLiterals; -void QWasmFontDatabase::populateFontDatabase() + +namespace { + +class FontData +{ +public: + FontData(val fontData) + :m_fontData(fontData) {} + + QString family() const + { + return QString::fromStdString(m_fontData["family"].as<std::string>()); + } + + QString fullName() const + { + return QString::fromStdString(m_fontData["fullName"].as<std::string>()); + } + + QString postscriptName() const + { + return QString::fromStdString(m_fontData["postscriptName"].as<std::string>()); + } + + QString style() const + { + return QString::fromStdString(m_fontData["style"].as<std::string>()); + } + + val value() const + { + return m_fontData; + } + +private: + val m_fontData; +}; + +val makeObject(const char *key, const char *value) +{ + val obj = val::object(); + obj.set(key, std::string(value)); + return obj; +} + +void printError(val err) { + qCWarning(lcQpaFonts) + << QString::fromStdString(err["name"].as<std::string>()) + << QString::fromStdString(err["message"].as<std::string>()); + QWasmFontDatabase::endAllFontFileLoading(); +} + +void checkFontAccessPermitted(std::function<void(bool)> callback) +{ + const val permissions = val::global("navigator")["permissions"]; + if (permissions.isUndefined()) { + callback(false); + return; + } + + qstdweb::Promise::make(permissions, "query", { + .thenFunc = [callback](val status) { + callback(status["state"].as<std::string>() == "granted"); + }, + }, makeObject("name", "local-fonts")); +} + +void queryLocalFonts(std::function<void(const QList<FontData> &)> callback) +{ + emscripten::val window = emscripten::val::global("window"); + qstdweb::Promise::make(window, "queryLocalFonts", { + .thenFunc = [callback](emscripten::val fontArray) { + QList<FontData> fonts; + const int count = fontArray["length"].as<int>(); + fonts.reserve(count); + for (int i = 0; i < count; ++i) + fonts.append(FontData(fontArray.call<emscripten::val>("at", i))); + callback(fonts); + }, + .catchFunc = printError + }); +} + +void readBlob(val blob, std::function<void(const QByteArray &)> callback) +{ + qstdweb::Promise::make(blob, "arrayBuffer", { + .thenFunc = [callback](emscripten::val fontArrayBuffer) { + QByteArray fontData = qstdweb::Uint8Array(qstdweb::ArrayBuffer(fontArrayBuffer)).copyToQByteArray(); + callback(fontData); + }, + .catchFunc = printError + }); +} + +void readFont(FontData font, std::function<void(const QByteArray &)> callback) +{ + qstdweb::Promise::make(font.value(), "blob", { + .thenFunc = [callback](val blob) { + readBlob(blob, [callback](const QByteArray &data) { + callback(data); + }); + }, + .catchFunc = printError + }); +} + +emscripten::val getLocalFontsConfigProperty(const char *name) { + emscripten::val qt = val::module_property("qt"); + if (qt.isUndefined()) + return emscripten::val(); + emscripten::val localFonts = qt["localFonts"]; + if (localFonts.isUndefined()) + return emscripten::val(); + return localFonts[name]; +}; + +bool getLocalFontsBoolConfigPropertyWithDefault(const char *name, bool defaultValue) { + emscripten::val prop = getLocalFontsConfigProperty(name); + if (prop.isUndefined()) + return defaultValue; + return prop.as<bool>(); +}; + +QString getLocalFontsStringConfigPropertyWithDefault(const char *name, QString defaultValue) { + emscripten::val prop = getLocalFontsConfigProperty(name); + if (prop.isUndefined()) + return defaultValue; + return QString::fromStdString(prop.as<std::string>()); +}; + +QStringList getLocalFontsStringListConfigPropertyWithDefault(const char *name, QStringList defaultValue) { + emscripten::val array = getLocalFontsConfigProperty(name); + if (array.isUndefined()) + return defaultValue; + + QStringList list; + int size = array["length"].as<int>(); + for (int i = 0; i < size; ++i) { + emscripten::val element = array.call<emscripten::val>("at", i); + QString string = QString::fromStdString(element.as<std::string>()); + if (!string.isEmpty()) + list.append(string); + } + return list; +}; + +} // namespace + +QWasmFontDatabase::QWasmFontDatabase() +:QFreeTypeFontDatabase() +{ + m_localFontsApiSupported = val::global("window")["queryLocalFonts"].isUndefined() == false; + if (m_localFontsApiSupported) + beginFontDatabaseStartupTask(); +} + +QWasmFontDatabase *QWasmFontDatabase::get() +{ + return static_cast<QWasmFontDatabase *>(QWasmIntegration::get()->fontDatabase()); +} + +// Populates the font database with local fonts. Will make the browser ask +// the user for permission if needed. Does nothing if the Local Font Access API +// is not supported. +void QWasmFontDatabase::populateLocalfonts() { - // Load font file from resources. Currently - // all fonts needs to be bundled with the nexe - // as Qt resources. + // Decide which font families to populate based on user preferences + QStringList selectedLocalFontFamilies; + bool allFamilies = false; + switch (m_localFontFamilyLoadSet) { + case NoFontFamilies: + default: + // keep empty selectedLocalFontFamilies + break; + case DefaultFontFamilies: { + const QStringList webSafeFontFamilies = + {"Arial", "Verdana", "Tahoma", "Trebuchet", "Times New Roman", + "Georgia", "Garamond", "Courier New"}; + selectedLocalFontFamilies = webSafeFontFamilies; + } break; + case AllFontFamilies: + allFamilies = true; + break; + } + + selectedLocalFontFamilies += m_extraLocalFontFamilies; + + if (selectedLocalFontFamilies.isEmpty() && !allFamilies) { + endAllFontFileLoading(); + return; + } + + populateLocalFontFamilies(selectedLocalFontFamilies, allFamilies); +} + +namespace { + QStringList toStringList(emscripten::val array) + { + QStringList list; + int size = array["length"].as<int>(); + for (int i = 0; i < size; ++i) { + emscripten::val element = array.call<emscripten::val>("at", i); + QString string = QString::fromStdString(element.as<std::string>()); + if (!string.isEmpty()) + list.append(string); + } + return list; + } +} + +void QWasmFontDatabase::populateLocalFontFamilies(emscripten::val families) +{ + if (!m_localFontsApiSupported) + return; + populateLocalFontFamilies(toStringList(families), false); +} + +void QWasmFontDatabase::populateLocalFontFamilies(const QStringList &fontFamilies, bool allFamilies) +{ + queryLocalFonts([fontFamilies, allFamilies](const QList<FontData> &fonts) { + refFontFileLoading(); + QList<FontData> filteredFonts; + std::copy_if(fonts.begin(), fonts.end(), std::back_inserter(filteredFonts), + [fontFamilies, allFamilies](FontData fontData) { + return allFamilies || fontFamilies.contains(fontData.family()); + }); + + for (const FontData &font: filteredFonts) { + refFontFileLoading(); + readFont(font, [font](const QByteArray &fontData){ + QFreeTypeFontDatabase::registerFontFamily(font.family()); + QFreeTypeFontDatabase::addTTFile(fontData, QByteArray()); + derefFontFileLoading(); + }); + } + derefFontFileLoading(); + }); + +} + +void QWasmFontDatabase::populateFontDatabase() +{ + // Load bundled font file from resources. const QString fontFileNames[] = { QStringLiteral(":/fonts/DejaVuSansMono.ttf"), - QStringLiteral(":/fonts/Vera.ttf"), QStringLiteral(":/fonts/DejaVuSans.ttf"), }; for (const QString &fontFileName : fontFileNames) { @@ -27,11 +276,45 @@ void QWasmFontDatabase::populateFontDatabase() QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1()); } + + // Get config options for controlling local fonts usage + m_queryLocalFontsPermission = getLocalFontsBoolConfigPropertyWithDefault("requestPermission", false); + QString fontFamilyLoadSet = getLocalFontsStringConfigPropertyWithDefault("familiesCollection", "DefaultFontFamilies"); + m_extraLocalFontFamilies = getLocalFontsStringListConfigPropertyWithDefault("extraFamilies", QStringList()); + + if (fontFamilyLoadSet == "NoFontFamilies") { + m_localFontFamilyLoadSet = NoFontFamilies; + } else if (fontFamilyLoadSet == "DefaultFontFamilies") { + m_localFontFamilyLoadSet = DefaultFontFamilies; + } else if (fontFamilyLoadSet == "AllFontFamilies") { + m_localFontFamilyLoadSet = AllFontFamilies; + } else { + m_localFontFamilyLoadSet = NoFontFamilies; + qWarning() << "Unknown fontFamilyLoadSet value" << fontFamilyLoadSet; + } + + if (!m_localFontsApiSupported) + return; + + // Populate the font database with local fonts. Either try unconditianlly + // if displyaing a fonts permissions dialog at startup is allowed, or else + // only if we already have permission. + if (m_queryLocalFontsPermission) { + populateLocalfonts(); + } else { + checkFontAccessPermitted([this](bool granted) { + if (granted) + populateLocalfonts(); + else + endAllFontFileLoading(); + }); + } } QFontEngine *QWasmFontDatabase::fontEngine(const QFontDef &fontDef, void *handle) { - return QFreeTypeFontDatabase::fontEngine(fontDef, handle); + QFontEngine *fontEngine = QFreeTypeFontDatabase::fontEngine(fontDef, handle); + return fontEngine; } QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont::Style style, @@ -41,11 +324,13 @@ QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont:: QStringList fallbacks = QFreeTypeFontDatabase::fallbacksForFamily(family, style, styleHint, script); - // Add the vera.ttf font (loaded in populateFontDatabase above) as a falback font + // Add the DejaVuSans.ttf font (loaded in populateFontDatabase above) as a falback font // to all other fonts (except itself). - const QString veraFontFamily = QStringLiteral("Bitstream Vera Sans"); - if (family != veraFontFamily) - fallbacks.append(veraFontFamily); + static const QString wasmFallbackFonts[] = { "DejaVu Sans" }; + for (auto wasmFallbackFont : wasmFallbackFonts) { + if (family != wasmFallbackFont && !fallbacks.contains(wasmFallbackFont)) + fallbacks.append(wasmFallbackFont); + } return fallbacks; } @@ -57,7 +342,63 @@ void QWasmFontDatabase::releaseHandle(void *handle) QFont QWasmFontDatabase::defaultFont() const { - return QFont("Bitstream Vera Sans"_L1); + return QFont("DejaVu Sans"_L1); +} + +namespace { + int g_pendingFonts = 0; + bool g_fontStartupTaskCompleted = false; +} + +// Registers font loading as a startup task, which makes Qt delay +// sending onLoaded event until font loading has completed. +void QWasmFontDatabase::beginFontDatabaseStartupTask() +{ + g_fontStartupTaskCompleted = false; + QEventDispatcherWasm::registerStartupTask(); +} + +// Ends the font loading startup task. +void QWasmFontDatabase::endFontDatabaseStartupTask() +{ + if (!g_fontStartupTaskCompleted) { + g_fontStartupTaskCompleted = true; + QEventDispatcherWasm::completeStarupTask(); + } +} + +// Registers that a font file will be loaded. +void QWasmFontDatabase::refFontFileLoading() +{ + g_pendingFonts += 1; +} + +// Registers that one font file has been loaded, and sends notifactions +// when all pending font files have been loaded. +void QWasmFontDatabase::derefFontFileLoading() +{ + if (--g_pendingFonts <= 0) { + QFontCache::instance()->clear(); + emit qGuiApp->fontDatabaseChanged(); + endFontDatabaseStartupTask(); + } +} + +// Unconditionally ends local font loading, for instance if there +// are no fonts to load or if there was an unexpected error. +void QWasmFontDatabase::endAllFontFileLoading() +{ + bool hadPandingfonts = g_pendingFonts > 0; + if (hadPandingfonts) { + // The hadPandingfonts counter might no longer be correct; disable counting + // and send notifications unconditionally. + g_pendingFonts = 0; + QFontCache::instance()->clear(); + emit qGuiApp->fontDatabaseChanged(); + } + + endFontDatabaseStartupTask(); } + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmfontdatabase.h b/src/plugins/platforms/wasm/qwasmfontdatabase.h index ccb71b8e0b..a1c8f1ff48 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.h +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.h @@ -6,11 +6,16 @@ #include <QtGui/private/qfreetypefontdatabase_p.h> +#include <emscripten/val.h> + QT_BEGIN_NAMESPACE class QWasmFontDatabase : public QFreeTypeFontDatabase { public: + QWasmFontDatabase(); + static QWasmFontDatabase *get(); + void populateFontDatabase() override; QFontEngine *fontEngine(const QFontDef &fontDef, void *handle) override; QStringList fallbacksForFamily(const QString &family, QFont::Style style, @@ -18,6 +23,27 @@ public: QChar::Script script) const override; void releaseHandle(void *handle) override; QFont defaultFont() const override; + + void populateLocalfonts(); + void populateLocalFontFamilies(emscripten::val families); + void populateLocalFontFamilies(const QStringList &famliies, bool allFamilies); + + static void beginFontDatabaseStartupTask(); + static void endFontDatabaseStartupTask(); + static void refFontFileLoading(); + static void derefFontFileLoading(); + static void endAllFontFileLoading(); + +private: + bool m_localFontsApiSupported = false; + bool m_queryLocalFontsPermission = false; + enum FontFamilyLoadSet { + NoFontFamilies, + DefaultFontFamilies, + AllFontFamilies, + }; + FontFamilyLoadSet m_localFontFamilyLoadSet; + QStringList m_extraLocalFontFamilies; }; QT_END_NAMESPACE #endif diff --git a/src/plugins/platforms/wasm/qwasminputcontext.cpp b/src/plugins/platforms/wasm/qwasminputcontext.cpp index f27ea013c6..ae72e7b7f9 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.cpp +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -5,9 +5,9 @@ #include "qwasminputcontext.h" #include "qwasmintegration.h" +#include "qwasmplatform.h" #include <QRectF> #include <qpa/qplatforminputcontext.h> -#include "qwasmeventtranslator.h" #include "qwasmscreen.h" #include <qguiapplication.h> #include <qwindow.h> @@ -24,14 +24,13 @@ static void inputCallback(emscripten::val event) if (length <= 0) return; - // use only last character - emscripten::val _incomingCharVal = event["target"]["value"][length - 1]; + emscripten::val _incomingCharVal = event["data"]; if (_incomingCharVal != emscripten::val::undefined() && _incomingCharVal != emscripten::val::null()) { QString str = QString::fromStdString(_incomingCharVal.as<std::string>()); QWasmInputContext *wasmInput = - reinterpret_cast<QWasmInputContext*>(event["target"]["data-context"].as<quintptr>()); - wasmInput->inputStringChanged(str, wasmInput); + reinterpret_cast<QWasmInputContext*>(event["target"]["data-qinputcontext"].as<quintptr>()); + wasmInput->inputStringChanged(str, EMSCRIPTEN_EVENT_KEYDOWN, wasmInput); } // this clears the input string, so backspaces do not send a character // but stops suggestions @@ -39,7 +38,7 @@ static void inputCallback(emscripten::val event) } EMSCRIPTEN_BINDINGS(clipboard_module) { - function("qt_InputContextCallback", &inputCallback); + function("qtInputContextCallback", &inputCallback); } QWasmInputContext::QWasmInputContext() @@ -48,31 +47,26 @@ QWasmInputContext::QWasmInputContext() m_inputElement = document.call<emscripten::val>("createElement", std::string("input")); m_inputElement.set("type", "text"); m_inputElement.set("style", "position:absolute;left:-1000px;top:-1000px"); // offscreen - m_inputElement.set("contentaediable","true"); + m_inputElement.set("contenteditable","true"); - if (QWasmIntegration::get()->platform == QWasmIntegration::AndroidPlatform) { - emscripten::val body = document["body"]; - body.call<void>("appendChild", m_inputElement); - - m_inputElement.call<void>("addEventListener", std::string("input"), - emscripten::val::module_property("qt_InputContextCallback"), - emscripten::val(false)); - m_inputElement.set("data-context", - emscripten::val(quintptr(reinterpret_cast<void *>(this)))); - - // android sends Enter through target window, let's just handle this here - emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, (void *)this, 1, - &androidKeyboardCallback); - - } - if (QWasmIntegration::get()->platform == QWasmIntegration::MacOSPlatform || - QWasmIntegration::get()->platform == QWasmIntegration::iPhonePlatform) - { + if (platform() == Platform::MacOS || platform() == Platform::iOS) { auto callback = [=](emscripten::val) { m_inputElement["parentElement"].call<void>("removeChild", m_inputElement); inputPanelIsOpen = false; }; m_blurEventHandler.reset(new EventCallback(m_inputElement, "blur", callback)); + + } else { + + const std::string inputType = platform() == Platform::Windows ? "textInput" : "input"; + + document.call<void>("addEventListener", inputType, + emscripten::val::module_property("qtInputContextCallback"), + emscripten::val(false)); + m_inputElement.set("data-qinputcontext", + emscripten::val(quintptr(reinterpret_cast<void *>(this)))); + emscripten::val body = document["body"]; + body.call<void>("appendChild", m_inputElement); } QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, @@ -81,7 +75,7 @@ QWasmInputContext::QWasmInputContext() QWasmInputContext::~QWasmInputContext() { - if (QWasmIntegration::get()->platform == QWasmIntegration::AndroidPlatform) + if (platform() == Platform::Android || platform() == Platform::Windows) emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, NULL); } @@ -90,14 +84,11 @@ void QWasmInputContext::focusWindowChanged(QWindow *focusWindow) m_focusWindow = focusWindow; } -emscripten::val QWasmInputContext::focusCanvas() +emscripten::val QWasmInputContext::inputHandlerElementForFocusedWindow() { if (!m_focusWindow) return emscripten::val::undefined(); - QScreen *screen = m_focusWindow->screen(); - if (!screen) - return emscripten::val::undefined(); - return QWasmScreen::get(screen)->canvas(); + return static_cast<QWasmWindow *>(m_focusWindow->handle())->inputHandlerElement(); } void QWasmInputContext::update(Qt::InputMethodQueries queries) @@ -107,25 +98,27 @@ void QWasmInputContext::update(Qt::InputMethodQueries queries) void QWasmInputContext::showInputPanel() { - if (QWasmIntegration::get()->platform == QWasmIntegration::WindowsPlatform - && inputPanelIsOpen) // call this only once for win32 - return; + if (platform() == Platform::Windows + && !inputPanelIsOpen) { // call this only once for win32 + m_inputElement.call<void>("focus"); + return; + } // this is called each time the keyboard is touched - // Add the input element as a child of the canvas for the + // Add the input element as a child of the screen for the // currently focused window and give it focus. The browser // will not display the input element, but mobile browsers // should display the virtual keyboard. Key events will be // captured by the keyboard event handler installed on the - // canvas. + // screen element. - if (QWasmIntegration::get()->platform == QWasmIntegration::MacOSPlatform // keep for compatibility - || QWasmIntegration::get()->platform == QWasmIntegration::iPhonePlatform - || QWasmIntegration::get()->platform == QWasmIntegration::WindowsPlatform) { - emscripten::val canvas = focusCanvas(); - if (canvas == emscripten::val::undefined()) + if (platform() == Platform::MacOS // keep for compatibility + || platform() == Platform::iOS + || platform() == Platform::Windows) { + emscripten::val inputWrapper = inputHandlerElementForFocusedWindow(); + if (inputWrapper.isUndefined()) return; - canvas.call<void>("appendChild", m_inputElement); + inputWrapper.call<void>("appendChild", m_inputElement); } m_inputElement.call<void>("focus"); @@ -140,29 +133,29 @@ void QWasmInputContext::hideInputPanel() inputPanelIsOpen = false; } -void QWasmInputContext::inputStringChanged(QString &inputString, QWasmInputContext *context) +void QWasmInputContext::inputStringChanged(QString &inputString, int eventType, QWasmInputContext *context) { Q_UNUSED(context) QKeySequence keys = QKeySequence::fromString(inputString); - // synthesize this keyevent as android is not normal - QWindowSystemInterface::handleKeyEvent<QWindowSystemInterface::SynchronousDelivery>( - 0, QEvent::KeyPress,keys[0].key(), keys[0].keyboardModifiers(), inputString); -} + Qt::Key thisKey = keys[0].key(); -int QWasmInputContext::androidKeyboardCallback(int eventType, - const EmscriptenKeyboardEvent *keyEvent, - void *userData) -{ - // we get Enter, Backspace and function keys via emscripten on target window - Q_UNUSED(eventType) - QString strKey(keyEvent->key); - if (strKey == "Unidentified" || strKey == "Process") - return false; - - QWasmInputContext *wasmInput = reinterpret_cast<QWasmInputContext*>(userData); - wasmInput->inputStringChanged(strKey, wasmInput); + // synthesize this keyevent as android is not normal + if (inputString.size() > 2 && (thisKey < Qt::Key_F35 + || thisKey > Qt::Key_Back)) { + inputString.clear(); + } + if (inputString == QStringLiteral("Escape")) { + thisKey = Qt::Key_Escape; + inputString.clear(); + } else if (thisKey == Qt::Key(0)) { + thisKey = Qt::Key_Return; + } - return true; + QWindowSystemInterface::handleKeyEvent( + 0, eventType == EMSCRIPTEN_EVENT_KEYDOWN ? QEvent::KeyPress : QEvent::KeyRelease, + thisKey, keys[0].keyboardModifiers(), + eventType == EMSCRIPTEN_EVENT_KEYDOWN ? inputString : QStringLiteral("")); } + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h index 58f3920c30..10dd1a0950 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.h +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -29,18 +29,17 @@ public: bool isValid() const override { return true; } void focusWindowChanged(QWindow *focusWindow); - emscripten::val focusCanvas(); - void inputStringChanged(QString &, QWasmInputContext *context); + void inputStringChanged(QString &, int eventType, QWasmInputContext *context); + emscripten::val m_inputElement = emscripten::val::null(); private: + emscripten::val inputHandlerElementForFocusedWindow(); + bool m_inputPanelVisible = false; QPointer<QWindow> m_focusWindow; - emscripten::val m_inputElement = emscripten::val::null(); std::unique_ptr<qstdweb::EventCallback> m_blurEventHandler; std::unique_ptr<qstdweb::EventCallback> m_inputEventHandler; - static int androidKeyboardCallback(int eventType, - const EmscriptenKeyboardEvent *keyEvent, void *userData); bool inputPanelIsOpen = false; }; diff --git a/src/plugins/platforms/wasm/qwasmintegration.cpp b/src/plugins/platforms/wasm/qwasmintegration.cpp index 2e231cc5ce..f5cc3e2eee 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -2,29 +2,26 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmintegration.h" -#include "qwasmeventtranslator.h" #include "qwasmeventdispatcher.h" #include "qwasmcompositor.h" #include "qwasmopenglcontext.h" #include "qwasmtheme.h" #include "qwasmclipboard.h" +#include "qwasmaccessibility.h" #include "qwasmservices.h" #include "qwasmoffscreensurface.h" -#include "qwasmstring.h" - +#include "qwasmplatform.h" #include "qwasmwindow.h" -#ifndef QT_NO_OPENGL -# include "qwasmbackingstore.h" -#endif +#include "qwasmbackingstore.h" #include "qwasmfontdatabase.h" -#if defined(Q_OS_UNIX) -#include <QtGui/private/qgenericunixeventdispatcher_p.h> -#endif +#include "qwasmdrag.h" + #include <qpa/qplatformwindow.h> #include <QtGui/qscreen.h> #include <qpa/qwindowsysteminterface.h> #include <QtCore/qcoreapplication.h> #include <qpa/qplatforminputcontextfactory_p.h> +#include <qpa/qwindowsysteminterface_p.h> #include <emscripten/bind.h> #include <emscripten/val.h> @@ -35,18 +32,25 @@ QT_BEGIN_NAMESPACE +extern void qt_set_sequence_auto_mnemonic(bool); + using namespace emscripten; using namespace Qt::StringLiterals; +static void setContainerElements(emscripten::val elementArray) +{ + QWasmIntegration::get()->setContainerElements(elementArray); +} + static void addContainerElement(emscripten::val element) { - QWasmIntegration::get()->addScreen(element); + QWasmIntegration::get()->addContainerElement(element); } static void removeContainerElement(emscripten::val element) { - QWasmIntegration::get()->removeScreen(element); + QWasmIntegration::get()->removeContainerElement(element); } static void resizeContainerElement(emscripten::val element) @@ -65,94 +69,80 @@ static void resizeAllScreens(emscripten::val event) QWasmIntegration::get()->resizeAllScreens(); } +static void loadLocalFontFamilies(emscripten::val event) +{ + QWasmIntegration::get()->loadLocalFontFamilies(event); +} + EMSCRIPTEN_BINDINGS(qtQWasmIntegraton) { + function("qtSetContainerElements", &setContainerElements); function("qtAddContainerElement", &addContainerElement); function("qtRemoveContainerElement", &removeContainerElement); function("qtResizeContainerElement", &resizeContainerElement); function("qtUpdateDpi", &qtUpdateDpi); function("qtResizeAllScreens", &resizeAllScreens); + function("qtLoadLocalFontFamilies", &loadLocalFontFamilies); } QWasmIntegration *QWasmIntegration::s_instance; QWasmIntegration::QWasmIntegration() - : m_fontDb(nullptr), - m_desktopServices(nullptr), - m_clipboard(new QWasmClipboard) + : m_fontDb(nullptr) + , m_desktopServices(nullptr) + , m_clipboard(new QWasmClipboard) +#if QT_CONFIG(accessibility) + , m_accessibility(new QWasmAccessibility) +#endif { s_instance = this; - touchPoints = emscripten::val::global("navigator")["maxTouchPoints"].as<int>(); - // The Platform Detect: expand coverage as needed - platform = GenericPlatform; - emscripten::val rawPlatform = emscripten::val::global("navigator")["platform"]; - - if (rawPlatform.call<bool>("includes", emscripten::val("Mac"))) - platform = MacOSPlatform; - if (rawPlatform.call<bool>("includes", emscripten::val("iPhone"))) - platform = iPhonePlatform; - if (rawPlatform.call<bool>("includes", emscripten::val("Win32"))) - platform = WindowsPlatform; - if (rawPlatform.call<bool>("includes", emscripten::val("Linux"))) { - platform = LinuxPlatform; - emscripten::val uAgent = emscripten::val::global("navigator")["userAgent"]; - if (uAgent.call<bool>("includes", emscripten::val("Android"))) - platform = AndroidPlatform; - } + if (platform() == Platform::MacOS) + qt_set_sequence_auto_mnemonic(false); + + touchPoints = emscripten::val::global("navigator")["maxTouchPoints"].as<int>(); + QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); - // Create screens for container elements. Each container element can be a div element (preferred), - // or a canvas element (legacy). Qt versions prior to 6.x read the "qtCanvasElements" module property, - // which we continue to do to preserve compatibility. The preferred property is now "qtContainerElements". + // Create screens for container elements. Each container element will ultimately become a + // div element. Qt historically supported supplying canvas for screen elements - these elements + // will be transformed into divs and warnings about deprecation will be printed. See + // QWasmScreen ctor. + emscripten::val filtered = emscripten::val::array(); emscripten::val qtContainerElements = val::module_property("qtContainerElements"); - emscripten::val qtCanvasElements = val::module_property("qtCanvasElements"); - if (!qtContainerElements.isUndefined()) { - emscripten::val length = qtContainerElements["length"]; - int count = length.as<int>(); - if (length.isUndefined()) - qWarning("qtContainerElements does not have the length property set. Qt expects an array of html elements (possibly containing one element only)"); - for (int i = 0; i < count; ++i) { + if (qtContainerElements.isArray()) { + for (int i = 0; i < qtContainerElements["length"].as<int>(); ++i) { emscripten::val element = qtContainerElements[i].as<emscripten::val>(); - if (element.isNull() ||element.isUndefined()) { - qWarning() << "Skipping null or undefined element in qtContainerElements"; - } else { - addScreen(element); - } + if (element.isNull() || element.isUndefined()) + qWarning() << "Skipping null or undefined element in qtContainerElements"; + else + filtered.call<void>("push", element); } - } else if (!qtCanvasElements.isUndefined()) { - qWarning() << "The qtCanvaseElements property is deprecated. Qt will stop reading" - << "it in some future version, please use qtContainerElements instead"; - emscripten::val length = qtCanvasElements["length"]; - int count = length.as<int>(); - for (int i = 0; i < count; ++i) - addScreen(qtCanvasElements[i].as<emscripten::val>()); } else { // No screens, which may or may not be intended - qWarning() << "Note: The qtContainerElements module property was not set. Proceeding with no screens."; + qWarning() << "The qtContainerElements module property was not set or is invalid. " + "Proceeding with no screens."; } + setContainerElements(filtered); // install browser window resize handler - auto onWindowResize = [](int eventType, const EmscriptenUiEvent *e, void *userData) -> int { - Q_UNUSED(eventType); - Q_UNUSED(e); - Q_UNUSED(userData); - - // This resize event is called when the HTML window is resized. Depending - // on the page layout the canvas(es) might also have been resized, so we - // update the Qt screen sizes (and canvas render sizes). - if (QWasmIntegration *integration = QWasmIntegration::get()) - integration->resizeAllScreens(); - return 0; - }; - emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, onWindowResize); + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, + [](int, const EmscriptenUiEvent *, void *) -> int { + // This resize event is called when the HTML window is + // resized. Depending on the page layout the elements might + // also have been resized, so we update the Qt screen sizes + // (and canvas render sizes). + if (QWasmIntegration *integration = QWasmIntegration::get()) + integration->resizeAllScreens(); + return 0; + }); // install visualViewport resize handler which picks up size and scale change on mobile. emscripten::val visualViewport = emscripten::val::global("window")["visualViewport"]; if (!visualViewport.isUndefined()) { visualViewport.call<void>("addEventListener", val("resize"), - val::module_property("qtResizeAllScreens")); + val::module_property("qtResizeAllScreens")); } - m_drag = new QWasmDrag(); + m_drag = std::make_unique<QWasmDrag>(); } QWasmIntegration::~QWasmIntegration() @@ -169,10 +159,12 @@ QWasmIntegration::~QWasmIntegration() delete m_desktopServices; if (m_platformInputContext) delete m_platformInputContext; - delete m_drag; +#if QT_CONFIG(accessibility) + delete m_accessibility; +#endif for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->deleteScreen(); + elementAndScreen.wasmScreen->deleteScreen(); m_screens.clear(); @@ -195,20 +187,18 @@ bool QWasmIntegration::hasCapability(QPlatformIntegration::Capability cap) const QPlatformWindow *QWasmIntegration::createPlatformWindow(QWindow *window) const { - QWasmCompositor *compositor = QWasmScreen::get(window->screen())->compositor(); - return new QWasmWindow(window, compositor, m_backingStores.value(window)); + auto *wasmScreen = QWasmScreen::get(window->screen()); + QWasmCompositor *compositor = wasmScreen->compositor(); + return new QWasmWindow(window, wasmScreen->deadKeySupport(), compositor, + m_backingStores.value(window)); } QPlatformBackingStore *QWasmIntegration::createPlatformBackingStore(QWindow *window) const { -#ifndef QT_NO_OPENGL QWasmCompositor *compositor = QWasmScreen::get(window->screen())->compositor(); QWasmBackingStore *backingStore = new QWasmBackingStore(compositor, window); m_backingStores.insert(window, backingStore); return backingStore; -#else - return nullptr; -#endif } void QWasmIntegration::removeBackingStore(QWindow* window) @@ -216,21 +206,31 @@ void QWasmIntegration::removeBackingStore(QWindow* window) m_backingStores.remove(window); } +void QWasmIntegration::releaseRequesetUpdateHold() +{ + if (QWasmCompositor::releaseRequestUpdateHold()) + { + for (const auto &elementAndScreen : m_screens) { + elementAndScreen.wasmScreen->compositor()->requestUpdate(); + } + } +} + #ifndef QT_NO_OPENGL QPlatformOpenGLContext *QWasmIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - return new QWasmOpenGLContext(context->format()); + return new QWasmOpenGLContext(context); } #endif void QWasmIntegration::initialize() { - if (touchPoints < 1) // only touchscreen need inputcontexts + auto icStrs = QPlatformInputContextFactory::requested(); + if (icStrs.isEmpty() && touchPoints < 1) return; - QString icStr = QPlatformInputContextFactory::requested(); - if (!icStr.isNull()) - m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); + if (!icStrs.isEmpty()) + m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); else m_inputContext.reset(new QWasmInputContext()); } @@ -242,7 +242,7 @@ QPlatformInputContext *QWasmIntegration::inputContext() const QPlatformOffscreenSurface *QWasmIntegration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const { - return new QWasmOffscrenSurface(surface); + return new QWasmOffscreenSurface(surface); } QPlatformFontDatabase *QWasmIntegration::fontDatabase() const @@ -260,10 +260,14 @@ QAbstractEventDispatcher *QWasmIntegration::createEventDispatcher() const QVariant QWasmIntegration::styleHint(QPlatformIntegration::StyleHint hint) const { - if (hint == ShowIsFullScreen) + switch (hint) { + case ShowIsFullScreen: return true; - - return QPlatformIntegration::styleHint(hint); + case UnderlineShortcut: + return platform() != Platform::MacOS; + default: + return QPlatformIntegration::styleHint(hint); + } } Qt::WindowState QWasmIntegration::defaultWindowState(Qt::WindowFlags flags) const @@ -299,34 +303,100 @@ QPlatformClipboard* QWasmIntegration::clipboard() const return m_clipboard; } -void QWasmIntegration::addScreen(const emscripten::val &element) +#ifndef QT_NO_ACCESSIBILITY +QPlatformAccessibility *QWasmIntegration::accessibility() const +{ + return m_accessibility; +} +#endif + +void QWasmIntegration::setContainerElements(emscripten::val elementArray) +{ + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + QList<ScreenMapping> newScreens; + + QList<QWasmScreen *> screensToDelete; + std::transform(m_screens.begin(), m_screens.end(), std::back_inserter(screensToDelete), + [](const ScreenMapping &mapping) { return mapping.wasmScreen; }); + + for (int i = 0; i < elementArray["length"].as<int>(); ++i) { + const auto element = elementArray[i]; + const auto it = std::find_if( + m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; }); + QWasmScreen *screen; + if (it != m_screens.end()) { + screen = it->wasmScreen; + screensToDelete.erase(std::remove_if(screensToDelete.begin(), screensToDelete.end(), + [screen](const QWasmScreen *removedScreen) { + return removedScreen == screen; + }), + screensToDelete.end()); + } else { + screen = new QWasmScreen(element); + QWindowSystemInterface::handleScreenAdded(screen); + } + newScreens.push_back({element, screen}); + } + + std::for_each(screensToDelete.begin(), screensToDelete.end(), + [](QWasmScreen *removed) { removed->deleteScreen(); }); + + m_screens = newScreens; + auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore) + QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter); +} + +void QWasmIntegration::addContainerElement(emscripten::val element) { + Q_ASSERT_X(m_screens.end() + == std::find_if(m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { + return screen.emscriptenVal == element; + }), + Q_FUNC_INFO, "Double-add of an element"); + QWasmScreen *screen = new QWasmScreen(element); - m_screens.append(qMakePair(element, screen)); - m_clipboard->installEventHandlers(element); QWindowSystemInterface::handleScreenAdded(screen); + m_screens.push_back({element, screen}); } -void QWasmIntegration::removeScreen(const emscripten::val &element) +void QWasmIntegration::removeContainerElement(emscripten::val element) { - auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(element); }); + const auto *primaryScreenBefore = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + + const auto it = + std::find_if(m_screens.begin(), m_screens.end(), + [&element](const ScreenMapping &screen) { return screen.emscriptenVal == element; }); if (it == m_screens.end()) { - qWarning() << "Attempting to remove non-existing screen for element" << QWasmString::toQString(element["id"]);; + qWarning() << "Attempt to remove a nonexistent screen."; return; } - it->second->deleteScreen(); + + QWasmScreen *removedScreen = it->wasmScreen; + removedScreen->deleteScreen(); + + m_screens.erase(std::remove_if(m_screens.begin(), m_screens.end(), + [removedScreen](const ScreenMapping &mapping) { + return removedScreen == mapping.wasmScreen; + }), + m_screens.end()); + auto *primaryScreenAfter = m_screens.isEmpty() ? nullptr : m_screens[0].wasmScreen; + if (primaryScreenAfter && primaryScreenAfter != primaryScreenBefore) + QWindowSystemInterface::handlePrimaryScreenChanged(primaryScreenAfter); } void QWasmIntegration::resizeScreen(const emscripten::val &element) { auto it = std::find_if(m_screens.begin(), m_screens.end(), - [&] (const QPair<emscripten::val, QWasmScreen *> &candidate) { return candidate.first.equals(element); }); + [&] (const ScreenMapping &candidate) { return candidate.emscriptenVal.equals(element); }); if (it == m_screens.end()) { - qWarning() << "Attempting to resize non-existing screen for element" << QWasmString::toQString(element["id"]);; + qWarning() << "Attempting to resize non-existing screen for element" + << QString::fromEcmaString(element["id"]); return; } - it->second->updateQScreenAndCanvasRenderSize(); + it->wasmScreen->updateQScreenAndCanvasRenderSize(); } void QWasmIntegration::updateDpi() @@ -336,13 +406,18 @@ void QWasmIntegration::updateDpi() return; qreal dpiValue = dpi.as<qreal>(); for (const auto &elementAndScreen : m_screens) - QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.second->screen(), dpiValue, dpiValue); + QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(elementAndScreen.wasmScreen->screen(), dpiValue, dpiValue); } void QWasmIntegration::resizeAllScreens() { for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->updateQScreenAndCanvasRenderSize(); + elementAndScreen.wasmScreen->updateQScreenAndCanvasRenderSize(); +} + +void QWasmIntegration::loadLocalFontFamilies(emscripten::val families) +{ + m_fontDb->populateLocalFontFamilies(families); } quint64 QWasmIntegration::getTimestamp() @@ -353,7 +428,7 @@ quint64 QWasmIntegration::getTimestamp() #if QT_CONFIG(draganddrop) QPlatformDrag *QWasmIntegration::drag() const { - return m_drag; + return m_drag.get(); } #endif // QT_CONFIG(draganddrop) diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index d996ec3065..870bd0d16b 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -6,23 +6,20 @@ #include "qwasmwindow.h" +#include "qwasminputcontext.h" + #include <qpa/qplatformintegration.h> #include <qpa/qplatformscreen.h> #include <qpa/qplatforminputcontext.h> #include <QtCore/qhash.h> +#include <private/qstdweb_p.h> + #include <emscripten.h> #include <emscripten/html5.h> #include <emscripten/val.h> -#include "qwasminputcontext.h" -#include <private/qstdweb_p.h> - -#if QT_CONFIG(draganddrop) -#include "qwasmdrag.h" -#endif - QT_BEGIN_NAMESPACE class QWasmEventTranslator; @@ -33,21 +30,14 @@ class QWasmScreen; class QWasmCompositor; class QWasmBackingStore; class QWasmClipboard; +class QWasmAccessibility; class QWasmServices; +class QWasmDrag; class QWasmIntegration : public QObject, public QPlatformIntegration { Q_OBJECT public: - enum Platform { - GenericPlatform, - MacOSPlatform, - WindowsPlatform, - LinuxPlatform, - AndroidPlatform, - iPhonePlatform - }; - QWasmIntegration(); ~QWasmIntegration(); @@ -66,6 +56,9 @@ public: QPlatformTheme *createPlatformTheme(const QString &name) const override; QPlatformServices *services() const override; QPlatformClipboard *clipboard() const override; +#ifndef QT_NO_ACCESSIBILITY + QPlatformAccessibility *accessibility() const override; +#endif void initialize() override; QPlatformInputContext *inputContext() const override; @@ -77,23 +70,32 @@ public: QWasmInputContext *getWasmInputContext() { return m_platformInputContext; } static QWasmIntegration *get() { return s_instance; } - void addScreen(const emscripten::val &canvas); - void removeScreen(const emscripten::val &canvas); + void setContainerElements(emscripten::val elementArray); + void addContainerElement(emscripten::val elementArray); + void removeContainerElement(emscripten::val elementArray); void resizeScreen(const emscripten::val &canvas); - void resizeAllScreens(); void updateDpi(); + void resizeAllScreens(); + void loadLocalFontFamilies(emscripten::val families); void removeBackingStore(QWindow* window); + void releaseRequesetUpdateHold(); static quint64 getTimestamp(); - Platform platform; int touchPoints; private: + struct ScreenMapping { + emscripten::val emscriptenVal; + QWasmScreen *wasmScreen; + }; + mutable QWasmFontDatabase *m_fontDb; mutable QWasmServices *m_desktopServices; mutable QHash<QWindow *, QWasmBackingStore *> m_backingStores; - QList<QPair<emscripten::val, QWasmScreen *>> m_screens; + QList<ScreenMapping> m_screens; mutable QWasmClipboard *m_clipboard; + mutable QWasmAccessibility *m_accessibility; + qreal m_fontDpi = -1; mutable QScopedPointer<QPlatformInputContext> m_inputContext; static QWasmIntegration *s_instance; @@ -101,7 +103,7 @@ private: mutable QWasmInputContext *m_platformInputContext = nullptr; #if QT_CONFIG(draganddrop) - QWasmDrag *m_drag; + std::unique_ptr<QWasmDrag> m_drag; #endif }; diff --git a/src/plugins/platforms/wasm/qwasmkeytranslator.cpp b/src/plugins/platforms/wasm/qwasmkeytranslator.cpp new file mode 100644 index 0000000000..8f5240d2d0 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmkeytranslator.cpp @@ -0,0 +1,295 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmkeytranslator.h" +#include "qwasmevent.h" + +#include <QtCore/private/qmakearray_p.h> +#include <QtCore/qglobal.h> +#include <QtCore/qobject.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace { +struct WebKb2QtData +{ + static constexpr char StringTerminator = '\0'; + + const char *web; + unsigned int qt; + + constexpr bool operator<=(const WebKb2QtData &that) const noexcept + { + return !(strcmp(that) > 0); + } + + bool operator<(const WebKb2QtData &that) const noexcept { return ::strcmp(web, that.web) < 0; } + + constexpr bool operator==(const WebKb2QtData &that) const noexcept { return strcmp(that) == 0; } + + constexpr int strcmp(const WebKb2QtData &that, const int i = 0) const + { + return web[i] == StringTerminator && that.web[i] == StringTerminator ? 0 + : web[i] == StringTerminator ? -1 + : that.web[i] == StringTerminator ? 1 + : web[i] < that.web[i] ? -1 + : web[i] > that.web[i] ? 1 + : strcmp(that, i + 1); + } +}; + +template<unsigned int Qt, char... WebChar> +struct Web2Qt +{ + static constexpr const char storage[sizeof...(WebChar) + 1] = { WebChar..., '\0' }; + using Type = WebKb2QtData; + static constexpr Type data() noexcept { return Type{ storage, Qt }; } +}; + +template<unsigned int Qt, char... WebChar> +constexpr char Web2Qt<Qt, WebChar...>::storage[]; + +static constexpr const auto WebToQtKeyCodeMappings = qMakeArray( + QSortedData<Web2Qt<Qt::Key_Alt, 'A', 'l', 't', 'L', 'e', 'f', 't'>, + Web2Qt<Qt::Key_Alt, 'A', 'l', 't'>, + Web2Qt<Qt::Key_AltGr, 'A', 'l', 't', 'R', 'i', 'g', 'h', 't'>, + Web2Qt<Qt::Key_Apostrophe, 'Q', 'u', 'o', 't', 'e'>, + Web2Qt<Qt::Key_Backspace, 'B', 'a', 'c', 'k', 's', 'p', 'a', 'c', 'e'>, + Web2Qt<Qt::Key_CapsLock, 'C', 'a', 'p', 's', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_Control, 'C', 'o', 'n', 't', 'r', 'o', 'l'>, + Web2Qt<Qt::Key_Delete, 'D', 'e', 'l', 'e', 't', 'e'>, + Web2Qt<Qt::Key_Down, 'A', 'r', 'r', 'o', 'w', 'D', 'o', 'w', 'n'>, + Web2Qt<Qt::Key_Escape, 'E', 's', 'c', 'a', 'p', 'e'>, + Web2Qt<Qt::Key_F1, 'F', '1'>, Web2Qt<Qt::Key_F2, 'F', '2'>, + Web2Qt<Qt::Key_F11, 'F', '1', '1'>, Web2Qt<Qt::Key_F12, 'F', '1', '2'>, + Web2Qt<Qt::Key_F13, 'F', '1', '3'>, Web2Qt<Qt::Key_F14, 'F', '1', '4'>, + Web2Qt<Qt::Key_F15, 'F', '1', '5'>, Web2Qt<Qt::Key_F16, 'F', '1', '6'>, + Web2Qt<Qt::Key_F17, 'F', '1', '7'>, Web2Qt<Qt::Key_F18, 'F', '1', '8'>, + Web2Qt<Qt::Key_F19, 'F', '1', '9'>, Web2Qt<Qt::Key_F20, 'F', '2', '0'>, + Web2Qt<Qt::Key_F21, 'F', '2', '1'>, Web2Qt<Qt::Key_F22, 'F', '2', '2'>, + Web2Qt<Qt::Key_F23, 'F', '2', '3'>, + Web2Qt<Qt::Key_F3, 'F', '3'>, Web2Qt<Qt::Key_F4, 'F', '4'>, + Web2Qt<Qt::Key_F5, 'F', '5'>, Web2Qt<Qt::Key_F6, 'F', '6'>, + Web2Qt<Qt::Key_F7, 'F', '7'>, Web2Qt<Qt::Key_F8, 'F', '8'>, + Web2Qt<Qt::Key_F9, 'F', '9'>, Web2Qt<Qt::Key_F10, 'F', '1', '0'>, + Web2Qt<Qt::Key_Help, 'H', 'e', 'l', 'p'>, + Web2Qt<Qt::Key_Home, 'H', 'o', 'm', 'e'>, Web2Qt<Qt::Key_End, 'E', 'n', 'd'>, + Web2Qt<Qt::Key_Insert, 'I', 'n', 's', 'e', 'r', 't'>, + Web2Qt<Qt::Key_Left, 'A', 'r', 'r', 'o', 'w', 'L', 'e', 'f', 't'>, + Web2Qt<Qt::Key_Meta, 'M', 'e', 't', 'a'>, Web2Qt<Qt::Key_Meta, 'O', 'S'>, + Web2Qt<Qt::Key_Menu, 'C', 'o', 'n', 't', 'e', 'x', 't', 'M', 'e', 'n', 'u'>, + Web2Qt<Qt::Key_NumLock, 'N', 'u', 'm', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_PageDown, 'P', 'a', 'g', 'e', 'D', 'o', 'w', 'n'>, + Web2Qt<Qt::Key_PageUp, 'P', 'a', 'g', 'e', 'U', 'p'>, + Web2Qt<Qt::Key_Paste, 'P', 'a', 's', 't', 'e'>, + Web2Qt<Qt::Key_Pause, 'C', 'l', 'e', 'a', 'r'>, + Web2Qt<Qt::Key_Pause, 'P', 'a', 'u', 's', 'e'>, + Web2Qt<Qt::Key_QuoteLeft, 'B', 'a', 'c', 'k', 'q', 'u', 'o', 't', 'e'>, + Web2Qt<Qt::Key_QuoteLeft, 'I', 'n', 't', 'l', 'B', 'a', 'c', 'k', 's', 'l', 'a', 's', 'h'>, + Web2Qt<Qt::Key_Return, 'E', 'n', 't', 'e', 'r'>, + Web2Qt<Qt::Key_Right, 'A', 'r', 'r', 'o', 'w', 'R', 'i', 'g', 'h', 't'>, + Web2Qt<Qt::Key_ScrollLock, 'S', 'c', 'r', 'o', 'l', 'l', 'L', 'o', 'c', 'k'>, + Web2Qt<Qt::Key_Shift, 'S', 'h', 'i', 'f', 't'>, + Web2Qt<Qt::Key_Tab, 'T', 'a', 'b'>, + Web2Qt<Qt::Key_Up, 'A', 'r', 'r', 'o', 'w', 'U', 'p'>, + Web2Qt<Qt::Key_yen, 'I', 'n', 't', 'l', 'Y', 'e', 'n'>>::Data{}); + +static constexpr const auto DiacriticalCharsKeyToTextLowercase = qMakeArray( + QSortedData< + Web2Qt<Qt::Key_Aacute, '\xc3', '\xa1'>, + Web2Qt<Qt::Key_Acircumflex, '\xc3', '\xa2'>, + Web2Qt<Qt::Key_Adiaeresis, '\xc3', '\xa4'>, + Web2Qt<Qt::Key_AE, '\xc3', '\xa6'>, + Web2Qt<Qt::Key_Agrave, '\xc3', '\xa0'>, + Web2Qt<Qt::Key_Aring, '\xc3', '\xa5'>, + Web2Qt<Qt::Key_Atilde, '\xc3', '\xa3'>, + Web2Qt<Qt::Key_Ccedilla, '\xc3', '\xa7'>, + Web2Qt<Qt::Key_Eacute, '\xc3', '\xa9'>, + Web2Qt<Qt::Key_Ecircumflex, '\xc3', '\xaa'>, + Web2Qt<Qt::Key_Ediaeresis, '\xc3', '\xab'>, + Web2Qt<Qt::Key_Egrave, '\xc3', '\xa8'>, + Web2Qt<Qt::Key_Iacute, '\xc3', '\xad'>, + Web2Qt<Qt::Key_Icircumflex, '\xc3', '\xae'>, + Web2Qt<Qt::Key_Idiaeresis, '\xc3', '\xaf'>, + Web2Qt<Qt::Key_Igrave, '\xc3', '\xac'>, + Web2Qt<Qt::Key_Ntilde, '\xc3', '\xb1'>, + Web2Qt<Qt::Key_Oacute, '\xc3', '\xb3'>, + Web2Qt<Qt::Key_Ocircumflex, '\xc3', '\xb4'>, + Web2Qt<Qt::Key_Odiaeresis, '\xc3', '\xb6'>, + Web2Qt<Qt::Key_Ograve, '\xc3', '\xb2'>, + Web2Qt<Qt::Key_Ooblique, '\xc3', '\xb8'>, + Web2Qt<Qt::Key_Otilde, '\xc3', '\xb5'>, + Web2Qt<Qt::Key_Uacute, '\xc3', '\xba'>, + Web2Qt<Qt::Key_Ucircumflex, '\xc3', '\xbb'>, + Web2Qt<Qt::Key_Udiaeresis, '\xc3', '\xbc'>, + Web2Qt<Qt::Key_Ugrave, '\xc3', '\xb9'>, + Web2Qt<Qt::Key_Yacute, '\xc3', '\xbd'>, + Web2Qt<Qt::Key_ydiaeresis, '\xc3', '\xbf'>>::Data{}); + +static constexpr const auto DiacriticalCharsKeyToTextUppercase = qMakeArray( + QSortedData< + Web2Qt<Qt::Key_Aacute, '\xc3', '\x81'>, + Web2Qt<Qt::Key_Acircumflex, '\xc3', '\x82'>, + Web2Qt<Qt::Key_Adiaeresis, '\xc3', '\x84'>, + Web2Qt<Qt::Key_AE, '\xc3', '\x86'>, + Web2Qt<Qt::Key_Agrave, '\xc3', '\x80'>, + Web2Qt<Qt::Key_Aring, '\xc3', '\x85'>, + Web2Qt<Qt::Key_Atilde, '\xc3', '\x83'>, + Web2Qt<Qt::Key_Ccedilla, '\xc3', '\x87'>, + Web2Qt<Qt::Key_Eacute, '\xc3', '\x89'>, + Web2Qt<Qt::Key_Ecircumflex, '\xc3', '\x8a'>, + Web2Qt<Qt::Key_Ediaeresis, '\xc3', '\x8b'>, + Web2Qt<Qt::Key_Egrave, '\xc3', '\x88'>, + Web2Qt<Qt::Key_Iacute, '\xc3', '\x8d'>, + Web2Qt<Qt::Key_Icircumflex, '\xc3', '\x8e'>, + Web2Qt<Qt::Key_Idiaeresis, '\xc3', '\x8f'>, + Web2Qt<Qt::Key_Igrave, '\xc3', '\x8c'>, + Web2Qt<Qt::Key_Ntilde, '\xc3', '\x91'>, + Web2Qt<Qt::Key_Oacute, '\xc3', '\x93'>, + Web2Qt<Qt::Key_Ocircumflex, '\xc3', '\x94'>, + Web2Qt<Qt::Key_Odiaeresis, '\xc3', '\x96'>, + Web2Qt<Qt::Key_Ograve, '\xc3', '\x92'>, + Web2Qt<Qt::Key_Ooblique, '\xc3', '\x98'>, + Web2Qt<Qt::Key_Otilde, '\xc3', '\x95'>, + Web2Qt<Qt::Key_Uacute, '\xc3', '\x9a'>, + Web2Qt<Qt::Key_Ucircumflex, '\xc3', '\x9b'>, + Web2Qt<Qt::Key_Udiaeresis, '\xc3', '\x9c'>, + Web2Qt<Qt::Key_Ugrave, '\xc3', '\x99'>, + Web2Qt<Qt::Key_Yacute, '\xc3', '\x9d'>, + Web2Qt<Qt::Key_ydiaeresis, '\xc5', '\xb8'>>::Data{}); + +static_assert(DiacriticalCharsKeyToTextLowercase.size() + == DiacriticalCharsKeyToTextUppercase.size(), + "Add the new key to both arrays"); + +struct KeyMapping +{ + Qt::Key from, to; +}; + +constexpr KeyMapping tildeKeyTable[] = { + // ~ + { Qt::Key_A, Qt::Key_Atilde }, + { Qt::Key_N, Qt::Key_Ntilde }, + { Qt::Key_O, Qt::Key_Otilde }, +}; +constexpr KeyMapping graveKeyTable[] = { + // ` + { Qt::Key_A, Qt::Key_Agrave }, { Qt::Key_E, Qt::Key_Egrave }, { Qt::Key_I, Qt::Key_Igrave }, + { Qt::Key_O, Qt::Key_Ograve }, { Qt::Key_U, Qt::Key_Ugrave }, +}; +constexpr KeyMapping acuteKeyTable[] = { + // ' + { Qt::Key_A, Qt::Key_Aacute }, { Qt::Key_E, Qt::Key_Eacute }, { Qt::Key_I, Qt::Key_Iacute }, + { Qt::Key_O, Qt::Key_Oacute }, { Qt::Key_U, Qt::Key_Uacute }, { Qt::Key_Y, Qt::Key_Yacute }, +}; +constexpr KeyMapping diaeresisKeyTable[] = { + // umlaut ¨ + { Qt::Key_A, Qt::Key_Adiaeresis }, { Qt::Key_E, Qt::Key_Ediaeresis }, + { Qt::Key_I, Qt::Key_Idiaeresis }, { Qt::Key_O, Qt::Key_Odiaeresis }, + { Qt::Key_U, Qt::Key_Udiaeresis }, { Qt::Key_Y, Qt::Key_ydiaeresis }, +}; +constexpr KeyMapping circumflexKeyTable[] = { + // ^ + { Qt::Key_A, Qt::Key_Acircumflex }, { Qt::Key_E, Qt::Key_Ecircumflex }, + { Qt::Key_I, Qt::Key_Icircumflex }, { Qt::Key_O, Qt::Key_Ocircumflex }, + { Qt::Key_U, Qt::Key_Ucircumflex }, +}; + +static Qt::Key find_impl(const KeyMapping *first, const KeyMapping *last, Qt::Key key) noexcept +{ + while (first != last) { + if (first->from == key) + return first->to; + ++first; + } + return Qt::Key_unknown; +} + +template<size_t N> +static Qt::Key find(const KeyMapping (&map)[N], Qt::Key key) noexcept +{ + return find_impl(map, map + N, key); +} + +Qt::Key translateBaseKeyUsingDeadKey(Qt::Key accentBaseKey, Qt::Key deadKey) +{ + switch (deadKey) { + case Qt::Key_Dead_Grave: + return find(graveKeyTable, accentBaseKey); + case Qt::Key_Dead_Acute: + return find(acuteKeyTable, accentBaseKey); + case Qt::Key_Dead_Tilde: + return find(tildeKeyTable, accentBaseKey); + case Qt::Key_Dead_Diaeresis: + return find(diaeresisKeyTable, accentBaseKey); + case Qt::Key_Dead_Circumflex: + return find(circumflexKeyTable, accentBaseKey); + default: + return Qt::Key_unknown; + }; +} + +template<class T> +std::optional<QString> findKeyTextByKeyId(const T &mappingArray, Qt::Key qtKey) +{ + const auto it = std::find_if(mappingArray.cbegin(), mappingArray.cend(), + [qtKey](const WebKb2QtData &data) { return data.qt == qtKey; }); + return it != mappingArray.cend() ? it->web : std::optional<QString>(); +} +} // namespace + +std::optional<Qt::Key> QWasmKeyTranslator::mapWebKeyTextToQtKey(const char *toFind) +{ + const WebKb2QtData searchKey{ toFind, 0 }; + const auto it = std::lower_bound(WebToQtKeyCodeMappings.cbegin(), WebToQtKeyCodeMappings.cend(), + searchKey); + return it != WebToQtKeyCodeMappings.cend() && searchKey == *it ? static_cast<Qt::Key>(it->qt) + : std::optional<Qt::Key>(); +} + +QWasmDeadKeySupport::QWasmDeadKeySupport() = default; + +QWasmDeadKeySupport::~QWasmDeadKeySupport() = default; + +void QWasmDeadKeySupport::applyDeadKeyTranslations(KeyEvent *event) +{ + if (event->deadKey) { + m_activeDeadKey = event->key; + } else if (m_activeDeadKey != Qt::Key_unknown + && (((m_keyModifiedByDeadKeyOnPress == Qt::Key_unknown + && event->type == EventType::KeyDown)) + || (m_keyModifiedByDeadKeyOnPress == event->key + && event->type == EventType::KeyUp))) { + const Qt::Key baseKey = event->key; + const Qt::Key translatedKey = translateBaseKeyUsingDeadKey(baseKey, m_activeDeadKey); + if (translatedKey != Qt::Key_unknown) { + event->key = translatedKey; + + auto foundText = event->modifiers.testFlag(Qt::ShiftModifier) + ? findKeyTextByKeyId(DiacriticalCharsKeyToTextUppercase, event->key) + : findKeyTextByKeyId(DiacriticalCharsKeyToTextLowercase, event->key); + Q_ASSERT(foundText.has_value()); + event->text = foundText->size() == 1 ? *foundText : QString(); + } + + if (!event->text.isEmpty()) { + if (event->type == EventType::KeyDown) { + // Assume the first keypress with an active dead key is treated as modified, + // regardless of whether it has actually been modified or not. Take into account + // only events that produce actual key text. + if (!event->text.isEmpty()) + m_keyModifiedByDeadKeyOnPress = baseKey; + } else { + Q_ASSERT(event->type == EventType::KeyUp); + Q_ASSERT(m_keyModifiedByDeadKeyOnPress == baseKey); + m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; + m_activeDeadKey = Qt::Key_unknown; + } + } + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmkeytranslator.h b/src/plugins/platforms/wasm/qwasmkeytranslator.h new file mode 100644 index 0000000000..11a89e6193 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmkeytranslator.h @@ -0,0 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMKEYTRANSLATOR_H +#define QWASMKEYTRANSLATOR_H + +#include <QtCore/qnamespace.h> +#include <QtCore/qtypes.h> + +#include <optional> + +QT_BEGIN_NAMESPACE + +struct KeyEvent; + +namespace QWasmKeyTranslator { +std::optional<Qt::Key> mapWebKeyTextToQtKey(const char *toFind); +} + +class QWasmDeadKeySupport +{ +public: + explicit QWasmDeadKeySupport(); + ~QWasmDeadKeySupport(); + + void applyDeadKeyTranslations(KeyEvent *event); + +private: + Qt::Key m_activeDeadKey = Qt::Key_unknown; + Qt::Key m_keyModifiedByDeadKeyOnPress = Qt::Key_unknown; +}; + +QT_END_NAMESPACE +#endif // QWASMKEYTRANSLATOR_H diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp index 79ec9af62b..dcfc4433e6 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp @@ -5,15 +5,31 @@ QT_BEGIN_NAMESPACE -QWasmOffscrenSurface::QWasmOffscrenSurface(QOffscreenSurface *offscreenSurface) - :QPlatformOffscreenSurface(offscreenSurface) +QWasmOffscreenSurface::QWasmOffscreenSurface(QOffscreenSurface *offscreenSurface) + : QPlatformOffscreenSurface(offscreenSurface), m_offscreenCanvas(emscripten::val::undefined()) { + const auto offscreenCanvasClass = emscripten::val::global("OffscreenCanvas"); + // The OffscreenCanvas is not supported on some browsers, most notably on Safari. + if (!offscreenCanvasClass) + return; + m_offscreenCanvas = offscreenCanvasClass.new_(offscreenSurface->size().width(), + offscreenSurface->size().height()); + + m_specialTargetId = std::string("!qtoffscreen_") + std::to_string(uintptr_t(this)); + + emscripten::val::module_property("specialHTMLTargets") + .set(m_specialTargetId, m_offscreenCanvas); } -QWasmOffscrenSurface::~QWasmOffscrenSurface() +QWasmOffscreenSurface::~QWasmOffscreenSurface() { + emscripten::val::module_property("specialHTMLTargets").delete_(m_specialTargetId); +} +bool QWasmOffscreenSurface::isValid() const +{ + return !m_offscreenCanvas.isNull() && !m_offscreenCanvas.isUndefined(); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.h b/src/plugins/platforms/wasm/qwasmoffscreensurface.h index ad9c556d10..1c71310448 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.h +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.h @@ -6,16 +6,25 @@ #include <qpa/qplatformoffscreensurface.h> +#include <emscripten/val.h> + +#include <string> + QT_BEGIN_NAMESPACE class QOffscreenSurface; -class QWasmOffscrenSurface : public QPlatformOffscreenSurface +class QWasmOffscreenSurface final : public QPlatformOffscreenSurface { public: - explicit QWasmOffscrenSurface(QOffscreenSurface *offscreenSurface); - ~QWasmOffscrenSurface(); -private: + explicit QWasmOffscreenSurface(QOffscreenSurface *offscreenSurface); + ~QWasmOffscreenSurface() final; + const std::string &id() const { return m_specialTargetId; } + bool isValid() const override; + +private: + std::string m_specialTargetId; + emscripten::val m_offscreenCanvas; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp index da9219bd6e..8a4664ec8c 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp @@ -2,38 +2,43 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmopenglcontext.h" + +#include "qwasmoffscreensurface.h" #include "qwasmintegration.h" #include <EGL/egl.h> +#include <emscripten/bind.h> #include <emscripten/val.h> +namespace { +void qtDoNothing(emscripten::val) { } +} // namespace + +EMSCRIPTEN_BINDINGS(qwasmopenglcontext) +{ + function("qtDoNothing", &qtDoNothing); +} + QT_BEGIN_NAMESPACE -QWasmOpenGLContext::QWasmOpenGLContext(const QSurfaceFormat &format) - : m_requestedFormat(format) +QWasmOpenGLContext::QWasmOpenGLContext(QOpenGLContext *context) + : m_actualFormat(context->format()), m_qGlContext(context) { - m_requestedFormat.setRenderableType(QSurfaceFormat::OpenGLES); + m_actualFormat.setRenderableType(QSurfaceFormat::OpenGLES); // if we set one, we need to set the other as well since in webgl, these are tied together - if (format.depthBufferSize() < 0 && format.stencilBufferSize() > 0) - m_requestedFormat.setDepthBufferSize(16); - - if (format.stencilBufferSize() < 0 && format.depthBufferSize() > 0) - m_requestedFormat.setStencilBufferSize(8); + if (m_actualFormat.depthBufferSize() < 0 && m_actualFormat.stencilBufferSize() > 0) + m_actualFormat.setDepthBufferSize(16); + if (m_actualFormat.stencilBufferSize() < 0 && m_actualFormat.depthBufferSize() > 0) + m_actualFormat.setStencilBufferSize(8); } QWasmOpenGLContext::~QWasmOpenGLContext() { - if (m_context) { - // Destroy GL context. Work around bug in emscripten_webgl_destroy_context - // which removes all event handlers on the canvas by temporarily removing - // emscripten's JSEvents global object. - emscripten::val jsEvents = emscripten::val::global("window")["JSEvents"]; - emscripten::val::global("window").set("JSEvents", emscripten::val::undefined()); - emscripten_webgl_destroy_context(m_context); - emscripten::val::global("window").set("JSEvents", jsEvents); - m_context = 0; - } + // Destroy GL context. Work around bug in emscripten_webgl_destroy_context + // which removes all event handlers on the canvas by temporarily replacing the function + // that does the removal with a function that does nothing. + destroyWebGLContext(m_ownedWebGLContext.handle); } bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format) @@ -46,27 +51,66 @@ bool QWasmOpenGLContext::isOpenGLVersionSupported(QSurfaceFormat format) (format.majorVersion() == 3 && format.minorVersion() == 0)); } -bool QWasmOpenGLContext::maybeCreateEmscriptenContext(QPlatformSurface *surface) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE +QWasmOpenGLContext::obtainEmscriptenContext(QPlatformSurface *surface) { - // Native emscripten/WebGL contexts are tied to a single screen/canvas. The first - // call to this function creates a native canvas for the given screen, subsequent - // calls verify that the surface is on/off the same screen. - QPlatformScreen *screen = surface->screen(); - if (m_context && !screen) - return false; // Alternative: return true to support makeCurrent on QOffScreenSurface with - // no screen. However, Qt likes to substitute QGuiApplication::primaryScreen() - // for null screens, which foils this plan. - if (!screen) - return false; - if (m_context) - return m_screen == screen; + if (m_ownedWebGLContext.surface == surface) + return m_ownedWebGLContext.handle; + + if (surface->surface()->surfaceClass() == QSurface::Offscreen) { + // Reuse the existing context for offscreen drawing, even if it happens to be a canvas + // context. This is because it is impossible to re-home an existing context to the + // new surface and works as an emulation measure. + if (m_ownedWebGLContext.handle) + return m_ownedWebGLContext.handle; + + // The non-shared offscreen context is heavily limited on WASM, but we provide it + // anyway for potential pixel readbacks. + m_ownedWebGLContext = + QOpenGLContextData{ .surface = surface, + .handle = createEmscriptenContext( + static_cast<QWasmOffscreenSurface *>(surface)->id(), + m_actualFormat) }; + } else { + destroyWebGLContext(m_ownedWebGLContext.handle); + + // Create a full on-screen context for the window canvas. + m_ownedWebGLContext = QOpenGLContextData{ + .surface = surface, + .handle = createEmscriptenContext(static_cast<QWasmWindow *>(surface)->canvasSelector(), + m_actualFormat) + }; + } + + EmscriptenWebGLContextAttributes actualAttributes; + + EMSCRIPTEN_RESULT attributesResult = emscripten_webgl_get_context_attributes(m_ownedWebGLContext.handle, &actualAttributes); + if (attributesResult == EMSCRIPTEN_RESULT_SUCCESS) { + if (actualAttributes.majorVersion == 1) { + m_actualFormat.setMajorVersion(2); + } else if (actualAttributes.majorVersion == 2) { + m_actualFormat.setMajorVersion(3); + } + m_actualFormat.setMinorVersion(0); + } + + return m_ownedWebGLContext.handle; +} - m_context = createEmscriptenContext(QWasmScreen::get(screen)->canvasTargetId(), m_requestedFormat); - m_screen = screen; - return true; +void QWasmOpenGLContext::destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle) +{ + if (!contextHandle) + return; + emscripten::val jsEvents = emscripten::val::module_property("JSEvents"); + emscripten::val savedRemoveAllHandlersOnTargetFunction = jsEvents["removeAllHandlersOnTarget"]; + jsEvents.set("removeAllHandlersOnTarget", emscripten::val::module_property("qtDoNothing")); + emscripten_webgl_destroy_context(contextHandle); + jsEvents.set("removeAllHandlersOnTarget", savedRemoveAllHandlersOnTargetFunction); } -EMSCRIPTEN_WEBGL_CONTEXT_HANDLE QWasmOpenGLContext::createEmscriptenContext(const QString &canvasTargetId, QSurfaceFormat format) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE +QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector, + QSurfaceFormat format) { EmscriptenWebGLContextAttributes attributes; emscripten_webgl_init_context_attributes(&attributes); // Populate with default attributes @@ -75,27 +119,30 @@ EMSCRIPTEN_WEBGL_CONTEXT_HANDLE QWasmOpenGLContext::createEmscriptenContext(cons attributes.failIfMajorPerformanceCaveat = false; attributes.antialias = true; attributes.enableExtensionsByDefault = true; - attributes.majorVersion = format.majorVersion() - 1; - attributes.minorVersion = format.minorVersion(); - + attributes.majorVersion = 2; // try highest supported version ES3.0 / WebGL 2.0 + attributes.minorVersion = 0; // emscripten only supports minor version 0 // WebGL doesn't allow separate attach buffers to STENCIL_ATTACHMENT and DEPTH_ATTACHMENT // we need both or none - bool useDepthStencil = (format.depthBufferSize() > 0 || format.stencilBufferSize() > 0); + const bool useDepthStencil = (format.depthBufferSize() > 0 || format.stencilBufferSize() > 0); // WebGL offers enable/disable control but not size control for these attributes.alpha = format.alphaBufferSize() > 0; attributes.depth = useDepthStencil; attributes.stencil = useDepthStencil; + EMSCRIPTEN_RESULT contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); - QByteArray convasSelector = canvasTargetId.toUtf8(); - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context = emscripten_webgl_create_context(convasSelector.constData(), &attributes); - - return context; + if (contextResult <= 0) { + // fallback to opengles2/webgl1 + // for devices that do not support opengles3/webgl2 + attributes.majorVersion = 1; + contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); + } + return contextResult; } QSurfaceFormat QWasmOpenGLContext::format() const { - return m_requestedFormat; + return m_actualFormat; } GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) const @@ -105,11 +152,22 @@ GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) c bool QWasmOpenGLContext::makeCurrent(QPlatformSurface *surface) { - bool ok = maybeCreateEmscriptenContext(surface); - if (!ok) + static bool sentSharingWarning = false; + if (!sentSharingWarning && isSharing()) { + qWarning() << "The functionality for sharing OpenGL contexts is limited, see documentation"; + sentSharingWarning = true; + } + + if (auto *shareContext = m_qGlContext->shareContext()) + return shareContext->makeCurrent(surface->surface()); + + const auto context = obtainEmscriptenContext(surface); + if (!context) return false; - return emscripten_webgl_make_context_current(m_context) == EMSCRIPTEN_RESULT_SUCCESS; + m_usedWebGLContextHandle = context; + + return emscripten_webgl_make_context_current(context) == EMSCRIPTEN_RESULT_SUCCESS; } void QWasmOpenGLContext::swapBuffers(QPlatformSurface *surface) @@ -125,17 +183,17 @@ void QWasmOpenGLContext::doneCurrent() bool QWasmOpenGLContext::isSharing() const { - return false; + return m_qGlContext->shareContext(); } bool QWasmOpenGLContext::isValid() const { - if (!(isOpenGLVersionSupported(m_requestedFormat))) + if (!isOpenGLVersionSupported(m_actualFormat)) return false; // Note: we get isValid() calls before we see the surface and can // create a native context, so no context is also a valid state. - return !m_context || !emscripten_is_webgl_context_lost(m_context); + return !m_usedWebGLContextHandle || !emscripten_is_webgl_context_lost(m_usedWebGLContextHandle); } QFunctionPointer QWasmOpenGLContext::getProcAddress(const char *procName) diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.h b/src/plugins/platforms/wasm/qwasmopenglcontext.h index 9cd10f3ea6..2a8bcc5d9b 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.h +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.h @@ -1,6 +1,9 @@ // Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#ifndef QWASMOPENGLCONTEXT_H +#define QWASMOPENGLCONTEXT_H + #include <qpa/qplatformopenglcontext.h> #include <emscripten.h> @@ -8,11 +11,13 @@ QT_BEGIN_NAMESPACE +class QOpenGLContext; class QPlatformScreen; +class QPlatformSurface; class QWasmOpenGLContext : public QPlatformOpenGLContext { public: - QWasmOpenGLContext(const QSurfaceFormat &format); + explicit QWasmOpenGLContext(QOpenGLContext *context); ~QWasmOpenGLContext(); QSurfaceFormat format() const override; @@ -25,14 +30,25 @@ public: QFunctionPointer getProcAddress(const char *procName) override; private: + struct QOpenGLContextData + { + QPlatformSurface *surface = nullptr; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle = 0; + }; + static bool isOpenGLVersionSupported(QSurfaceFormat format); - bool maybeCreateEmscriptenContext(QPlatformSurface *surface); - static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE createEmscriptenContext(const QString &canvasId, QSurfaceFormat format); + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE obtainEmscriptenContext(QPlatformSurface *surface); + static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE + createEmscriptenContext(const std::string &canvasSelector, QSurfaceFormat format); + + static void destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle); - QSurfaceFormat m_requestedFormat; - QPlatformScreen *m_screen = nullptr; - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_context = 0; + QSurfaceFormat m_actualFormat; + QOpenGLContext *m_qGlContext; + QOpenGLContextData m_ownedWebGLContext; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_usedWebGLContextHandle = 0; }; QT_END_NAMESPACE +#endif // QWASMOPENGLCONTEXT_H diff --git a/src/plugins/platforms/wasm/qwasmplatform.cpp b/src/plugins/platforms/wasm/qwasmplatform.cpp new file mode 100644 index 0000000000..e54992be1d --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmplatform.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmplatform.h" + +QT_BEGIN_NAMESPACE + +Platform platform() +{ + static const Platform qtDetectedPlatform = ([]() { + // The Platform Detect: expand coverage as needed + emscripten::val rawPlatform = emscripten::val::global("navigator")["platform"]; + + if (rawPlatform.call<bool>("includes", emscripten::val("Mac"))) + return Platform::MacOS; + if (rawPlatform.call<bool>("includes", emscripten::val("iPhone")) + || rawPlatform.call<bool>("includes", emscripten::val("iPad"))) + return Platform::iOS; + if (rawPlatform.call<bool>("includes", emscripten::val("Win32"))) + return Platform::Windows; + if (rawPlatform.call<bool>("includes", emscripten::val("Linux"))) { + emscripten::val uAgent = emscripten::val::global("navigator")["userAgent"]; + if (uAgent.call<bool>("includes", emscripten::val("Android"))) + return Platform::Android; + return Platform::Linux; + } + return Platform::Generic; + })(); + return qtDetectedPlatform; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmplatform.h b/src/plugins/platforms/wasm/qwasmplatform.h new file mode 100644 index 0000000000..5b32e43633 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmplatform.h @@ -0,0 +1,29 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMPLATFORM_H +#define QWASMPLATFORM_H + +#include <QtCore/qglobal.h> +#include <QtCore/qnamespace.h> + +#include <QPoint> + +#include <emscripten/val.h> + +QT_BEGIN_NAMESPACE + +enum class Platform { + Generic, + MacOS, + Windows, + Linux, + Android, + iOS +}; + +Platform platform(); + +QT_END_NAMESPACE + +#endif // QWASMPLATFORM_H diff --git a/src/plugins/platforms/wasm/qwasmscreen.cpp b/src/plugins/platforms/wasm/qwasmscreen.cpp index 42ca608da1..0490b2bfe0 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.cpp +++ b/src/plugins/platforms/wasm/qwasmscreen.cpp @@ -2,116 +2,124 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmscreen.h" -#include "qwasmwindow.h" -#include "qwasmeventtranslator.h" + #include "qwasmcompositor.h" +#include "qwasmcssstyle.h" #include "qwasmintegration.h" -#include "qwasmstring.h" +#include "qwasmkeytranslator.h" +#include "qwasmwindow.h" #include <emscripten/bind.h> #include <emscripten/val.h> -#include <QtGui/private/qeglconvenience_p.h> -#ifndef QT_NO_OPENGL -# include <QtGui/private/qeglplatformcontext_p.h> -#endif #include <qpa/qwindowsysteminterface.h> #include <QtCore/qcoreapplication.h> #include <QtGui/qguiapplication.h> #include <private/qhighdpiscaling_p.h> +#include <tuple> + QT_BEGIN_NAMESPACE using namespace emscripten; -const char * QWasmScreen::m_canvasResizeObserverCallbackContextPropertyName = "data-qtCanvasResizeObserverCallbackContext"; +const char *QWasmScreen::m_canvasResizeObserverCallbackContextPropertyName = + "data-qtCanvasResizeObserverCallbackContext"; QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) - : m_container(containerOrCanvas) - , m_canvas(emscripten::val::undefined()) - , m_compositor(new QWasmCompositor(this)) - , m_eventTranslator(new QWasmEventTranslator()) + : m_container(containerOrCanvas), + m_intermediateContainer(emscripten::val::undefined()), + m_shadowContainer(emscripten::val::undefined()), + m_compositor(new QWasmCompositor(this)), + m_deadKeySupport(std::make_unique<QWasmDeadKeySupport>()) { - // Each screen is backed by a html canvas element. Use either - // a user-supplied canvas or create one as a child of the user- - // supplied root element. - std::string tagName = containerOrCanvas["tagName"].as<std::string>(); - if (tagName == "CANVAS" || tagName == "canvas") { - m_canvas = containerOrCanvas; - } else { - // Create the canvas (for the correct document) as a child of the container - m_canvas = containerOrCanvas["ownerDocument"].call<emscripten::val>("createElement", std::string("canvas")); - containerOrCanvas.call<void>("appendChild", m_canvas); - std::string screenId = std::string("qtcanvas_") + std::to_string(uint32_t(this)); - m_canvas.set("id", screenId); - - // Make the canvas occupy 100% of parent - emscripten::val style = m_canvas["style"]; - style.set("width", std::string("100%")); - style.set("height", std::string("100%")); + auto document = m_container["ownerDocument"]; + // Each screen is represented by a div container. All of the windows exist therein as + // its children. Qt versions < 6.5 used to represent screens as canvas. Support that by + // transforming the canvas into a div. + if (m_container["tagName"].call<std::string>("toLowerCase") == "canvas") { + qWarning() << "Support for canvas elements as an element backing screen is deprecated. The " + "canvas provided for the screen will be transformed into a div."; + auto container = document.call<emscripten::val>("createElement", emscripten::val("div")); + m_container["parentNode"].call<void>("replaceChild", container, m_container); + m_container = container; } - // Configure canvas - emscripten::val style = m_canvas["style"]; - style.set("border", std::string("0px none")); - style.set("background-color", std::string("white")); - - // Set contenteditable so that the canvas gets clipboard events, - // then hide the resulting focus frame, and reset the cursor. - m_canvas.set("contentEditable", std::string("true")); - // set inputmode to none to stop mobile keyboard opening - // when user clicks anywhere on the canvas. - m_canvas.set("inputmode", std::string("none")); - style.set("outline", std::string("0px solid transparent")); - style.set("caret-color", std::string("transparent")); - style.set("cursor", std::string("default")); + // Create an intermediate container which we can remove during cleanup in ~QWasmScreen(). + // This is required due to the attachShadow() call below; there is no corresponding + // "detachShadow()" API to return the container to its previous state. + m_intermediateContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); + m_intermediateContainer.set("id", std::string("qt-shadow-container")); + emscripten::val intermediateContainerStyle = m_intermediateContainer["style"]; + intermediateContainerStyle.set("width", std::string("100%")); + intermediateContainerStyle.set("height", std::string("100%")); + m_container.call<void>("appendChild", m_intermediateContainer); + + auto shadowOptions = emscripten::val::object(); + shadowOptions.set("mode", "open"); + auto shadow = m_intermediateContainer.call<emscripten::val>("attachShadow", shadowOptions); + + m_shadowContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); + + shadow.call<void>("appendChild", QWasmCSSStyle::createStyleElement(m_shadowContainer)); + + shadow.call<void>("appendChild", m_shadowContainer); + + m_shadowContainer.set("id", std::string("qt-screen-") + std::to_string(uintptr_t(this))); + + m_shadowContainer["classList"].call<void>("add", std::string("qt-screen")); // Disable the default context menu; Qt applications typically // provide custom right-click behavior. - m_onContextMenu = std::make_unique<qstdweb::EventCallback>(m_canvas, "contextmenu", [](emscripten::val event){ - event.call<void>("preventDefault"); - }); - - // Create "specialHTMLTargets" mapping for the canvas. Normally, Emscripten - // uses the html element id when targeting elements, for example when registering - // event callbacks. However, this approach is limited to supporting single-document - // apps/ages only, since Emscripten uses the main document to look up the element. - // As a workaround for this, Emscripten supports registering custom mappings in the - // "specialHTMLTargets" object. Add a mapping for the canvas for this screen. - // - // This functionality is gated on "specialHTMLTargets" being available as a module - // property. One way to ensure this is the case is to add it to EXPORTED_RUNTIME_METHODS. - // Qt does not currently do this by default since if added it _must_ be used in order - // to avoid an undefined reference error at startup, and there are cases when Qt won't use - // it, for example if QGuiApplication is not usded. - if (hasSpecialHtmlTargets()) - emscripten::val::module_property("specialHTMLTargets").set(canvasSpecialHtmlTargetId(), m_canvas); - - // Install event handlers on the container/canvas. This must be - // done after the canvas has been created above. - m_compositor->initEventHandlers(); + m_onContextMenu = std::make_unique<qstdweb::EventCallback>( + m_shadowContainer, "contextmenu", + [](emscripten::val event) { event.call<void>("preventDefault"); }); + // Create "specialHTMLTargets" mapping for the canvas - the element might be unreachable based + // on its id only under some conditions, like the target being embedded in a shadow DOM or a + // subframe. + emscripten::val::module_property("specialHTMLTargets") + .set(eventTargetId().toStdString(), m_shadowContainer); + + emscripten::val::module_property("specialHTMLTargets") + .set(outerScreenId().toStdString(), m_container); updateQScreenAndCanvasRenderSize(); - m_canvas.call<void>("focus"); + m_shadowContainer.call<void>("focus"); + + m_touchDevice = std::make_unique<QPointingDevice>( + "touchscreen", 1, QInputDevice::DeviceType::TouchScreen, + QPointingDevice::PointerType::Finger, + QPointingDevice::Capability::Position | QPointingDevice::Capability::Area + | QPointingDevice::Capability::NormalizedPosition, + 10, 0); + m_tabletDevice = std::make_unique<QPointingDevice>( + "stylus", 2, QInputDevice::DeviceType::Stylus, + QPointingDevice::PointerType::Pen, + QPointingDevice::Capability::Position | QPointingDevice::Capability::Pressure + | QPointingDevice::Capability::NormalizedPosition + | QInputDevice::Capability::MouseEmulation + | QInputDevice::Capability::Hover | QInputDevice::Capability::Rotation + | QInputDevice::Capability::XTilt | QInputDevice::Capability::YTilt + | QInputDevice::Capability::TangentialPressure, + 0, 0); + + QWindowSystemInterface::registerInputDevice(m_touchDevice.get()); } QWasmScreen::~QWasmScreen() { - // Delete the compositor before removing the screen from specialHTMLTargets, - // since its destructor needs to look up the target when deregistering - // event handlers. - m_compositor = nullptr; + m_intermediateContainer.call<void>("remove"); - if (hasSpecialHtmlTargets()) - emscripten::val::module_property("specialHTMLTargets") - .set(canvasSpecialHtmlTargetId(), emscripten::val::undefined()); + emscripten::val::module_property("specialHTMLTargets") + .set(eventTargetId().toStdString(), emscripten::val::undefined()); - m_canvas.set(m_canvasResizeObserverCallbackContextPropertyName, emscripten::val(intptr_t(0))); + m_shadowContainer.set(m_canvasResizeObserverCallbackContextPropertyName, + emscripten::val(intptr_t(0))); } void QWasmScreen::deleteScreen() { - m_compositor->destroy(); + // Deletes |this|! QWindowSystemInterface::handleScreenRemoved(this); } @@ -122,6 +130,8 @@ QWasmScreen *QWasmScreen::get(QPlatformScreen *screen) QWasmScreen *QWasmScreen::get(QScreen *screen) { + if (!screen) + return nullptr; return get(screen->handle()); } @@ -130,58 +140,21 @@ QWasmCompositor *QWasmScreen::compositor() return m_compositor.get(); } -QWasmEventTranslator *QWasmScreen::eventTranslator() -{ - return m_eventTranslator.get(); -} - -emscripten::val QWasmScreen::container() const -{ - return m_container; -} - -emscripten::val QWasmScreen::canvas() const -{ - return m_canvas; -} - -// Returns the html element id for the screen's canvas. -QString QWasmScreen::canvasId() const -{ - return QWasmString::toQString(m_canvas["id"]); -} - -// Returns the canvas _target_ id, for use with Emscripten's event registration -// functions. This either based on the id registered in specialHtmlTargets, or -// on the canvas id. -QString QWasmScreen::canvasTargetId() const +emscripten::val QWasmScreen::element() const { - if (hasSpecialHtmlTargets()) - return QString::fromStdString(canvasSpecialHtmlTargetId()); - else - return QStringLiteral("#") + canvasId(); + return m_shadowContainer; } -std::string QWasmScreen::canvasSpecialHtmlTargetId() const +QString QWasmScreen::eventTargetId() const { // Return a globally unique id for the canvas. We can choose any string, // as long as it starts with a "!". - return std::string("!qtcanvas_") + std::to_string(uint32_t(this)); + return QString("!qtcanvas_%1").arg(uintptr_t(this)); } -bool QWasmScreen::hasSpecialHtmlTargets() const +QString QWasmScreen::outerScreenId() const { - static bool gotIt = []{ - // Enable use of specialHTMLTargets, if available - emscripten::val htmlTargets = emscripten::val::module_property("specialHTMLTargets"); - if (htmlTargets.isUndefined()) - return false; - - // Check that the object has the expected type - it can also be - // defined as an abort() function which prints an error on usage. - return htmlTargets["constructor"]["name"].as<std::string>() == std::string("Array"); - }(); - return gotIt; + return QString("!outerscreen_%1").arg(uintptr_t(this)); } QRect QWasmScreen::geometry() const @@ -233,7 +206,7 @@ qreal QWasmScreen::devicePixelRatio() const QString QWasmScreen::name() const { - return canvasId(); + return QString::fromEcmaString(m_shadowContainer["id"]); } QPlatformCursor *QWasmScreen::cursor() const @@ -250,12 +223,30 @@ void QWasmScreen::resizeMaximizedWindows() QWindow *QWasmScreen::topWindow() const { - return m_compositor->keyWindow(); + return activeChild() ? activeChild()->window() : nullptr; } QWindow *QWasmScreen::topLevelAt(const QPoint &p) const { - return m_compositor->windowAt(p); + const auto found = + std::find_if(childStack().begin(), childStack().end(), [&p](const QWasmWindow *window) { + const QRect geometry = window->windowFrameGeometry(); + + return window->isVisible() && geometry.contains(p); + }); + return found != childStack().end() ? (*found)->window() : nullptr; +} + +QPointF QWasmScreen::mapFromLocal(const QPointF &p) const +{ + return geometry().topLeft() + p; +} + +QPointF QWasmScreen::clipPoint(const QPointF &p) const +{ + const auto geometryF = screen()->geometry().toRectF(); + return QPointF(qBound(geometryF.left(), p.x(), geometryF.right()), + qBound(geometryF.top(), p.y(), geometryF.bottom())); } void QWasmScreen::invalidateSize() @@ -266,10 +257,23 @@ void QWasmScreen::invalidateSize() void QWasmScreen::setGeometry(const QRect &rect) { m_geometry = rect; - QWindowSystemInterface::handleScreenGeometryChange(QPlatformScreen::screen(), geometry(), availableGeometry()); + QWindowSystemInterface::handleScreenGeometryChange(QPlatformScreen::screen(), geometry(), + availableGeometry()); resizeMaximizedWindows(); } +void QWasmScreen::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + Q_UNUSED(parent); + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && childStack().size() == 1) { + child->window()->setFlag(Qt::WindowStaysOnBottomHint); + } + QWasmWindowTreeNode::onSubtreeChanged(changeType, parent, child); + m_compositor->onWindowTreeChanged(changeType, child); +} + void QWasmScreen::updateQScreenAndCanvasRenderSize() { // The HTML canvas has two sizes: the CSS size and the canvas render size. @@ -278,28 +282,26 @@ void QWasmScreen::updateQScreenAndCanvasRenderSize() // size must be set manually and is not auto-updated on CSS size change. // Setting the render size to a value larger than the CSS size enables high-dpi // rendering. - - QByteArray canvasSelector = canvasTargetId().toUtf8(); double css_width; double css_height; - emscripten_get_element_css_size(canvasSelector.constData(), &css_width, &css_height); + emscripten_get_element_css_size(outerScreenId().toUtf8().constData(), &css_width, &css_height); QSizeF cssSize(css_width, css_height); QSizeF canvasSize = cssSize * devicePixelRatio(); - m_canvas.set("width", canvasSize.width()); - m_canvas.set("height", canvasSize.height()); + m_shadowContainer.set("width", canvasSize.width()); + m_shadowContainer.set("height", canvasSize.height()); // Returns the html elements document/body position auto getElementBodyPosition = [](const emscripten::val &element) -> QPoint { - emscripten::val bodyRect = element["ownerDocument"]["body"].call<emscripten::val>("getBoundingClientRect"); + emscripten::val bodyRect = + element["ownerDocument"]["body"].call<emscripten::val>("getBoundingClientRect"); emscripten::val canvasRect = element.call<emscripten::val>("getBoundingClientRect"); - return QPoint (canvasRect["left"].as<int>() - bodyRect["left"].as<int>(), - canvasRect["top"].as<int>() - bodyRect["top"].as<int>()); + return QPoint(canvasRect["left"].as<int>() - bodyRect["left"].as<int>(), + canvasRect["top"].as<int>() - bodyRect["top"].as<int>()); }; - setGeometry(QRect(getElementBodyPosition(m_canvas), cssSize.toSize())); - m_compositor->requestUpdateAllWindows(); + setGeometry(QRect(getElementBodyPosition(m_shadowContainer), cssSize.toSize())); } void QWasmScreen::canvasResizeObserverCallback(emscripten::val entries, emscripten::val) @@ -308,20 +310,23 @@ void QWasmScreen::canvasResizeObserverCallback(emscripten::val entries, emscript if (count == 0) return; emscripten::val entry = entries[0]; - QWasmScreen *screen = - reinterpret_cast<QWasmScreen *>(entry["target"][m_canvasResizeObserverCallbackContextPropertyName].as<intptr_t>()); + QWasmScreen *screen = reinterpret_cast<QWasmScreen *>( + entry["target"][m_canvasResizeObserverCallbackContextPropertyName].as<intptr_t>()); if (!screen) { qWarning() << "QWasmScreen::canvasResizeObserverCallback: missing screen pointer"; return; } // We could access contentBoxSize|contentRect|devicePixelContentBoxSize on the entry here, but - // these are not universally supported across all browsers. Get the sizes from the canvas instead. + // these are not universally supported across all browsers. Get the sizes from the canvas + // instead. screen->updateQScreenAndCanvasRenderSize(); } -EMSCRIPTEN_BINDINGS(qtCanvasResizeObserverCallback) { - emscripten::function("qtCanvasResizeObserverCallback", &QWasmScreen::canvasResizeObserverCallback); +EMSCRIPTEN_BINDINGS(qtCanvasResizeObserverCallback) +{ + emscripten::function("qtCanvasResizeObserverCallback", + &QWasmScreen::canvasResizeObserverCallback); } void QWasmScreen::installCanvasResizeObserver() @@ -329,15 +334,44 @@ void QWasmScreen::installCanvasResizeObserver() emscripten::val ResizeObserver = emscripten::val::global("ResizeObserver"); if (ResizeObserver == emscripten::val::undefined()) return; // ResizeObserver API is not available - emscripten::val resizeObserver = ResizeObserver.new_(emscripten::val::module_property("qtCanvasResizeObserverCallback")); + emscripten::val resizeObserver = + ResizeObserver.new_(emscripten::val::module_property("qtCanvasResizeObserverCallback")); if (resizeObserver == emscripten::val::undefined()) return; // Something went horribly wrong // We need to get back to this instance from the (static) resize callback; // set a "data-" property on the canvas element. - m_canvas.set(m_canvasResizeObserverCallbackContextPropertyName, emscripten::val(intptr_t(this))); + m_shadowContainer.set(m_canvasResizeObserverCallbackContextPropertyName, + emscripten::val(intptr_t(this))); + + resizeObserver.call<void>("observe", m_shadowContainer); +} + +emscripten::val QWasmScreen::containerElement() +{ + return m_shadowContainer; +} - resizeObserver.call<void>("observe", m_canvas); +QWasmWindowTreeNode *QWasmScreen::parentNode() +{ + return nullptr; +} + +QList<QWasmWindow *> QWasmScreen::allWindows() +{ + QList<QWasmWindow *> windows; + for (auto *child : childStack()) { + const QWindowList list = child->window()->findChildren<QWindow *>(Qt::FindChildrenRecursively); + for (auto child : list) { + auto handle = child->handle(); + if (handle) { + auto wnd = static_cast<QWasmWindow *>(handle); + windows.push_back(wnd); + } + } + windows.push_back(child); + } + return windows; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmscreen.h b/src/plugins/platforms/wasm/qwasmscreen.h index 0e7d5f1e43..da171d3f50 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.h +++ b/src/plugins/platforms/wasm/qwasmscreen.h @@ -6,6 +6,8 @@ #include "qwasmcursor.h" +#include "qwasmwindowtreenode.h" + #include <qpa/qplatformscreen.h> #include <QtCore/qscopedpointer.h> @@ -20,10 +22,10 @@ class QPlatformOpenGLContext; class QWasmWindow; class QWasmBackingStore; class QWasmCompositor; -class QWasmEventTranslator; +class QWasmDeadKeySupport; class QOpenGLContext; -class QWasmScreen : public QObject, public QPlatformScreen +class QWasmScreen : public QObject, public QPlatformScreen, public QWasmWindowTreeNode { Q_OBJECT public: @@ -33,13 +35,16 @@ public: static QWasmScreen *get(QPlatformScreen *screen); static QWasmScreen *get(QScreen *screen); - emscripten::val container() const; - emscripten::val canvas() const; - QString canvasId() const; - QString canvasTargetId() const; + emscripten::val element() const; + QString eventTargetId() const; + QString outerScreenId() const; + QPointingDevice *touchDevice() { return m_touchDevice.get(); } + QPointingDevice *tabletDevice() { return m_tabletDevice.get(); } QWasmCompositor *compositor(); - QWasmEventTranslator *eventTranslator(); + QWasmDeadKeySupport *deadKeySupport() { return m_deadKeySupport.get(); } + + QList<QWasmWindow *> allWindows(); QRect geometry() const override; int depth() const override; @@ -53,6 +58,13 @@ public: QWindow *topWindow() const; QWindow *topLevelAt(const QPoint &p) const override; + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + + QPointF mapFromLocal(const QPointF &p) const; + QPointF clipPoint(const QPointF &p) const; + void invalidateSize(); void updateQScreenAndCanvasRenderSize(); void installCanvasResizeObserver(); @@ -62,18 +74,22 @@ public slots: void setGeometry(const QRect &rect); private: - std::string canvasSpecialHtmlTargetId() const; - bool hasSpecialHtmlTargets() const; + // QWasmWindowTreeNode: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final; emscripten::val m_container; - emscripten::val m_canvas; + emscripten::val m_intermediateContainer; + emscripten::val m_shadowContainer; std::unique_ptr<QWasmCompositor> m_compositor; - std::unique_ptr<QWasmEventTranslator> m_eventTranslator; + std::unique_ptr<QPointingDevice> m_touchDevice; + std::unique_ptr<QPointingDevice> m_tabletDevice; + std::unique_ptr<QWasmDeadKeySupport> m_deadKeySupport; QRect m_geometry = QRect(0, 0, 100, 100); int m_depth = 32; QImage::Format m_format = QImage::Format_RGB32; QWasmCursor m_cursor; - static const char * m_canvasResizeObserverCallbackContextPropertyName; + static const char *m_canvasResizeObserverCallbackContextPropertyName; std::unique_ptr<qstdweb::EventCallback> m_onContextMenu; }; diff --git a/src/plugins/platforms/wasm/qwasmservices.cpp b/src/plugins/platforms/wasm/qwasmservices.cpp index b9f48090e1..e767295e41 100644 --- a/src/plugins/platforms/wasm/qwasmservices.cpp +++ b/src/plugins/platforms/wasm/qwasmservices.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qwasmservices.h" -#include "qwasmstring.h" #include <QtCore/QUrl> #include <QtCore/QDebug> @@ -13,8 +12,8 @@ QT_BEGIN_NAMESPACE bool QWasmServices::openUrl(const QUrl &url) { - emscripten::val jsUrl = QWasmString::fromQString(url.toString()); - emscripten::val::global("window").call<void>("open", jsUrl, emscripten::val("_blank")); + emscripten::val::global("window").call<void>("open", url.toString().toEcmaString(), + emscripten::val("_blank")); return true; } diff --git a/src/plugins/platforms/wasm/qwasmstring.cpp b/src/plugins/platforms/wasm/qwasmstring.cpp deleted file mode 100644 index 3de84afef3..0000000000 --- a/src/plugins/platforms/wasm/qwasmstring.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2020 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#include "qwasmstring.h" - -QT_BEGIN_NAMESPACE - -using namespace emscripten; - -val QWasmString::fromQString(const QString &str) -{ - static const val UTF16ToString( - val::module_property("UTF16ToString")); - - auto ptr = quintptr(str.utf16()); - return UTF16ToString(val(ptr)); -} - -QString QWasmString::toQString(const val &v) -{ - QString result; - if (!v.isString()) - return result; - - static const val stringToUTF16( - val::module_property("stringToUTF16")); - static const val length("length"); - - int len = v[length].as<int>(); - result.resize(len); - auto ptr = quintptr(result.utf16()); - stringToUTF16(v, val(ptr), val((len + 1) * 2)); - return result; -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmstring.h b/src/plugins/platforms/wasm/qwasmstring.h deleted file mode 100644 index 62927ee93c..0000000000 --- a/src/plugins/platforms/wasm/qwasmstring.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (C) 2020 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#pragma once - -#include <qstring.h> - -#include <emscripten/val.h> - -QT_BEGIN_NAMESPACE - -class QWasmString -{ -public: - static emscripten::val fromQString(const QString &str); - static QString toQString(const emscripten::val &v); -}; -QT_END_NAMESPACE - diff --git a/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h b/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h deleted file mode 100644 index 0f061a38b3..0000000000 --- a/src/plugins/platforms/wasm/qwasmstylepixmaps_p.h +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) 2018 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#ifndef QWASMSTYLEPIXMAPS_P_H -#define QWASMSTYLEPIXMAPS_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -/* XPM */ -static const char * const qt_menu_xpm[] = { -"16 16 72 1", -" c None", -". c #65AF36", -"+ c #66B036", -"@ c #77B94C", -"# c #A7D28C", -"$ c #BADBA4", -"% c #A4D088", -"& c #72B646", -"* c #9ACB7A", -"= c #7FBD56", -"- c #85C05F", -"; c #F4F9F0", -"> c #FFFFFF", -", c #E5F1DC", -"' c #ECF5E7", -") c #7ABA50", -"! c #83BF5C", -"~ c #AED595", -"{ c #D7EACA", -"] c #A9D28D", -"^ c #BCDDA8", -"/ c #C4E0B1", -"( c #81BE59", -"_ c #D0E7C2", -": c #D4E9C6", -"< c #6FB542", -"[ c #6EB440", -"} c #88C162", -"| c #98CA78", -"1 c #F4F9F1", -"2 c #8FC56C", -"3 c #F1F8EC", -"4 c #E8F3E1", -"5 c #D4E9C7", -"6 c #74B748", -"7 c #80BE59", -"8 c #73B747", -"9 c #6DB43F", -"0 c #CBE4BA", -"a c #80BD58", -"b c #6DB33F", -"c c #FEFFFE", -"d c #68B138", -"e c #F9FCF7", -"f c #91C66F", -"g c #E8F3E0", -"h c #DCEDD0", -"i c #91C66E", -"j c #A3CF86", -"k c #C9E3B8", -"l c #B0D697", -"m c #E3F0DA", -"n c #95C873", -"o c #E6F2DE", -"p c #9ECD80", -"q c #BEDEAA", -"r c #C7E2B6", -"s c #79BA4F", -"t c #6EB441", -"u c #BCDCA7", -"v c #FAFCF8", -"w c #F6FAF3", -"x c #84BF5D", -"y c #EDF6E7", -"z c #FAFDF9", -"A c #88C263", -"B c #98CA77", -"C c #CDE5BE", -"D c #67B037", -"E c #D9EBCD", -"F c #6AB23C", -"G c #77B94D", -" .++++++++++++++", -".+++++++++++++++", -"+++@#$%&+++*=+++", -"++-;>,>')+!>~+++", -"++{>]+^>/(_>:~<+", -"+[>>}+|>123>456+", -"+7>>8+->>90>~+++", -"+a>>b+a>c[0>~+++", -"+de>=+f>g+0>~+++", -"++h>i+j>k+0>~+++", -"++l>mno>p+q>rst+", -"++duv>wl++xy>zA+", -"++++B>Cb++++&D++", -"+++++0zE++++++++", -"++++++FG+++++++.", -"++++++++++++++. "}; - -static const char * const qt_close_xpm[] = { -"10 10 2 1", -"# c #000000", -". c None", -"..........", -".##....##.", -"..##..##..", -"...####...", -"....##....", -"...####...", -"..##..##..", -".##....##.", -"..........", -".........."}; - -static const char * const qt_maximize_xpm[]={ -"10 10 2 1", -"# c #000000", -". c None", -"#########.", -"#########.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#.......#.", -"#########.", -".........."}; - - -static const char * const qt_normalizeup_xpm[] = { -"10 10 2 1", -"# c #000000", -". c None", -"...######.", -"...######.", -"...#....#.", -".######.#.", -".######.#.", -".#....###.", -".#....#...", -".#....#...", -".######...", -".........."}; - -QT_END_NAMESPACE -#endif // QWASMSTYLEPIXMAPS_P_H diff --git a/src/plugins/platforms/wasm/qwasmtheme.cpp b/src/plugins/platforms/wasm/qwasmtheme.cpp index 51399159c4..b188dcb4b6 100644 --- a/src/plugins/platforms/wasm/qwasmtheme.cpp +++ b/src/plugins/platforms/wasm/qwasmtheme.cpp @@ -4,6 +4,7 @@ #include "qwasmtheme.h" #include <QtCore/qvariant.h> #include <QFontDatabase> +#include <QList> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 4206ede1bb..0513f46e5b 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -3,71 +3,230 @@ #include <qpa/qwindowsysteminterface.h> #include <private/qguiapplication_p.h> -#include <QtGui/private/qopenglcontext_p.h> +#include <QtCore/qfile.h> #include <QtGui/private/qwindow_p.h> -#include <QtGui/qopenglcontext.h> - +#include <QtGui/private/qhighdpiscaling_p.h> +#include <private/qpixmapcache_p.h> +#include <QtGui/qopenglfunctions.h> +#include <QBuffer> + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmclipboard.h" +#include "qwasmintegration.h" +#include "qwasmkeytranslator.h" #include "qwasmwindow.h" +#include "qwasmwindowclientarea.h" #include "qwasmscreen.h" #include "qwasmcompositor.h" +#include "qwasmevent.h" #include "qwasmeventdispatcher.h" +#include "qwasmaccessibility.h" +#include "qwasmclipboard.h" #include <iostream> +#include <sstream> + +#include <emscripten/val.h> +#include <QtCore/private/qstdweb_p.h> QT_BEGIN_NAMESPACE +namespace { +QWasmWindowStack::PositionPreference positionPreferenceFromWindowFlags(Qt::WindowFlags flags) +{ + if (flags.testFlag(Qt::WindowStaysOnTopHint)) + return QWasmWindowStack::PositionPreference::StayOnTop; + if (flags.testFlag(Qt::WindowStaysOnBottomHint)) + return QWasmWindowStack::PositionPreference::StayOnBottom; + return QWasmWindowStack::PositionPreference::Regular; +} +} // namespace + Q_GUI_EXPORT int qt_defaultDpiX(); -QWasmWindow::QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore) +QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, + QWasmCompositor *compositor, QWasmBackingStore *backingStore) : QPlatformWindow(w), m_window(w), m_compositor(compositor), - m_backingStore(backingStore) + m_backingStore(backingStore), + m_deadKeySupport(deadKeySupport), + m_document(dom::document()), + m_qtWindow(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_windowContents(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_canvasContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_a11yContainer(m_document.call<emscripten::val>("createElement", emscripten::val("div"))), + m_canvas(m_document.call<emscripten::val>("createElement", emscripten::val("canvas"))) { - m_needsCompositor = w->surfaceType() != QSurface::OpenGLSurface; + m_qtWindow.set("className", "qt-window"); + m_qtWindow["style"].set("display", std::string("none")); + + m_nonClientArea = std::make_unique<NonClientArea>(this, m_qtWindow); + m_nonClientArea->titleBar()->setTitle(window()->title()); + + m_clientArea = std::make_unique<ClientArea>(this, compositor->screen(), m_windowContents); + + m_windowContents.set("className", "qt-window-contents"); + m_qtWindow.call<void>("appendChild", m_windowContents); + + m_canvas["classList"].call<void>("add", emscripten::val("qt-window-content")); + + // Set contenteditable so that the canvas gets clipboard events, + // then hide the resulting focus frame. + m_canvas.set("contentEditable", std::string("true")); + m_canvas["style"].set("outline", std::string("none")); + + QWasmClipboard::installEventHandlers(m_canvas); + + // set inputMode to none to stop mobile keyboard opening + // when user clicks anywhere on the canvas. + m_canvas.set("inputMode", std::string("none")); + + // Hide the canvas from screen readers. + m_canvas.call<void>("setAttribute", std::string("aria-hidden"), std::string("true")); + + m_windowContents.call<void>("appendChild", m_canvasContainer); + + m_canvasContainer["classList"].call<void>("add", emscripten::val("qt-window-canvas-container")); + m_canvasContainer.call<void>("appendChild", m_canvas); + + m_canvasContainer.call<void>("appendChild", m_a11yContainer); + m_a11yContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-container")); + + const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface; + if (rendersTo2dContext) + m_context2d = m_canvas.call<emscripten::val>("getContext", emscripten::val("2d")); static int serialNo = 0; - m_winid = ++serialNo; + m_winId = ++serialNo; + m_qtWindow.set("id", "qt-window-" + std::to_string(m_winId)); + emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas); + + m_flags = window()->flags(); + + const auto pointerCallback = std::function([this](emscripten::val event) { + if (processPointer(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + m_pointerEnterCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerenter", pointerCallback); + m_pointerLeaveCallback = + std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerleave", pointerCallback); + + m_wheelEventCallback = std::make_unique<qstdweb::EventCallback>( + m_qtWindow, "wheel", [this](emscripten::val event) { + if (processWheel(*WheelEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + + const auto keyCallback = std::function([this](emscripten::val event) { + if (processKey(*KeyEvent::fromWebWithDeadKeyTranslation(event, m_deadKeySupport))) + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + emscripten::val keyFocusWindow; + if (QWasmIntegration::get()->inputContext()) { + QWasmInputContext *wasmContext = + static_cast<QWasmInputContext *>(QWasmIntegration::get()->inputContext()); + // if there is an touchscreen input context, + // use that window for key input + keyFocusWindow = wasmContext->m_inputElement; + } else { + keyFocusWindow = m_qtWindow; + } - m_compositor->addWindow(this); + m_keyDownCallback = + std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keydown", keyCallback); + m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keyup", keyCallback); - // Pure OpenGL windows draw directly using egl, disable the compositor. - m_compositor->setEnabled(w->surfaceType() != QSurface::OpenGLSurface); + setParent(parent()); } QWasmWindow::~QWasmWindow() { - m_compositor->removeWindow(this); + emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector()); + m_canvasContainer.call<void>("removeChild", m_canvas); + m_context2d = emscripten::val::undefined(); + commitParent(nullptr); if (m_requestAnimationFrameId > -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); +#if QT_CONFIG(accessibility) + QWasmAccessibility::removeAccessibilityEnableButton(window()); +#endif } -void QWasmWindow::destroy() +QSurfaceFormat QWasmWindow::format() const { - if (m_backingStore) - m_backingStore->destroy(); + return window()->requestedFormat(); } -void QWasmWindow::initialize() +QWasmWindow *QWasmWindow::fromWindow(QWindow *window) +{ + return static_cast<QWasmWindow *>(window->handle()); +} + +void QWasmWindow::onRestoreClicked() +{ + window()->setWindowState(Qt::WindowNoState); +} + +void QWasmWindow::onMaximizeClicked() { - QRect rect = windowGeometry(); + window()->setWindowState(Qt::WindowMaximized); +} - QPlatformWindow::setGeometry(rect); +void QWasmWindow::onToggleMaximized() +{ + window()->setWindowState(m_state.testFlag(Qt::WindowMaximized) ? Qt::WindowNoState + : Qt::WindowMaximized); +} - const QSize minimumSize = windowMinimumSize(); - if (rect.width() > 0 || rect.height() > 0) { - rect.setWidth(qBound(1, rect.width(), 2000)); - rect.setHeight(qBound(1, rect.height(), 2000)); - } else if (minimumSize.width() > 0 || minimumSize.height() > 0) { - rect.setSize(minimumSize); - } +void QWasmWindow::onCloseClicked() +{ + window()->close(); +} + +void QWasmWindow::onNonClientAreaInteraction() +{ + requestActivateWindow(); + QGuiApplicationPrivate::instance()->closeAllPopups(); +} + +bool QWasmWindow::onNonClientEvent(const PointerEvent &event) +{ + QPointF pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + return QWindowSystemInterface::handleMouseEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, event.mouseButtons, event.mouseButton, + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::NonClient), + event.modifiers); +} + +void QWasmWindow::initialize() +{ + auto initialGeometry = QPlatformWindow::initialGeometry(window(), + windowGeometry(), defaultWindowSize, defaultWindowSize); + m_normalGeometry = initialGeometry; setWindowState(window()->windowStates()); setWindowFlags(window()->flags()); setWindowTitle(window()->title()); + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); + if (window()->isTopLevel()) setWindowIcon(window()->icon()); - m_normalGeometry = rect; + QPlatformWindow::setGeometry(m_normalGeometry); + +#if QT_CONFIG(accessibility) + // Add accessibility-enable button. The user can activate this + // button to opt-in to accessibility. + if (window()->isTopLevel()) + QWasmAccessibility::addAccessibilityEnableButton(window()); +#endif } QWasmScreen *QWasmWindow::platformScreen() const @@ -75,362 +234,468 @@ QWasmScreen *QWasmWindow::platformScreen() const return static_cast<QWasmScreen *>(window()->screen()->handle()); } -void QWasmWindow::setGeometry(const QRect &rect) +void QWasmWindow::paint() +{ + if (!m_backingStore || !isVisible() || m_context2d.isUndefined()) + return; + + auto image = m_backingStore->getUpdatedWebImage(this); + if (image.isUndefined()) + return; + m_context2d.call<void>("putImageData", image, emscripten::val(0), emscripten::val(0)); +} + +void QWasmWindow::setZOrder(int z) +{ + m_qtWindow["style"].set("zIndex", std::to_string(z)); +} + +void QWasmWindow::setWindowCursor(QByteArray cssCursorName) { - QRect r = rect; - if (m_needsCompositor) { - int yMin = window()->geometry().top() - window()->frameGeometry().top(); + m_windowContents["style"].set("cursor", emscripten::val(cssCursorName.constData())); +} - if (r.y() < yMin) - r.moveTop(yMin); +void QWasmWindow::setGeometry(const QRect &rect) +{ + const auto margins = frameMargins(); + + const QRect clientAreaRect = ([this, &rect, &margins]() { + if (m_state.testFlag(Qt::WindowFullScreen)) + return platformScreen()->geometry(); + if (m_state.testFlag(Qt::WindowMaximized)) + return platformScreen()->availableGeometry().marginsRemoved(frameMargins()); + + auto offset = rect.topLeft() - (!parent() ? screen()->geometry().topLeft() : QPoint()); + + // In viewport + auto containerGeometryInViewport = + QRectF::fromDOMRect(parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")) + .toRect(); + + auto rectInViewport = QRect(containerGeometryInViewport.topLeft() + offset, rect.size()); + + QRect cappedGeometry(rectInViewport); + if (!parent()) { + // Clamp top level windows top position to the screen bounds + cappedGeometry.moveTop( + std::max(std::min(rectInViewport.y(), containerGeometryInViewport.bottom()), + containerGeometryInViewport.y() + margins.top())); + } + cappedGeometry.setSize( + cappedGeometry.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); + return QRect(QPoint(rect.x(), rect.y() + cappedGeometry.y() - rectInViewport.y()), + rect.size()); + })(); + m_nonClientArea->onClientAreaWidthChange(clientAreaRect.width()); + + const auto frameRect = + clientAreaRect + .adjusted(-margins.left(), -margins.top(), margins.right(), margins.bottom()) + .translated(!parent() ? -screen()->geometry().topLeft() : QPoint()); + + m_qtWindow["style"].set("left", std::to_string(frameRect.left()) + "px"); + m_qtWindow["style"].set("top", std::to_string(frameRect.top()) + "px"); + m_canvasContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_canvasContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); + m_a11yContainer["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + m_a11yContainer["style"].set("height", std::to_string(clientAreaRect.height()) + "px"); + + // Important for the title flexbox to shrink correctly + m_windowContents["style"].set("width", std::to_string(clientAreaRect.width()) + "px"); + + QSizeF canvasSize = clientAreaRect.size() * devicePixelRatio(); + + m_canvas.set("width", canvasSize.width()); + m_canvas.set("height", canvasSize.height()); + + bool shouldInvalidate = true; + if (!m_state.testFlag(Qt::WindowFullScreen) && !m_state.testFlag(Qt::WindowMaximized)) { + shouldInvalidate = m_normalGeometry.size() != clientAreaRect.size(); + m_normalGeometry = clientAreaRect; } - if (!m_windowState.testFlag(Qt::WindowFullScreen) && !m_windowState.testFlag(Qt::WindowMaximized)) - m_normalGeometry = r; - QPlatformWindow::setGeometry(r); - QWindowSystemInterface::handleGeometryChange(window(), r); - invalidate(); + QWindowSystemInterface::handleGeometryChange(window(), clientAreaRect); + if (shouldInvalidate) + invalidate(); + else + m_compositor->requestUpdateWindow(this); } void QWasmWindow::setVisible(bool visible) { + // TODO(mikolajboc): isVisible()? + const bool nowVisible = m_qtWindow["style"]["display"].as<std::string>() == "block"; + if (visible == nowVisible) + return; + + m_compositor->requestUpdateWindow(this, QWasmCompositor::ExposeEventDelivery); + m_qtWindow["style"].set("display", visible ? "block" : "none"); + if (window()->isActive()) + m_canvas.call<void>("focus"); if (visible) applyWindowState(); - m_compositor->setVisible(this, visible); } -bool QWasmWindow::isVisible() +bool QWasmWindow::isVisible() const { return window()->isVisible(); } QMargins QWasmWindow::frameMargins() const { - int border = hasTitleBar() ? 4. * (qreal(qt_defaultDpiX()) / 96.0) : 0; - int titleBarHeight = hasTitleBar() ? titleHeight() : 0; - - QMargins margins; - margins.setLeft(border); - margins.setRight(border); - margins.setTop(2*border + titleBarHeight); - margins.setBottom(border); - - return margins; + const auto frameRect = + QRectF::fromDOMRect(m_qtWindow.call<emscripten::val>("getBoundingClientRect")); + const auto canvasRect = + QRectF::fromDOMRect(m_windowContents.call<emscripten::val>("getBoundingClientRect")); + return QMarginsF(canvasRect.left() - frameRect.left(), canvasRect.top() - frameRect.top(), + frameRect.right() - canvasRect.right(), + frameRect.bottom() - canvasRect.bottom()) + .toMargins(); } void QWasmWindow::raise() { - m_compositor->raise(this); - QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), geometry().size())); + bringToTop(); invalidate(); + if (QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); } void QWasmWindow::lower() { - m_compositor->lower(this); - QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), geometry().size())); + sendToBottom(); invalidate(); } WId QWasmWindow::winId() const { - return m_winid; + return m_winId; } void QWasmWindow::propagateSizeHints() { - QRect rect = windowGeometry(); - if (rect.size().width() < windowMinimumSize().width() - && rect.size().height() < windowMinimumSize().height()) { - rect.setSize(windowMinimumSize()); - setGeometry(rect); - } + // setGeometry() will take care of minimum and maximum size constraints + setGeometry(windowGeometry()); + m_nonClientArea->propagateSizeHints(); } -void QWasmWindow::injectMousePressed(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods) +void QWasmWindow::setOpacity(qreal level) { - Q_UNUSED(local); - Q_UNUSED(mods); + m_qtWindow["style"].set("opacity", qBound(0.0, level, 1.0)); +} - if (!hasTitleBar() || button != Qt::LeftButton) - return; +void QWasmWindow::invalidate() +{ + m_compositor->requestUpdateWindow(this); +} - if (maxButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarMaxButton; - else if (minButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarMinButton; - else if (closeButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarCloseButton; - else if (normButtonRect().contains(global)) - m_activeControl = QWasmCompositor::SC_TitleBarNormalButton; +void QWasmWindow::onActivationChanged(bool active) +{ + dom::syncCSSClassWith(m_qtWindow, "inactive", !active); +} - invalidate(); +void QWasmWindow::setWindowFlags(Qt::WindowFlags flags) +{ + if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint) + || flags.testFlag(Qt::WindowStaysOnBottomHint) + != m_flags.testFlag(Qt::WindowStaysOnBottomHint)) { + onPositionPreferenceChanged(positionPreferenceFromWindowFlags(flags)); + } + m_flags = flags; + dom::syncCSSClassWith(m_qtWindow, "frameless", !hasFrame()); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "has-shadow", hasShadow()); + dom::syncCSSClassWith(m_qtWindow, "has-title", hasTitleBar()); + dom::syncCSSClassWith(m_qtWindow, "transparent-for-input", + flags.testFlag(Qt::WindowTransparentForInput)); + + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); + m_nonClientArea->titleBar()->setCloseVisible(m_flags.testFlag(Qt::WindowCloseButtonHint)); } -void QWasmWindow::injectMouseReleased(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods) +void QWasmWindow::setWindowState(Qt::WindowStates newState) { - Q_UNUSED(local); - Q_UNUSED(mods); + // Child windows can not have window states other than Qt::WindowActive + if (parent()) + newState &= Qt::WindowActive; - if (!hasTitleBar() || button != Qt::LeftButton) - return; + const Qt::WindowStates oldState = m_state; - if (closeButtonRect().contains(global) && m_activeControl == QWasmCompositor::SC_TitleBarCloseButton) { - window()->close(); + if (newState.testFlag(Qt::WindowMinimized)) { + newState.setFlag(Qt::WindowMinimized, false); + qWarning("Qt::WindowMinimized is not implemented in wasm"); + window()->setWindowStates(newState); return; } - if (maxButtonRect().contains(global) && m_activeControl == QWasmCompositor::SC_TitleBarMaxButton) { - window()->setWindowState(Qt::WindowMaximized); - } - - if (normButtonRect().contains(global) && m_activeControl == QWasmCompositor::SC_TitleBarNormalButton) { - window()->setWindowState(Qt::WindowNoState); - } + if (newState == oldState) + return; - m_activeControl = QWasmCompositor::SC_None; + m_state = newState; + m_previousWindowState = oldState; - invalidate(); + applyWindowState(); } -int QWasmWindow::titleHeight() const +void QWasmWindow::setWindowTitle(const QString &title) { - return 18. * (qreal(qt_defaultDpiX()) / 96.0);//dpiScaled(18.); + m_nonClientArea->titleBar()->setTitle(title); } -int QWasmWindow::borderWidth() const +void QWasmWindow::setWindowIcon(const QIcon &icon) { - return 4. * (qreal(qt_defaultDpiX()) / 96.0);// dpiScaled(4.); + const auto dpi = screen()->devicePixelRatio(); + auto pixmap = icon.pixmap(10 * dpi, 10 * dpi); + if (pixmap.isNull()) { + m_nonClientArea->titleBar()->setIcon( + Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + return; + } + + QByteArray bytes; + QBuffer buffer(&bytes); + pixmap.save(&buffer, "png"); + m_nonClientArea->titleBar()->setIcon(bytes.toBase64().toStdString(), "png"); } -QRegion QWasmWindow::titleGeometry() const +void QWasmWindow::applyWindowState() { - int border = borderWidth(); + QRect newGeom; + + const bool isFullscreen = m_state.testFlag(Qt::WindowFullScreen); + const bool isMaximized = m_state.testFlag(Qt::WindowMaximized); + if (isFullscreen) + newGeom = platformScreen()->geometry(); + else if (isMaximized) + newGeom = platformScreen()->availableGeometry().marginsRemoved(frameMargins()); + else + newGeom = normalGeometry(); - QRegion result(window()->frameGeometry().x() + border, - window()->frameGeometry().y() + border, - window()->frameGeometry().width() - 2*border, - titleHeight()); + dom::syncCSSClassWith(m_qtWindow, "has-border", hasBorder()); + dom::syncCSSClassWith(m_qtWindow, "maximized", isMaximized); - result -= titleControlRegion(); + m_nonClientArea->titleBar()->setRestoreVisible(isMaximized); + m_nonClientArea->titleBar()->setMaximizeVisible(hasMaximizeButton()); - return result; + if (isVisible()) + QWindowSystemInterface::handleWindowStateChanged(window(), m_state, m_previousWindowState); + setGeometry(newGeom); } -QRegion QWasmWindow::resizeRegion() const +void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) { - int border = borderWidth(); - QRegion result(window()->frameGeometry().adjusted(-border, -border, border, border)); - result -= window()->frameGeometry().adjusted(border, border, -border, -border); - - return result; + onParentChanged(m_commitedParent, parent, positionPreferenceFromWindowFlags(window()->flags())); + m_commitedParent = parent; } -bool QWasmWindow::isPointOnTitle(QPoint point) const +bool QWasmWindow::processKey(const KeyEvent &event) { - bool ok = titleGeometry().contains(point); - return ok; + constexpr bool ProceedToNativeEvent = false; + Q_ASSERT(event.type == EventType::KeyDown || event.type == EventType::KeyUp); + + const auto clipboardResult = + QWasmIntegration::get()->getWasmClipboard()->processKeyboard(event); + + using ProcessKeyboardResult = QWasmClipboard::ProcessKeyboardResult; + if (clipboardResult == ProcessKeyboardResult::NativeClipboardEventNeeded) + return ProceedToNativeEvent; + + const auto result = QWindowSystemInterface::handleKeyEvent( + 0, event.type == EventType::KeyDown ? QEvent::KeyPress : QEvent::KeyRelease, event.key, + event.modifiers, event.text, event.autoRepeat); + return clipboardResult == ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded + ? ProceedToNativeEvent + : result; } -bool QWasmWindow::isPointOnResizeRegion(QPoint point) const +bool QWasmWindow::processPointer(const PointerEvent &event) { - if (window()->flags().testFlag(Qt::Popup)) + if (event.pointerType != PointerType::Mouse && event.pointerType != PointerType::Pen) return false; - return resizeRegion().contains(point); -} - -QWasmCompositor::ResizeMode QWasmWindow::resizeModeAtPoint(QPoint point) const -{ - QPoint p1 = window()->frameGeometry().topLeft() - QPoint(5, 5); - QPoint p2 = window()->frameGeometry().bottomRight() + QPoint(5, 5); - int corner = 20; - - QRect top(p1, QPoint(p2.x(), p1.y() + corner)); - QRect middle(QPoint(p1.x(), p1.y() + corner), QPoint(p2.x(), p2.y() - corner)); - QRect bottom(QPoint(p1.x(), p2.y() - corner), p2); - - QRect left(p1, QPoint(p1.x() + corner, p2.y())); - QRect center(QPoint(p1.x() + corner, p1.y()), QPoint(p2.x() - corner, p2.y())); - QRect right(QPoint(p2.x() - corner, p1.y()), p2); - - if (top.contains(point)) { - // Top - if (left.contains(point)) - return QWasmCompositor::ResizeTopLeft; - if (center.contains(point)) - return QWasmCompositor::ResizeTop; - if (right.contains(point)) - return QWasmCompositor::ResizeTopRight; - } else if (middle.contains(point)) { - // Middle - if (left.contains(point)) - return QWasmCompositor::ResizeLeft; - if (right.contains(point)) - return QWasmCompositor::ResizeRight; - } else if (bottom.contains(point)) { - // Bottom - if (left.contains(point)) - return QWasmCompositor::ResizeBottomLeft; - if (center.contains(point)) - return QWasmCompositor::ResizeBottom; - if (right.contains(point)) - return QWasmCompositor::ResizeBottomRight; + + switch (event.type) { + case EventType::PointerEnter: { + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); + QWindowSystemInterface::handleEnterEvent( + window(), m_window->mapFromGlobal(pointInScreen), pointInScreen); + break; + } + case EventType::PointerLeave: + QWindowSystemInterface::handleLeaveEvent(window()); + break; + default: + break; } - return QWasmCompositor::ResizeNone; + return false; } -QRect getSubControlRect(const QWasmWindow *window, QWasmCompositor::SubControls subControl) +bool QWasmWindow::processWheel(const WheelEvent &event) { - QWasmCompositor::QWasmTitleBarOptions options = QWasmCompositor::makeTitleBarOptions(window); + // Web scroll deltas are inverted from Qt deltas - negate. + const int scrollFactor = -([&event]() { + switch (event.deltaMode) { + case DeltaMode::Pixel: + return 1; + case DeltaMode::Line: + return 12; + case DeltaMode::Page: + return 20; + }; + })(); - QRect r = QWasmCompositor::titlebarRect(options, subControl); - r.translate(window->window()->frameGeometry().x(), window->window()->frameGeometry().y()); + const auto pointInScreen = platformScreen()->mapFromLocal( + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); - return r; + return QWindowSystemInterface::handleWheelEvent( + window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), + pointInScreen, (event.delta * scrollFactor).toPoint(), + (event.delta * scrollFactor).toPoint(), event.modifiers, Qt::NoScrollPhase, + Qt::MouseEventNotSynthesized, event.webkitDirectionInvertedFromDevice); } -QRect QWasmWindow::maxButtonRect() const +QRect QWasmWindow::normalGeometry() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarMaxButton); + return m_normalGeometry; } -QRect QWasmWindow::minButtonRect() const +qreal QWasmWindow::devicePixelRatio() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarMinButton); + return screen()->devicePixelRatio(); } -QRect QWasmWindow::closeButtonRect() const +void QWasmWindow::requestUpdate() { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarCloseButton); + m_compositor->requestUpdateWindow(this, QWasmCompositor::UpdateRequestDelivery); } -QRect QWasmWindow::normButtonRect() const +bool QWasmWindow::hasFrame() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarNormalButton); + return !m_flags.testFlag(Qt::FramelessWindowHint); } -QRect QWasmWindow::sysMenuRect() const +bool QWasmWindow::hasBorder() const { - return getSubControlRect(this, QWasmCompositor::SC_TitleBarSysMenu); + return hasFrame() && !m_state.testFlag(Qt::WindowFullScreen) && !m_flags.testFlag(Qt::SubWindow) + && !windowIsPopupType(m_flags) && !parent(); } -QRegion QWasmWindow::titleControlRegion() const +bool QWasmWindow::hasTitleBar() const { - QRegion result; - result += closeButtonRect(); - result += minButtonRect(); - result += maxButtonRect(); - result += sysMenuRect(); - - return result; + return hasBorder() && m_flags.testFlag(Qt::WindowTitleHint); } -void QWasmWindow::invalidate() +bool QWasmWindow::hasShadow() const { - m_compositor->requestUpdateWindow(this); + return hasBorder() && !m_flags.testFlag(Qt::NoDropShadowWindowHint); } -QWasmCompositor::SubControls QWasmWindow::activeSubControl() const +bool QWasmWindow::hasMaximizeButton() const { - return m_activeControl; + return !m_state.testFlag(Qt::WindowMaximized) && m_flags.testFlag(Qt::WindowMaximizeButtonHint); } -void QWasmWindow::setWindowState(Qt::WindowStates newState) +bool QWasmWindow::windowIsPopupType(Qt::WindowFlags flags) const { - const Qt::WindowStates oldState = m_windowState; - bool isActive = oldState.testFlag(Qt::WindowActive); + if (flags.testFlag(Qt::Tool)) + return false; // Qt::Tool has the Popup bit set but isn't an actual Popup window - if (newState.testFlag(Qt::WindowMinimized)) { - newState.setFlag(Qt::WindowMinimized, false); - qWarning("Qt::WindowMinimized is not implemented in wasm"); - } + return (flags.testFlag(Qt::Popup)); +} - // Always keep OpenGL apps fullscreen - if (!m_needsCompositor && !newState.testFlag(Qt::WindowFullScreen)) { - newState.setFlag(Qt::WindowFullScreen, true); - qWarning("Qt::WindowFullScreen must be set for OpenGL surfaces"); +void QWasmWindow::requestActivateWindow() +{ + QWindow *modalWindow; + if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) { + static_cast<QWasmWindow *>(modalWindow->handle())->requestActivateWindow(); + return; } - // Ignore WindowActive flag in comparison, as we want to preserve it either way - if ((newState & ~Qt::WindowActive) == (oldState & ~Qt::WindowActive)) - return; + raise(); + setAsActiveNode(); - newState.setFlag(Qt::WindowActive, isActive); + if (!QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); - m_previousWindowState = oldState; - m_windowState = newState; + QPlatformWindow::requestActivateWindow(); +} - if (isVisible()) { - applyWindowState(); - } +bool QWasmWindow::setMouseGrabEnabled(bool grab) +{ + Q_UNUSED(grab); + return false; } -void QWasmWindow::applyWindowState() +bool QWasmWindow::windowEvent(QEvent *event) { - QRect newGeom; + switch (event->type()) { + case QEvent::WindowBlocked: + m_qtWindow["classList"].call<void>("add", emscripten::val("blocked")); + return false; // Propagate further + case QEvent::WindowUnblocked:; + m_qtWindow["classList"].call<void>("remove", emscripten::val("blocked")); + return false; // Propagate further + default: + return QPlatformWindow::windowEvent(event); + } +} - if (m_windowState.testFlag(Qt::WindowFullScreen)) - newGeom = platformScreen()->geometry(); - else if (m_windowState.testFlag(Qt::WindowMaximized)) - newGeom = platformScreen()->availableGeometry(); - else - newGeom = normalGeometry(); +void QWasmWindow::setMask(const QRegion ®ion) +{ + if (region.isEmpty()) { + m_qtWindow["style"].set("clipPath", emscripten::val("")); + return; + } - QWindowSystemInterface::handleWindowStateChanged(window(), m_windowState, m_previousWindowState); - setGeometry(newGeom); + std::ostringstream cssClipPath; + cssClipPath << "path('"; + for (const auto &rect : region) { + const auto cssRect = rect.adjusted(0, 0, 1, 1); + cssClipPath << "M " << cssRect.left() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.top() << " "; + cssClipPath << "L " << cssRect.right() << " " << cssRect.bottom() << " "; + cssClipPath << "L " << cssRect.left() << " " << cssRect.bottom() << " z "; + } + cssClipPath << "')"; + m_qtWindow["style"].set("clipPath", emscripten::val(cssClipPath.str())); } -QRect QWasmWindow::normalGeometry() const +void QWasmWindow::setParent(const QPlatformWindow *) { - return m_normalGeometry; + commitParent(parentNode()); } -qreal QWasmWindow::devicePixelRatio() const +std::string QWasmWindow::canvasSelector() const { - return screen()->devicePixelRatio(); + return "!qtwindow" + std::to_string(m_winId); } -void QWasmWindow::requestUpdate() +emscripten::val QWasmWindow::containerElement() { - if (m_compositor) { - m_compositor->requestUpdateWindow(this, QWasmCompositor::UpdateRequestDelivery); - return; - } - - static auto frame = [](double time, void *context) -> int { - Q_UNUSED(time); - QWasmWindow *window = static_cast<QWasmWindow *>(context); - window->m_requestAnimationFrameId = -1; - window->deliverUpdateRequest(); - return 0; - }; - m_requestAnimationFrameId = emscripten_request_animation_frame(frame, this); + return m_windowContents; } -bool QWasmWindow::hasTitleBar() const +QWasmWindowTreeNode *QWasmWindow::parentNode() { - Qt::WindowFlags flags = window()->flags(); - return !(m_windowState & Qt::WindowFullScreen) - && flags.testFlag(Qt::WindowTitleHint) - && !(windowIsPopupType(flags)) - && m_needsCompositor; + if (parent()) + return static_cast<QWasmWindow *>(parent()); + return platformScreen(); } -bool QWasmWindow::windowIsPopupType(Qt::WindowFlags flags) const +QWasmWindow *QWasmWindow::asWasmWindow() { - if (flags.testFlag(Qt::Tool)) - return false; // Qt::Tool has the Popup bit set but isn't - - return (flags.testFlag(Qt::Popup)); + return this; } -void QWasmWindow::requestActivateWindow() +void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) { - if (window()->isTopLevel()) - raise(); - QPlatformWindow::requestActivateWindow(); + if (previous) + previous->containerElement().call<void>("removeChild", m_qtWindow); + if (current) + current->containerElement().call<void>("appendChild", m_qtWindow); + QWasmWindowTreeNode::onParentChanged(previous, current, positionPreference); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindow.h b/src/plugins/platforms/wasm/qwasmwindow.h index 6503c4a5c6..ab0dc68e83 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -6,83 +6,163 @@ #include "qwasmintegration.h" #include <qpa/qplatformwindow.h> +#include <qpa/qplatformwindow_p.h> #include <emscripten/html5.h> #include "qwasmbackingstore.h" #include "qwasmscreen.h" #include "qwasmcompositor.h" +#include "qwasmwindownonclientarea.h" +#include "qwasmwindowstack.h" +#include "qwasmwindowtreenode.h" + +#include <QtCore/private/qstdweb_p.h> +#include "QtGui/qopenglcontext.h" +#include <QtOpenGL/qopengltextureblitter.h> + +#include <emscripten/val.h> + +#include <memory> QT_BEGIN_NAMESPACE -class QWasmWindow : public QPlatformWindow +namespace qstdweb { +class EventCallback; +} + +class ClientArea; +struct KeyEvent; +struct PointerEvent; +class QWasmDeadKeySupport; +struct WheelEvent; + +class QWasmWindow final : public QPlatformWindow, + public QWasmWindowTreeNode, + public QNativeInterface::Private::QWasmWindow { public: - QWasmWindow(QWindow *w, QWasmCompositor *compositor, QWasmBackingStore *backingStore); - ~QWasmWindow(); - void destroy(); - + QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmCompositor *compositor, + QWasmBackingStore *backingStore); + ~QWasmWindow() final; + + static QWasmWindow *fromWindow(QWindow *window); + QSurfaceFormat format() const override; + + void paint(); + void setZOrder(int order); + void setWindowCursor(QByteArray cssCursorName); + void onActivationChanged(bool active); + bool isVisible() const; + + void onNonClientAreaInteraction(); + void onRestoreClicked(); + void onMaximizeClicked(); + void onToggleMaximized(); + void onCloseClicked(); + bool onNonClientEvent(const PointerEvent &event); + + // QPlatformWindow: void initialize() override; - void setGeometry(const QRect &) override; void setVisible(bool visible) override; - bool isVisible(); QMargins frameMargins() const override; - WId winId() const override; - void propagateSizeHints() override; + void setOpacity(qreal level) override; void raise() override; void lower() override; QRect normalGeometry() const override; qreal devicePixelRatio() const override; void requestUpdate() override; void requestActivateWindow() override; + void setWindowFlags(Qt::WindowFlags flags) override; + void setWindowState(Qt::WindowStates state) override; + void setWindowTitle(const QString &title) override; + void setWindowIcon(const QIcon &icon) override; + bool setKeyboardGrabEnabled(bool) override { return false; } + bool setMouseGrabEnabled(bool grab) final; + bool windowEvent(QEvent *event) final; + void setMask(const QRegion ®ion) final; + void setParent(const QPlatformWindow *window) final; QWasmScreen *platformScreen() const; void setBackingStore(QWasmBackingStore *store) { m_backingStore = store; } QWasmBackingStore *backingStore() const { return m_backingStore; } QWindow *window() const { return m_window; } - void injectMousePressed(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods); - void injectMouseReleased(const QPoint &local, const QPoint &global, - Qt::MouseButton button, Qt::KeyboardModifiers mods); - - int titleHeight() const; - int borderWidth() const; - QRegion titleGeometry() const; - QRegion resizeRegion() const; - bool isPointOnTitle(QPoint point) const; - bool isPointOnResizeRegion(QPoint point) const; - QWasmCompositor::ResizeMode resizeModeAtPoint(QPoint point) const; - QRect maxButtonRect() const; - QRect minButtonRect() const; - QRect closeButtonRect() const; - QRect sysMenuRect() const; - QRect normButtonRect() const; - QRegion titleControlRegion() const; - QWasmCompositor::SubControls activeSubControl() const; + std::string canvasSelector() const; - void setWindowState(Qt::WindowStates state) override; - void applyWindowState(); - bool setKeyboardGrabEnabled(bool) override { return false; } - bool setMouseGrabEnabled(bool) override { return false; } + emscripten::val context2d() const { return m_context2d; } + emscripten::val a11yContainer() const { return m_a11yContainer; } + emscripten::val inputHandlerElement() const { return m_windowContents; } + + // QNativeInterface::Private::QWasmWindow + emscripten::val document() const override { return m_document; } + emscripten::val clientArea() const override { return m_qtWindow; } + + // QWasmWindowTreeNode: + emscripten::val containerElement() final; + QWasmWindowTreeNode *parentNode() final; + +private: + friend class QWasmScreen; + static constexpr auto defaultWindowSize = 160; + + // QWasmWindowTreeNode: + QWasmWindow *asWasmWindow() final; + void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) final; -protected: void invalidate(); + bool hasFrame() const; bool hasTitleBar() const; + bool hasBorder() const; + bool hasShadow() const; + bool hasMaximizeButton() const; + void applyWindowState(); + void commitParent(QWasmWindowTreeNode *parent); -protected: - friend class QWasmScreen; + bool processKey(const KeyEvent &event); + bool processPointer(const PointerEvent &event); + bool processWheel(const WheelEvent &event); - QWindow* m_window = nullptr; + QWindow *m_window = nullptr; QWasmCompositor *m_compositor = nullptr; QWasmBackingStore *m_backingStore = nullptr; + QWasmDeadKeySupport *m_deadKeySupport; QRect m_normalGeometry {0, 0, 0 ,0}; - Qt::WindowStates m_windowState = Qt::WindowNoState; + emscripten::val m_document; + emscripten::val m_qtWindow; + emscripten::val m_windowContents; + emscripten::val m_canvasContainer; + emscripten::val m_a11yContainer; + emscripten::val m_canvas; + emscripten::val m_context2d = emscripten::val::undefined(); + + std::unique_ptr<NonClientArea> m_nonClientArea; + std::unique_ptr<ClientArea> m_clientArea; + + QWasmWindowTreeNode *m_commitedParent = nullptr; + + std::unique_ptr<qstdweb::EventCallback> m_keyDownCallback; + std::unique_ptr<qstdweb::EventCallback> m_keyUpCallback; + + std::unique_ptr<qstdweb::EventCallback> m_pointerLeaveCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerEnterCallback; + + std::unique_ptr<qstdweb::EventCallback> m_dropCallback; + + std::unique_ptr<qstdweb::EventCallback> m_wheelEventCallback; + + Qt::WindowStates m_state = Qt::WindowNoState; Qt::WindowStates m_previousWindowState = Qt::WindowNoState; - QWasmCompositor::SubControls m_activeControl = QWasmCompositor::SC_None; - WId m_winid = 0; + + Qt::WindowFlags m_flags = Qt::Widget; + + QPoint m_lastPointerMovePoint; + + WId m_winId = 0; + bool m_wantCapture = false; bool m_hasTitle = false; bool m_needsCompositor = false; long m_requestAnimationFrameId = -1; @@ -90,5 +170,6 @@ protected: friend class QWasmEventTranslator; bool windowIsPopupType(Qt::WindowFlags flags) const; }; + QT_END_NAMESPACE #endif // QWASMWINDOW_H diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp new file mode 100644 index 0000000000..6da3e24c05 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp @@ -0,0 +1,195 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowclientarea.h" + +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmscreen.h" +#include "qwasmwindow.h" +#include "qwasmdrag.h" + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpointingdevice.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +ClientArea::ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val element) + : m_screen(screen), m_window(window), m_element(element) +{ + const auto callback = std::function([this](emscripten::val event) { + processPointer(*PointerEvent::fromWeb(event)); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + + m_pointerDownCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointerdown", callback); + m_pointerMoveCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointermove", callback); + m_pointerUpCallback = std::make_unique<qstdweb::EventCallback>(element, "pointerup", callback); + m_pointerCancelCallback = + std::make_unique<qstdweb::EventCallback>(element, "pointercancel", callback); + + element.call<void>("setAttribute", emscripten::val("draggable"), emscripten::val("true")); + + m_dragStartCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragstart", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragStarted(&event); + }); + m_dragOverCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragover", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragOver(&event); + }); + m_dropCallback = std::make_unique<qstdweb::EventCallback>( + element, "drop", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDrop(&event); + }); + m_dragEndCallback = std::make_unique<qstdweb::EventCallback>( + element, "dragend", [this](emscripten::val webEvent) { + webEvent.call<void>("preventDefault"); + auto event = *DragEvent::fromWeb(webEvent, m_window->window()); + QWasmDrag::instance()->onNativeDragFinished(&event); + }); + +} + +bool ClientArea::processPointer(const PointerEvent &event) +{ + + switch (event.type) { + case EventType::PointerDown: + m_element.call<void>("setPointerCapture", event.pointerId); + if ((m_window->window()->flags() & Qt::WindowDoesNotAcceptFocus) + != Qt::WindowDoesNotAcceptFocus + && m_window->window()->isTopLevel()) + m_window->window()->requestActivate(); + break; + case EventType::PointerUp: + m_element.call<void>("releasePointerCapture", event.pointerId); + break; + default: + break; + }; + + const bool eventAccepted = deliverEvent(event); + if (!eventAccepted && event.type == EventType::PointerDown) + QGuiApplicationPrivate::instance()->closeAllPopups(); + return eventAccepted; +} + +bool ClientArea::deliverEvent(const PointerEvent &event) +{ + const auto pointInScreen = m_screen->mapFromLocal( + dom::mapPoint(event.target(), m_screen->element(), event.localPoint)); + + const auto geometryF = m_screen->geometry().toRectF(); + const QPointF targetPointClippedToScreen( + qBound(geometryF.left(), pointInScreen.x(), geometryF.right()), + qBound(geometryF.top(), pointInScreen.y(), geometryF.bottom())); + + if (event.pointerType == PointerType::Mouse) { + const QEvent::Type eventType = + MouseEvent::mouseEventTypeFromEventType(event.type, WindowArea::Client); + + return eventType != QEvent::None + && QWindowSystemInterface::handleMouseEvent( + m_window->window(), QWasmIntegration::getTimestamp(), + m_window->window()->mapFromGlobal(targetPointClippedToScreen), + targetPointClippedToScreen, event.mouseButtons, event.mouseButton, + eventType, event.modifiers); + } + + if (event.pointerType == PointerType::Pen) { + qreal pressure; + switch (event.type) { + case EventType::PointerDown : + case EventType::PointerMove : + pressure = event.pressure; + break; + case EventType::PointerUp : + pressure = 0.0; + break; + default: + return false; + } + // Tilt in the browser is in the range +-90, but QTabletEvent only goes to +-60. + qreal xTilt = qBound(-60.0, event.tiltX, 60.0); + qreal yTilt = qBound(-60.0, event.tiltY, 60.0); + // Barrel rotation is reported as 0 to 359, but QTabletEvent wants a signed value. + qreal rotation = event.twist > 180.0 ? 360.0 - event.twist : event.twist; + return QWindowSystemInterface::handleTabletEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->tabletDevice(), + m_window->window()->mapFromGlobal(targetPointClippedToScreen), + targetPointClippedToScreen, event.mouseButtons, pressure, xTilt, yTilt, + event.tangentialPressure, rotation, event.modifiers); + } + + QWindowSystemInterface::TouchPoint *touchPoint; + + QPointF pointInTargetWindowCoords = + QPointF(m_window->window()->mapFromGlobal(targetPointClippedToScreen)); + QPointF normalPosition(pointInTargetWindowCoords.x() / m_window->window()->width(), + pointInTargetWindowCoords.y() / m_window->window()->height()); + + const auto tp = m_pointerIdToTouchPoints.find(event.pointerId); + if (event.pointerType != PointerType::Pen && tp != m_pointerIdToTouchPoints.end()) { + touchPoint = &tp.value(); + } else { + touchPoint = &m_pointerIdToTouchPoints + .insert(event.pointerId, QWindowSystemInterface::TouchPoint()) + .value(); + + // Assign touch point id. TouchPoint::id is int, but QGuiApplicationPrivate::processTouchEvent() + // will not synthesize mouse events for touch points with negative id; use the absolute value for + // the touch point id. + touchPoint->id = qAbs(event.pointerId); + + touchPoint->state = QEventPoint::State::Pressed; + } + + const bool stationaryTouchPoint = (normalPosition == touchPoint->normalPosition); + touchPoint->normalPosition = normalPosition; + touchPoint->area = QRectF(targetPointClippedToScreen, QSizeF(event.width, event.height)) + .translated(-event.width / 2, -event.height / 2); + touchPoint->pressure = event.pressure; + + switch (event.type) { + case EventType::PointerUp: + touchPoint->state = QEventPoint::State::Released; + break; + case EventType::PointerMove: + touchPoint->state = (stationaryTouchPoint ? QEventPoint::State::Stationary + : QEventPoint::State::Updated); + break; + default: + break; + } + + QList<QWindowSystemInterface::TouchPoint> touchPointList; + touchPointList.reserve(m_pointerIdToTouchPoints.size()); + std::transform(m_pointerIdToTouchPoints.begin(), m_pointerIdToTouchPoints.end(), + std::back_inserter(touchPointList), + [](const QWindowSystemInterface::TouchPoint &val) { return val; }); + + if (event.type == EventType::PointerUp) + m_pointerIdToTouchPoints.remove(event.pointerId); + + return event.type == EventType::PointerCancel + ? QWindowSystemInterface::handleTouchCancelEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->touchDevice(), + event.modifiers) + : QWindowSystemInterface::handleTouchEvent( + m_window->window(), QWasmIntegration::getTimestamp(), m_screen->touchDevice(), + touchPointList, event.modifiers); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.h b/src/plugins/platforms/wasm/qwasmwindowclientarea.h new file mode 100644 index 0000000000..ba745a59a8 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWCLIENTAREA_H +#define QWASMWINDOWCLIENTAREA_H + +#include <QtCore/qnamespace.h> +#include <qpa/qwindowsysteminterface.h> +#include <QtCore/QMap> + +#include <emscripten/val.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +namespace qstdweb { +class EventCallback; +} + +struct PointerEvent; +class QWasmScreen; +class QWasmWindow; + +class ClientArea +{ +public: + ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val element); + +private: + bool processPointer(const PointerEvent &event); + bool deliverEvent(const PointerEvent &event); + + std::unique_ptr<qstdweb::EventCallback> m_pointerDownCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerMoveCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerUpCallback; + std::unique_ptr<qstdweb::EventCallback> m_pointerCancelCallback; + + std::unique_ptr<qstdweb::EventCallback> m_dragOverCallback; + std::unique_ptr<qstdweb::EventCallback> m_dragStartCallback; + std::unique_ptr<qstdweb::EventCallback> m_dragEndCallback; + std::unique_ptr<qstdweb::EventCallback> m_dropCallback; + + QMap<int, QWindowSystemInterface::TouchPoint> m_pointerIdToTouchPoints; + + QWasmScreen *m_screen; + QWasmWindow *m_window; + emscripten::val m_element; +}; + +QT_END_NAMESPACE +#endif // QWASMWINDOWNONCLIENTAREA_H diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp new file mode 100644 index 0000000000..00fa8fb236 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp @@ -0,0 +1,460 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindownonclientarea.h" + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/qassert.h> + +QT_BEGIN_NAMESPACE + +WebImageButton::Callbacks::Callbacks() = default; +WebImageButton::Callbacks::Callbacks(std::function<void()> onInteraction, + std::function<void()> onClick) + : m_onInteraction(std::move(onInteraction)), m_onClick(std::move(onClick)) +{ + Q_ASSERT_X(!!m_onInteraction == !!m_onClick, Q_FUNC_INFO, + "Both callbacks need to be either null or non-null"); +} +WebImageButton::Callbacks::~Callbacks() = default; + +WebImageButton::Callbacks::Callbacks(Callbacks &&) = default; +WebImageButton::Callbacks &WebImageButton::Callbacks::operator=(Callbacks &&) = default; + +void WebImageButton::Callbacks::onInteraction() +{ + return m_onInteraction(); +} + +void WebImageButton::Callbacks::onClick() +{ + return m_onClick(); +} + +WebImageButton::WebImageButton() + : m_containerElement( + dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_imgElement(dom::document().call<emscripten::val>("createElement", emscripten::val("img"))) +{ + m_imgElement.set("draggable", false); + + m_containerElement["classList"].call<void>("add", emscripten::val("image-button")); + m_containerElement.call<void>("appendChild", m_imgElement); +} + +WebImageButton::~WebImageButton() = default; + +void WebImageButton::setCallbacks(Callbacks callbacks) +{ + if (callbacks) { + if (!m_webClickEventCallback) { + m_webMouseDownEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "pointerdown", [this](emscripten::val event) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + m_callbacks.onInteraction(); + }); + m_webClickEventCallback = std::make_unique<qstdweb::EventCallback>( + m_containerElement, "click", [this](emscripten::val event) { + m_callbacks.onClick(); + event.call<void>("stopPropagation"); + }); + } + } else { + m_webMouseDownEventCallback.reset(); + m_webClickEventCallback.reset(); + } + dom::syncCSSClassWith(m_containerElement, "action-button", !!callbacks); + m_callbacks = std::move(callbacks); +} + +void WebImageButton::setImage(std::string_view imageData, std::string_view format) +{ + m_imgElement.set("src", + "data:image/" + std::string(format) + ";base64," + std::string(imageData)); +} + +void WebImageButton::setVisible(bool visible) +{ + m_containerElement["style"].set("display", visible ? "flex" : "none"); +} + +Resizer::ResizerElement::ResizerElement(emscripten::val parentElement, Qt::Edges edges, + Resizer *resizer) + : m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_edges(edges), + m_resizer(resizer) +{ + Q_ASSERT_X(m_resizer, Q_FUNC_INFO, "Resizer cannot be null"); + + m_element["classList"].call<void>("add", emscripten::val("resize-outline")); + m_element["classList"].call<void>("add", emscripten::val(cssClassNameForEdges(edges))); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_resizer->onInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) + event.call<void>("preventDefault"); + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +Resizer::ResizerElement::~ResizerElement() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +Resizer::ResizerElement::ResizerElement(ResizerElement &&other) = default; + +bool Resizer::ResizerElement::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_resizer->startResize(m_edges, event); + return true; +} + +bool Resizer::ResizerElement::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->continueResize(event); + return true; +} + +bool Resizer::ResizerElement::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_resizer->finishResize(); + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + return true; +} + +Resizer::Resizer(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), m_windowElement(parentElement) +{ + Q_ASSERT_X(m_window, Q_FUNC_INFO, "Window must not be null"); + + constexpr std::array<int, 8> ResizeEdges = { Qt::TopEdge | Qt::LeftEdge, + Qt::TopEdge, + Qt::TopEdge | Qt::RightEdge, + Qt::LeftEdge, + Qt::RightEdge, + Qt::BottomEdge | Qt::LeftEdge, + Qt::BottomEdge, + Qt::BottomEdge | Qt::RightEdge }; + std::transform(std::begin(ResizeEdges), std::end(ResizeEdges), std::back_inserter(m_elements), + [parentElement, this](int edges) { + return std::make_unique<ResizerElement>(parentElement, + Qt::Edges::fromInt(edges), this); + }); +} + +Resizer::~Resizer() = default; + +ResizeConstraints Resizer::getResizeConstraints() { + const auto *window = m_window->window(); + const auto minShrink = QPoint(window->minimumWidth() - window->geometry().width(), + window->minimumHeight() - window->geometry().height()); + const auto maxGrow = QPoint(window->maximumWidth() - window->geometry().width(), + window->maximumHeight() - window->geometry().height()); + + const auto frameRect = + QRectF::fromDOMRect(m_windowElement.call<emscripten::val>("getBoundingClientRect")); + auto containerGeometry = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + + const int maxGrowTop = frameRect.top() - containerGeometry.top(); + + return ResizeConstraints{minShrink, maxGrow, maxGrowTop}; +} + +void Resizer::onInteraction() +{ + m_window->onNonClientAreaInteraction(); +} + +void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) +{ + Q_ASSERT_X(!m_currentResizeData, Q_FUNC_INFO, "Another resize in progress"); + + m_currentResizeData.reset(new ResizeData{ + .edges = resizeEdges, + .originInScreenCoords = dom::mapPoint( + event.target(), m_window->platformScreen()->element(), event.localPoint), + }); + + const auto resizeConstraints = getResizeConstraints(); + m_currentResizeData->minShrink = resizeConstraints.minShrink; + + m_currentResizeData->maxGrow = + QPoint(resizeConstraints.maxGrow.x(), + std::min(resizeEdges & Qt::Edge::TopEdge ? resizeConstraints.maxGrowTop : INT_MAX, + resizeConstraints.maxGrow.y())); + + m_currentResizeData->initialBounds = m_window->window()->geometry(); +} + +void Resizer::continueResize(const PointerEvent &event) +{ + const auto pointInScreen = + dom::mapPoint(event.target(), m_window->platformScreen()->element(), event.localPoint); + const auto amount = (pointInScreen - m_currentResizeData->originInScreenCoords).toPoint(); + const QPoint cappedGrowVector( + std::min(m_currentResizeData->maxGrow.x(), + std::max(m_currentResizeData->minShrink.x(), + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -amount.x() + : (m_currentResizeData->edges & Qt::Edge::RightEdge) + ? amount.x() + : 0)), + std::min(m_currentResizeData->maxGrow.y(), + std::max(m_currentResizeData->minShrink.y(), + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -amount.y() + : (m_currentResizeData->edges & Qt::Edge::BottomEdge) + ? amount.y() + : 0))); + + auto bounds = m_currentResizeData->initialBounds.adjusted( + (m_currentResizeData->edges & Qt::Edge::LeftEdge) ? -cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::TopEdge) ? -cappedGrowVector.y() : 0, + (m_currentResizeData->edges & Qt::Edge::RightEdge) ? cappedGrowVector.x() : 0, + (m_currentResizeData->edges & Qt::Edge::BottomEdge) ? cappedGrowVector.y() : 0); + + m_window->window()->setGeometry(bounds); +} + +void Resizer::finishResize() +{ + Q_ASSERT_X(m_currentResizeData, Q_FUNC_INFO, "No resize in progress"); + m_currentResizeData.reset(); +} + +TitleBar::TitleBar(QWasmWindow *window, emscripten::val parentElement) + : m_window(window), + m_element(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))), + m_label(dom::document().call<emscripten::val>("createElement", emscripten::val("div"))) +{ + m_icon = std::make_unique<WebImageButton>(); + m_icon->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::QtLogo), "svg+xml"); + m_element.call<void>("appendChild", m_icon->htmlElement()); + m_element.set("className", "title-bar"); + + auto spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer["style"].set("width", "4px"); + m_element.call<void>("appendChild", spacer); + + m_label.set("className", "window-name"); + + m_element.call<void>("appendChild", m_label); + + spacer = dom::document().call<emscripten::val>("createElement", emscripten::val("div")); + spacer.set("className", "spacer"); + m_element.call<void>("appendChild", spacer); + + m_restore = std::make_unique<WebImageButton>(); + m_restore->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Restore), + "svg+xml"); + m_restore->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onRestoreClicked(); })); + + m_element.call<void>("appendChild", m_restore->htmlElement()); + + m_maximize = std::make_unique<WebImageButton>(); + m_maximize->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::Maximize), + "svg+xml"); + m_maximize->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onMaximizeClicked(); })); + + m_element.call<void>("appendChild", m_maximize->htmlElement()); + + m_close = std::make_unique<WebImageButton>(); + m_close->setImage(Base64IconStore::get()->getIcon(Base64IconStore::IconType::X), "svg+xml"); + m_close->setCallbacks( + WebImageButton::Callbacks([this]() { m_window->onNonClientAreaInteraction(); }, + [this]() { m_window->onCloseClicked(); })); + + m_element.call<void>("appendChild", m_close->htmlElement()); + + parentElement.call<void>("appendChild", m_element); + + m_mouseDownEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerdown", [this](emscripten::val event) { + if (!onPointerDown(*PointerEvent::fromWeb(event))) + return; + m_window->onNonClientAreaInteraction(); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + }); + m_mouseMoveEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointermove", [this](emscripten::val event) { + if (onPointerMove(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + } + }); + m_mouseUpEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "pointerup", [this](emscripten::val event) { + if (onPointerUp(*PointerEvent::fromWeb(event))) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); + m_doubleClickEvent = std::make_unique<qstdweb::EventCallback>( + m_element, "dblclick", [this](emscripten::val event) { + if (onDoubleClick()) { + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); + } + }); +} + +TitleBar::~TitleBar() +{ + m_element["parentElement"].call<emscripten::val>("removeChild", m_element); +} + +void TitleBar::setTitle(const QString &title) +{ + m_label.set("innerText", emscripten::val(title.toStdString())); +} + +void TitleBar::setRestoreVisible(bool visible) +{ + m_restore->setVisible(visible); +} + +void TitleBar::setMaximizeVisible(bool visible) +{ + m_maximize->setVisible(visible); +} + +void TitleBar::setCloseVisible(bool visible) +{ + m_close->setVisible(visible); +} + +void TitleBar::setIcon(std::string_view imageData, std::string_view format) +{ + m_icon->setImage(imageData, format); +} + +void TitleBar::setWidth(int width) +{ + m_element["style"].set("width", std::to_string(width) + "px"); +} + +QRectF TitleBar::geometry() const +{ + return QRectF::fromDOMRect(m_element.call<emscripten::val>("getBoundingClientRect")); +} + +bool TitleBar::onPointerDown(const PointerEvent &event) +{ + m_element.call<void>("setPointerCapture", event.pointerId); + m_capturedPointerId = event.pointerId; + + m_moveStartWindowPosition = m_window->window()->position(); + m_moveStartPoint = clipPointWithScreen(event.localPoint); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerMove(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + const QPoint delta = (clipPointWithScreen(event.localPoint) - m_moveStartPoint).toPoint(); + + m_window->window()->setPosition(m_moveStartWindowPosition + delta); + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onPointerUp(const PointerEvent &event) +{ + if (m_capturedPointerId != event.pointerId) + return false; + + m_element.call<void>("releasePointerCapture", event.pointerId); + m_capturedPointerId = -1; + m_window->onNonClientEvent(event); + return true; +} + +bool TitleBar::onDoubleClick() +{ + m_window->onToggleMaximized(); + return true; +} + +QPointF TitleBar::clipPointWithScreen(const QPointF &pointInTitleBarCoords) const +{ + auto containerRect = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + const auto p = dom::mapPoint(m_element, m_window->parentNode()->containerElement(), + pointInTitleBarCoords); + + auto result = QPointF(qBound(0., qreal(p.x()), containerRect.width()), + qBound(0., qreal(p.y()), containerRect.height())); + return m_window->parent() ? result : m_window->platformScreen()->mapFromLocal(result).toPoint(); +} + +NonClientArea::NonClientArea(QWasmWindow *window, emscripten::val qtWindowElement) + : m_qtWindowElement(qtWindowElement), + m_resizer(std::make_unique<Resizer>(window, m_qtWindowElement)), + m_titleBar(std::make_unique<TitleBar>(window, m_qtWindowElement)) +{ + updateResizability(); +} + +NonClientArea::~NonClientArea() = default; + +void NonClientArea::onClientAreaWidthChange(int width) +{ + m_titleBar->setWidth(width); +} + +void NonClientArea::propagateSizeHints() +{ + updateResizability(); +} + +void NonClientArea::updateResizability() +{ + const auto resizeConstraints = m_resizer->getResizeConstraints(); + const bool nonResizable = resizeConstraints.minShrink.isNull() + && resizeConstraints.maxGrow.isNull() && resizeConstraints.maxGrowTop == 0; + dom::syncCSSClassWith(m_qtWindowElement, "no-resize", nonResizable); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.h b/src/plugins/platforms/wasm/qwasmwindownonclientarea.h new file mode 100644 index 0000000000..78c77585a0 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.h @@ -0,0 +1,227 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWNONCLIENTAREA_H +#define QWASMWINDOWNONCLIENTAREA_H + +#include <QtCore/qrect.h> +#include <QtCore/qtconfigmacros.h> +#include <QtCore/qnamespace.h> + +#include <emscripten/val.h> + +#include <functional> +#include <memory> +#include <string_view> +#include <vector> + +QT_BEGIN_NAMESPACE + +namespace qstdweb { +class EventCallback; +} + +struct PointerEvent; +class QWindow; +class Resizer; +class TitleBar; +class QWasmWindow; + +class NonClientArea +{ +public: + NonClientArea(QWasmWindow *window, emscripten::val containerElement); + ~NonClientArea(); + + void onClientAreaWidthChange(int width); + void propagateSizeHints(); + TitleBar *titleBar() const { return m_titleBar.get(); } + +private: + void updateResizability(); + + emscripten::val m_qtWindowElement; + std::unique_ptr<Resizer> m_resizer; + std::unique_ptr<TitleBar> m_titleBar; +}; + +class WebImageButton +{ +public: + class Callbacks + { + public: + Callbacks(); + Callbacks(std::function<void()> onInteraction, std::function<void()> onClick); + ~Callbacks(); + + Callbacks(const Callbacks &) = delete; + Callbacks(Callbacks &&); + Callbacks &operator=(const Callbacks &) = delete; + Callbacks &operator=(Callbacks &&); + + operator bool() const { return !!m_onInteraction; } + + void onInteraction(); + void onClick(); + + private: + std::function<void()> m_onInteraction; + std::function<void()> m_onClick; + }; + + WebImageButton(); + ~WebImageButton(); + + void setCallbacks(Callbacks callbacks); + void setImage(std::string_view imageData, std::string_view format); + void setVisible(bool visible); + + emscripten::val htmlElement() const { return m_containerElement; } + emscripten::val imageElement() const { return m_imgElement; } + +private: + emscripten::val m_containerElement; + emscripten::val m_imgElement; + + std::unique_ptr<qstdweb::EventCallback> m_webMouseMoveEventCallback; + std::unique_ptr<qstdweb::EventCallback> m_webMouseDownEventCallback; + std::unique_ptr<qstdweb::EventCallback> m_webClickEventCallback; + + Callbacks m_callbacks; +}; + +struct ResizeConstraints { + QPoint minShrink; + QPoint maxGrow; + int maxGrowTop; +}; + +class Resizer +{ +public: + class ResizerElement + { + public: + static constexpr const char *cssClassNameForEdges(Qt::Edges edges) + { + switch (edges) { + case Qt::TopEdge | Qt::LeftEdge:; + return "nw"; + case Qt::TopEdge: + return "n"; + case Qt::TopEdge | Qt::RightEdge: + return "ne"; + case Qt::LeftEdge: + return "w"; + case Qt::RightEdge: + return "e"; + case Qt::BottomEdge | Qt::LeftEdge: + return "sw"; + case Qt::BottomEdge: + return "s"; + case Qt::BottomEdge | Qt::RightEdge: + return "se"; + default: + return ""; + } + } + + ResizerElement(emscripten::val parentElement, Qt::Edges edges, Resizer *resizer); + ~ResizerElement(); + ResizerElement(const ResizerElement &other) = delete; + ResizerElement(ResizerElement &&other); + ResizerElement &operator=(const ResizerElement &other) = delete; + ResizerElement &operator=(ResizerElement &&other) = delete; + + bool onPointerDown(const PointerEvent &event); + bool onPointerMove(const PointerEvent &event); + bool onPointerUp(const PointerEvent &event); + + private: + emscripten::val m_element; + + int m_capturedPointerId = -1; + + const Qt::Edges m_edges; + + Resizer *m_resizer; + + std::unique_ptr<qstdweb::EventCallback> m_mouseDownEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseMoveEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseUpEvent; + }; + + using ClickCallback = std::function<void()>; + + Resizer(QWasmWindow *window, emscripten::val parentElement); + ~Resizer(); + + ResizeConstraints getResizeConstraints(); + +private: + void onInteraction(); + void startResize(Qt::Edges resizeEdges, const PointerEvent &event); + void continueResize(const PointerEvent &event); + void finishResize(); + + struct ResizeData + { + Qt::Edges edges = Qt::Edges::fromInt(0); + QPointF originInScreenCoords; + QPoint minShrink; + QPoint maxGrow; + QRect initialBounds; + }; + std::unique_ptr<ResizeData> m_currentResizeData; + + QWasmWindow *m_window; + emscripten::val m_windowElement; + std::vector<std::unique_ptr<ResizerElement>> m_elements; +}; + +class TitleBar +{ +public: + TitleBar(QWasmWindow *window, emscripten::val parentElement); + ~TitleBar(); + + void setTitle(const QString &title); + void setRestoreVisible(bool visible); + void setMaximizeVisible(bool visible); + void setCloseVisible(bool visible); + void setIcon(std::string_view imageData, std::string_view format); + void setWidth(int width); + + QRectF geometry() const; + +private: + bool onPointerDown(const PointerEvent &event); + bool onPointerMove(const PointerEvent &event); + bool onPointerUp(const PointerEvent &event); + bool onDoubleClick(); + + QPointF clipPointWithScreen(const QPointF &pointInTitleBarCoords) const; + + QWasmWindow *m_window; + + emscripten::val m_element; + emscripten::val m_label; + + std::unique_ptr<WebImageButton> m_close; + std::unique_ptr<WebImageButton> m_maximize; + std::unique_ptr<WebImageButton> m_restore; + std::unique_ptr<WebImageButton> m_icon; + + int m_capturedPointerId = -1; + QPointF m_moveStartPoint; + QPoint m_moveStartWindowPosition; + + std::unique_ptr<qstdweb::EventCallback> m_mouseDownEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseMoveEvent; + std::unique_ptr<qstdweb::EventCallback> m_mouseUpEvent; + std::unique_ptr<qstdweb::EventCallback> m_doubleClickEvent; +}; + +QT_END_NAMESPACE +#endif // QWASMWINDOWNONCLIENTAREA_H diff --git a/src/plugins/platforms/wasm/qwasmwindowstack.cpp b/src/plugins/platforms/wasm/qwasmwindowstack.cpp new file mode 100644 index 0000000000..d3769c7a1b --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowstack.cpp @@ -0,0 +1,203 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowstack.h" + +QT_BEGIN_NAMESPACE + +QWasmWindowStack::QWasmWindowStack(WindowOrderChangedCallbackType windowOrderChangedCallback) + : m_windowOrderChangedCallback(std::move(windowOrderChangedCallback)), + m_regularWindowsBegin(m_windowStack.begin()), + m_alwaysOnTopWindowsBegin(m_windowStack.begin()) +{ +} + +QWasmWindowStack::~QWasmWindowStack() = default; + +void QWasmWindowStack::pushWindow(QWasmWindow *window, PositionPreference position) +{ + Q_ASSERT(m_windowStack.count(window) == 0); + + if (position == PositionPreference::StayOnTop) { + const auto stayOnTopDistance = + std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + m_windowStack.push_back(window); + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance; + m_regularWindowsBegin = m_windowStack.begin() + regularDistance; + } else if (position == PositionPreference::Regular) { + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + m_alwaysOnTopWindowsBegin = m_windowStack.insert(m_alwaysOnTopWindowsBegin, window) + 1; + m_regularWindowsBegin = m_windowStack.begin() + regularDistance; + } else { + const auto stayOnTopDistance = + std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + m_regularWindowsBegin = m_windowStack.insert(m_regularWindowsBegin, window) + 1; + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance + 1; + } + + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::removeWindow(QWasmWindow *window) +{ + Q_ASSERT(m_windowStack.count(window) == 1); + + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + const auto position = getWindowPositionPreference(it); + const auto stayOnTopDistance = std::distance(m_windowStack.begin(), m_alwaysOnTopWindowsBegin); + const auto regularDistance = std::distance(m_windowStack.begin(), m_regularWindowsBegin); + + m_windowStack.erase(it); + + m_alwaysOnTopWindowsBegin = m_windowStack.begin() + stayOnTopDistance + - (position != PositionPreference::StayOnTop ? 1 : 0); + m_regularWindowsBegin = m_windowStack.begin() + regularDistance + - (position == PositionPreference::StayOnBottom ? 1 : 0); + + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::raise(QWasmWindow *window) +{ + Q_ASSERT(m_windowStack.count(window) == 1); + + if (window == topWindow()) + return; + + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + auto itEnd = ([this, position = getWindowPositionPreference(it)]() { + switch (position) { + case PositionPreference::StayOnTop: + return m_windowStack.end(); + case PositionPreference::Regular: + return m_alwaysOnTopWindowsBegin; + case PositionPreference::StayOnBottom: + return m_regularWindowsBegin; + } + })(); + std::rotate(it, it + 1, itEnd); + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::lower(QWasmWindow *window) +{ + Q_ASSERT(m_windowStack.count(window) == 1); + + if (window == *m_windowStack.begin()) + return; + + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + auto itBegin = ([this, position = getWindowPositionPreference(it)]() { + switch (position) { + case PositionPreference::StayOnTop: + return m_alwaysOnTopWindowsBegin; + case PositionPreference::Regular: + return m_regularWindowsBegin; + case PositionPreference::StayOnBottom: + return m_windowStack.begin(); + } + })(); + + std::rotate(itBegin, it, it + 1); + m_windowOrderChangedCallback(); +} + +void QWasmWindowStack::windowPositionPreferenceChanged(QWasmWindow *window, + PositionPreference position) +{ + auto it = std::find(m_windowStack.begin(), m_windowStack.end(), window); + const auto currentPosition = getWindowPositionPreference(it); + + const auto zones = static_cast<int>(position) - static_cast<int>(currentPosition); + Q_ASSERT(zones != 0); + + if (zones < 0) { + // Perform right rotation so that the window lands on top of regular windows + const auto begin = std::make_reverse_iterator(it + 1); + const auto end = position == PositionPreference::Regular + ? std::make_reverse_iterator(m_alwaysOnTopWindowsBegin) + : std::make_reverse_iterator(m_regularWindowsBegin); + std::rotate(begin, begin + 1, end); + if (zones == -2) { + ++m_alwaysOnTopWindowsBegin; + ++m_regularWindowsBegin; + } else if (position == PositionPreference::Regular) { + ++m_alwaysOnTopWindowsBegin; + } else { + ++m_regularWindowsBegin; + } + } else { + // Perform left rotation so that the window lands at the bottom of always on top windows + const auto begin = it; + const auto end = position == PositionPreference::Regular ? m_regularWindowsBegin + : m_alwaysOnTopWindowsBegin; + std::rotate(begin, begin + 1, end); + if (zones == 2) { + --m_alwaysOnTopWindowsBegin; + --m_regularWindowsBegin; + } else if (position == PositionPreference::Regular) { + --m_regularWindowsBegin; + } else { + --m_alwaysOnTopWindowsBegin; + } + } + m_windowOrderChangedCallback(); +} + +QWasmWindowStack::iterator QWasmWindowStack::begin() +{ + return m_windowStack.rbegin(); +} + +QWasmWindowStack::iterator QWasmWindowStack::end() +{ + return m_windowStack.rend(); +} + +QWasmWindowStack::const_iterator QWasmWindowStack::begin() const +{ + return m_windowStack.rbegin(); +} + +QWasmWindowStack::const_iterator QWasmWindowStack::end() const +{ + return m_windowStack.rend(); +} + +QWasmWindowStack::const_reverse_iterator QWasmWindowStack::rbegin() const +{ + return m_windowStack.begin(); +} + +QWasmWindowStack::const_reverse_iterator QWasmWindowStack::rend() const +{ + return m_windowStack.end(); +} + +bool QWasmWindowStack::empty() const +{ + return m_windowStack.empty(); +} + +size_t QWasmWindowStack::size() const +{ + return m_windowStack.size(); +} + +QWasmWindow *QWasmWindowStack::topWindow() const +{ + return m_windowStack.empty() ? nullptr : m_windowStack.last(); +} + +QWasmWindowStack::PositionPreference +QWasmWindowStack::getWindowPositionPreference(StorageType::iterator windowIt) const +{ + if (windowIt >= m_alwaysOnTopWindowsBegin) + return PositionPreference::StayOnTop; + if (windowIt >= m_regularWindowsBegin) + return PositionPreference::Regular; + return PositionPreference::StayOnBottom; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowstack.h b/src/plugins/platforms/wasm/qwasmwindowstack.h new file mode 100644 index 0000000000..c75001157a --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowstack.h @@ -0,0 +1,75 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWSTACK_H +#define QWASMWINDOWSTACK_H + +#include <qglobal.h> +#include <QtCore/qlist.h> + +#include <vector> + +QT_BEGIN_NAMESPACE + +class QWasmWindow; + +// Maintains a z-order hierarchy for a set of windows. The first added window is always treated as +// the 'root', which always stays at the bottom. Other windows are 'regular', which means they are +// subject to z-order changes via |raise| and |lower|/ +// If the root is ever removed, all of the current and future windows in the stack are treated as +// regular. +// Access to the top element is facilitated by |topWindow|. +// Changes to the top element are signaled via the |topWindowChangedCallback| supplied at +// construction. +class Q_AUTOTEST_EXPORT QWasmWindowStack +{ +public: + using WindowOrderChangedCallbackType = std::function<void()>; + + using StorageType = QList<QWasmWindow *>; + + using iterator = StorageType::reverse_iterator; + using const_iterator = StorageType::const_reverse_iterator; + using const_reverse_iterator = StorageType::const_iterator; + + enum class PositionPreference { + StayOnBottom, + Regular, + StayOnTop, + }; + + explicit QWasmWindowStack(WindowOrderChangedCallbackType topWindowChangedCallback); + ~QWasmWindowStack(); + + void pushWindow(QWasmWindow *window, PositionPreference position); + void removeWindow(QWasmWindow *window); + void raise(QWasmWindow *window); + void lower(QWasmWindow *window); + void windowPositionPreferenceChanged(QWasmWindow *window, PositionPreference position); + + // Iterates top-to-bottom + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + + // Iterates bottom-to-top + const_reverse_iterator rbegin() const; + const_reverse_iterator rend() const; + + bool empty() const; + size_t size() const; + QWasmWindow *topWindow() const; + +private: + PositionPreference getWindowPositionPreference(StorageType::iterator windowIt) const; + + WindowOrderChangedCallbackType m_windowOrderChangedCallback; + QList<QWasmWindow *> m_windowStack; + StorageType::iterator m_regularWindowsBegin; + StorageType::iterator m_alwaysOnTopWindowsBegin; +}; + +QT_END_NAMESPACE + +#endif // QWASMWINDOWSTACK_H diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp new file mode 100644 index 0000000000..ea8d8dbcfa --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmwindowtreenode.h" + +#include "qwasmwindow.h" + +QWasmWindowTreeNode::QWasmWindowTreeNode() + : m_childStack(std::bind(&QWasmWindowTreeNode::onTopWindowChanged, this)) +{ +} + +QWasmWindowTreeNode::~QWasmWindowTreeNode() = default; + +void QWasmWindowTreeNode::onParentChanged(QWasmWindowTreeNode *previousParent, + QWasmWindowTreeNode *currentParent, + QWasmWindowStack::PositionPreference positionPreference) +{ + auto *window = asWasmWindow(); + if (previousParent) { + previousParent->m_childStack.removeWindow(window); + previousParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeRemoval, previousParent, + window); + } + + if (currentParent) { + currentParent->m_childStack.pushWindow(window, positionPreference); + currentParent->onSubtreeChanged(QWasmWindowTreeNodeChangeType::NodeInsertion, currentParent, + window); + } +} + +QWasmWindow *QWasmWindowTreeNode::asWasmWindow() +{ + return nullptr; +} + +void QWasmWindowTreeNode::onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child) +{ + if (changeType == QWasmWindowTreeNodeChangeType::NodeInsertion && parent == this + && m_childStack.topWindow() + && m_childStack.topWindow()->window()) { + + const QVariant showWithoutActivating = m_childStack.topWindow()->window()->property("_q_showWithoutActivating"); + if (!showWithoutActivating.isValid() || !showWithoutActivating.toBool()) + m_childStack.topWindow()->requestActivateWindow(); + } + + if (parentNode()) + parentNode()->onSubtreeChanged(changeType, parent, child); +} + +void QWasmWindowTreeNode::setWindowZOrder(QWasmWindow *window, int z) +{ + window->setZOrder(z); +} + +void QWasmWindowTreeNode::onPositionPreferenceChanged( + QWasmWindowStack::PositionPreference positionPreference) +{ + if (parentNode()) { + parentNode()->m_childStack.windowPositionPreferenceChanged(asWasmWindow(), + positionPreference); + } +} + +void QWasmWindowTreeNode::setAsActiveNode() +{ + if (parentNode()) + parentNode()->setActiveChildNode(asWasmWindow()); +} + +void QWasmWindowTreeNode::bringToTop() +{ + if (!parentNode()) + return; + parentNode()->m_childStack.raise(asWasmWindow()); + parentNode()->bringToTop(); +} + +void QWasmWindowTreeNode::sendToBottom() +{ + if (!parentNode()) + return; + m_childStack.lower(asWasmWindow()); +} + +void QWasmWindowTreeNode::onTopWindowChanged() +{ + constexpr int zOrderForElementInFrontOfScreen = 3; + int z = zOrderForElementInFrontOfScreen; + std::for_each(m_childStack.rbegin(), m_childStack.rend(), + [this, &z](QWasmWindow *window) { setWindowZOrder(window, z++); }); +} + +void QWasmWindowTreeNode::setActiveChildNode(QWasmWindow *activeChild) +{ + m_activeChild = activeChild; + + auto it = m_childStack.begin(); + if (it == m_childStack.end()) + return; + for (; it != m_childStack.end(); ++it) + (*it)->onActivationChanged(*it == m_activeChild); + + setAsActiveNode(); +} diff --git a/src/plugins/platforms/wasm/qwasmwindowtreenode.h b/src/plugins/platforms/wasm/qwasmwindowtreenode.h new file mode 100644 index 0000000000..344fdb43cb --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmwindowtreenode.h @@ -0,0 +1,53 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWASMWINDOWTREENODE_H +#define QWASMWINDOWTREENODE_H + +#include "qwasmwindowstack.h" + +namespace emscripten { +class val; +} + +class QWasmWindow; + +enum class QWasmWindowTreeNodeChangeType { + NodeInsertion, + NodeRemoval, +}; + +class QWasmWindowTreeNode +{ +public: + QWasmWindowTreeNode(); + virtual ~QWasmWindowTreeNode(); + + virtual emscripten::val containerElement() = 0; + virtual QWasmWindowTreeNode *parentNode() = 0; + +protected: + virtual void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference); + virtual QWasmWindow *asWasmWindow(); + virtual void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindowTreeNode *parent, QWasmWindow *child); + virtual void setWindowZOrder(QWasmWindow *window, int z); + + void onPositionPreferenceChanged(QWasmWindowStack::PositionPreference positionPreference); + void setAsActiveNode(); + void bringToTop(); + void sendToBottom(); + + const QWasmWindowStack &childStack() const { return m_childStack; } + QWasmWindow *activeChild() const { return m_activeChild; } + +private: + void onTopWindowChanged(); + void setActiveChildNode(QWasmWindow *activeChild); + + QWasmWindowStack m_childStack; + QWasmWindow *m_activeChild = nullptr; +}; + +#endif // QWASMWINDOWTREENODE_H diff --git a/src/plugins/platforms/wasm/resources/maximize.svg b/src/plugins/platforms/wasm/resources/maximize.svg new file mode 100644 index 0000000000..b5fad4f707 --- /dev/null +++ b/src/plugins/platforms/wasm/resources/maximize.svg @@ -0,0 +1 @@ +<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg" class="svg-icon"><path stroke="null" d="M-.333-.333h1024.666v1024.666H-.333V-.333M127.75 255.833V896.25h768.5V255.833h-768.5z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/resources/qtlogo.svg b/src/plugins/platforms/wasm/resources/qtlogo.svg new file mode 100644 index 0000000000..bfe2493f46 --- /dev/null +++ b/src/plugins/platforms/wasm/resources/qtlogo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="616" height="452" viewBox="0 0 462 339"><path fill="#41cd52" d="M63.5 0H462v274.79c-21.4 21.47-42.87 42.87-64.39 64.21H0V63.39C21.08 42.18 42.34 21.13 63.5 0Z"/><path d="M122.37 71.33C137.5 61.32 156.21 58.79 174 58.95c16.94.21 34.72 3.18 48.76 13.29 10.2 7.17 16.83 18.24 21.25 29.69 7.15 18.8 9.25 39.1 9.49 59.08.03 20.12-.88 40.68-7.54 59.85-4.46 13.04-12.95 24.62-24.15 32.66 8.06 13.06 16.28 26.02 24.34 39.08-10.13 4.67-20.23 9.37-30.37 14.02-8.63-14.24-17.22-28.51-25.88-42.73-11.71 1.92-23.69 1.77-35.46.47-14.1-1.69-28.47-5.99-39.35-15.48-8.36-7.24-13.61-17.37-17.2-27.67-5.88-17.42-7.46-35.96-7.73-54.24-.14-19.76 1.12-39.83 7.08-58.79 4.61-14.26 12.24-28.49 25.13-36.85ZM294.13 70.69c10.6-.01 21.2-.01 31.8 0 .03 14.02-.01 28.03.02 42.05 13.55.02 27.1 0 40.65.01-.23 9.1-.48 18.2-.74 27.3-13.54.03-27.07-.01-40.61.02.03 22.98-.07 45.96.05 68.94.26 6.29.12 12.93 2.89 18.74 2.02 4.48 7.46 5.63 11.89 5.78 8.35-.03 16.69-.52 25.04-.67.51 8.36 1 16.73 1.48 25.09-16.61 2.79-34.04 6.13-50.54.91-6.95-2.06-13.43-6.67-16.25-13.54-5.05-11.69-5.46-24.7-5.68-37.25-.02-22.67 0-45.33-.01-68-7.39-.02-14.78.01-22.17-.02-.02-9.09-.02-18.19 0-27.29 7.39-.03 14.77.01 22.16-.02.03-14.02-.01-28.03.02-42.05Z" fill="#fff"/><path fill="#41cd52" d="M160.51 87.7c10.29-1.34 21.09-.98 30.83 2.91 7.89 3.12 14.59 9.23 18.13 16.97 5.43 11.73 7.51 24.68 8.56 37.47 1.14 17.02.98 34.2-1.37 51.12-1.65 10.07-4 20.68-10.82 28.62-6.92 7.97-17.59 11.39-27.83 12.19-10.8.79-22.19 0-31.94-5.11-5.69-3.03-10.52-7.78-13.34-13.6-3.42-6.97-5.3-14.58-6.62-22.2-3.98-24.16-4.94-49.16.5-73.18 2.24-9.06 5.5-18.36 12.12-25.19 5.76-5.85 13.78-8.87 21.78-10Z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/resources/restore.svg b/src/plugins/platforms/wasm/resources/restore.svg new file mode 100644 index 0000000000..70ee19170b --- /dev/null +++ b/src/plugins/platforms/wasm/resources/restore.svg @@ -0,0 +1 @@ +<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg" class="svg-icon"><path stroke="null" d="M449.191 44.905h535.142v528.951H449.191V44.906m66.893 132.237v330.594H917.44V177.143H516.084z"/><path stroke="null" d="M54.906 453.476h535.141v528.952H54.906V453.476m66.892 132.238V916.31h401.357V585.714H121.798z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/resources/x.svg b/src/plugins/platforms/wasm/resources/x.svg new file mode 100644 index 0000000000..1d9ba7361a --- /dev/null +++ b/src/plugins/platforms/wasm/resources/x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460.775 460.775" style="enable-background:new 0 0 460.775 460.775" xml:space="preserve"><path d="M285.08 230.397 456.218 59.27c6.076-6.077 6.076-15.911 0-21.986L423.511 4.565a15.55 15.55 0 0 0-21.985 0l-171.138 171.14L59.25 4.565a15.551 15.551 0 0 0-21.985 0L4.558 37.284c-6.077 6.075-6.077 15.909 0 21.986l171.138 171.128L4.575 401.505c-6.074 6.077-6.074 15.911 0 21.986l32.709 32.719a15.555 15.555 0 0 0 21.986 0l171.117-171.12 171.118 171.12a15.551 15.551 0 0 0 21.985 0l32.709-32.719c6.074-6.075 6.074-15.909 0-21.986L285.08 230.397z"/></svg>
\ No newline at end of file diff --git a/src/plugins/platforms/wasm/wasm_shell.html b/src/plugins/platforms/wasm/wasm_shell.html index ecbce6c043..6e93955552 100644 --- a/src/plugins/platforms/wasm/wasm_shell.html +++ b/src/plugins/platforms/wasm/wasm_shell.html @@ -1,3 +1,8 @@ +<!-- +Copyright (C) 2024 The Qt Company Ltd. +SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +--> + <!doctype html> <html lang="en-us"> <head> @@ -12,9 +17,8 @@ <title>@APPNAME@</title> <style> /* Make the html body cover the entire (visual) viewport with no scroll bars. */ - html, body { padding: 0; margin: 0; overflow:hidden; height: 100% } - /* Make the canvas cover the entire body */ - canvas { height:100%; width:100%; } + html, body { padding: 0; margin: 0; overflow: hidden; height: 100% } + #screen { width: 100%; height: 100%; } </style> </head> <body onload="init()"> @@ -26,44 +30,50 @@ <noscript>JavaScript is disabled. Please enable JavaScript to use this application.</noscript> </center> </figure> - <canvas id="qtcanvas"></canvas> + <div id="screen"></div> + + <script type="text/javascript"> + async function init() + { + const spinner = document.querySelector('#qtspinner'); + const screen = document.querySelector('#screen'); + const status = document.querySelector('#qtstatus'); + + const showUi = (ui) => { + [spinner, screen].forEach(element => element.style.display = 'none'); + if (screen === ui) + screen.style.position = 'default'; + ui.style.display = 'block'; + } - <script type='text/javascript'> - let qtLoader = undefined; - function init() { - var spinner = document.querySelector('#qtspinner'); - var canvas = document.querySelector('#qtcanvas'); - var status = document.querySelector('#qtstatus') + try { + showUi(spinner); + status.innerHTML = 'Loading...'; - qtLoader = QtLoader({ - canvasElements : [canvas], - showLoader: function(loaderStatus) { - spinner.style.display = 'block'; - canvas.style.display = 'none'; - status.innerHTML = loaderStatus + "..."; - }, - showError: function(errorText) { - status.innerHTML = errorText; - spinner.style.display = 'block'; - canvas.style.display = 'none'; - }, - showExit: function() { - status.innerHTML = "Application exit"; - if (qtLoader.exitCode !== undefined) - status.innerHTML += " with code " + qtLoader.exitCode; - if (qtLoader.exitText !== undefined) - status.innerHTML += " (" + qtLoader.exitText + ")"; - spinner.style.display = 'block'; - canvas.style.display = 'none'; - }, - showCanvas: function() { - spinner.style.display = 'none'; - canvas.style.display = 'block'; - }, - }); - qtLoader.loadEmscriptenModule("@APPNAME@"); - } + const instance = await qtLoad({ + qt: { + onLoaded: () => showUi(screen), + onExit: exitData => + { + status.innerHTML = 'Application exit'; + status.innerHTML += + exitData.code !== undefined ? ` with code ${exitData.code}` : ''; + status.innerHTML += + exitData.text !== undefined ? ` (${exitData.text})` : ''; + showUi(spinner); + }, + entryFunction: window.@APPEXPORTNAME@, + containerElements: [screen], + @PRELOAD@ + } + }); + } catch (e) { + console.error(e); + console.error(e.stack); + } + } </script> + <script src="@APPNAME@.js"></script> <script type="text/javascript" src="qtloader.js"></script> </body> </html> diff --git a/src/plugins/platforms/windows/CMakeLists.txt b/src/plugins/platforms/windows/CMakeLists.txt index d71d7e547f..8cd84e208b 100644 --- a/src/plugins/platforms/windows/CMakeLists.txt +++ b/src/plugins/platforms/windows/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from windows.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QWindowsIntegrationPlugin Plugin: @@ -7,26 +8,26 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin OUTPUT_NAME qwindows PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES windows # special case + DEFAULT_IF "windows" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qtwindowsglobal.h qwin10helpers.cpp qwin10helpers.h qwindowsapplication.cpp qwindowsapplication.h qwindowsbackingstore.cpp qwindowsbackingstore.h - qwindowscombase.h qwindowscontext.cpp qwindowscontext.h qwindowscursor.cpp qwindowscursor.h qwindowsdialoghelpers.cpp qwindowsdialoghelpers.h qwindowsdropdataobject.cpp qwindowsdropdataobject.h qwindowsgdiintegration.cpp qwindowsgdiintegration.h qwindowsgdinativeinterface.cpp qwindowsgdinativeinterface.h + qwindowsiconengine.cpp qwindowsiconengine.h qwindowsinputcontext.cpp qwindowsinputcontext.h qwindowsintegration.cpp qwindowsintegration.h qwindowsinternalmimedata.cpp qwindowsinternalmimedata.h qwindowskeymapper.cpp qwindowskeymapper.h qwindowsmenu.cpp qwindowsmenu.h - qwindowsmime.cpp qwindowsmime.h + qwindowsmimeregistry.cpp qwindowsmimeregistry.h qwindowsmousehandler.cpp qwindowsmousehandler.h qwindowsnativeinterface.cpp qwindowsnativeinterface.h qwindowsole.cpp qwindowsole.h @@ -37,6 +38,8 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin qwindowstheme.cpp qwindowstheme.h qwindowsthreadpoolrunner.h qwindowswindow.cpp qwindowswindow.h + NO_UNITY_BUILD_SOURCES + qwindowspointerhandler.cpp DEFINES QT_NO_CAST_FROM_ASCII QT_NO_FOREACH @@ -53,6 +56,7 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin imm32 ole32 oleaut32 + setupapi shell32 shlwapi user32 @@ -80,10 +84,6 @@ qt_internal_add_resource(QWindowsIntegrationPlugin "openglblacklists" ${openglblacklists_resource_files} ) - -#### Keys ignored in scope 1:.:.:windows.pro:<TRUE>: -# OTHER_FILES = "windows.json" - ## Scopes: ##################################################################### @@ -95,9 +95,6 @@ qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION QT_FEATURE_opengl Qt::OpenGLPrivate ) -#### Keys ignored in scope 3:.:.:windows.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" - qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION QT_FEATURE_opengl AND NOT QT_FEATURE_dynamicgl LIBRARIES opengl32 @@ -106,6 +103,8 @@ qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION QT_FEATURE_opengl qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION MINGW LIBRARIES uuid + NO_PCH_SOURCES + qwindowspointerhandler.cpp ) qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION QT_FEATURE_systemtrayicon @@ -176,6 +175,7 @@ endif() qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION QT_FEATURE_accessibility SOURCES + uiautomation/qwindowsuiautomation.cpp uiautomation/qwindowsuiautomation.h uiautomation/qwindowsuiaaccessibility.cpp uiautomation/qwindowsuiaaccessibility.h uiautomation/qwindowsuiabaseprovider.cpp uiautomation/qwindowsuiabaseprovider.h uiautomation/qwindowsuiaexpandcollapseprovider.cpp uiautomation/qwindowsuiaexpandcollapseprovider.h @@ -197,13 +197,17 @@ qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION QT_FEATURE_accessi uiautomation/qwindowsuiawindowprovider.cpp uiautomation/qwindowsuiawindowprovider.h ) +if(QT_FEATURE_accessibility) + find_library(UI_AUTOMATION_LIBRARY uiautomationcore) + if(UI_AUTOMATION_LIBRARY) + qt_internal_extend_target(QWindowsIntegrationPlugin + LIBRARIES + ${UI_AUTOMATION_LIBRARY} + ) + endif() +endif() + qt_internal_extend_target(QWindowsIntegrationPlugin CONDITION MINGW AND QT_FEATURE_accessibility LIBRARIES uuid ) - -# begin special case -if (MINGW) - set_source_files_properties(qwindowspointerhandler.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON) -endif() -# end special case diff --git a/src/plugins/platforms/windows/qtwindowsglobal.h b/src/plugins/platforms/windows/qtwindowsglobal.h index 9a65603e24..96a72600eb 100644 --- a/src/plugins/platforms/windows/qtwindowsglobal.h +++ b/src/plugins/platforms/windows/qtwindowsglobal.h @@ -65,7 +65,7 @@ QT_BEGIN_NAMESPACE namespace QtWindows { -enum +enum WindowsEventTypeFlags { WindowEventFlag = 0x10000, MouseEventFlag = 0x20000, @@ -107,6 +107,7 @@ enum WindowsEventType // Simplify event types PointerActivateWindowEvent = WindowEventFlag + 24, DpiScaledSizeEvent = WindowEventFlag + 25, DpiChangedAfterParentEvent = WindowEventFlag + 27, + TaskbarButtonCreated = WindowEventFlag + 28, MouseEvent = MouseEventFlag + 1, MouseWheelEvent = MouseEventFlag + 2, CursorEvent = MouseEventFlag + 3, @@ -118,7 +119,7 @@ enum WindowsEventType // Simplify event types NonClientPointerEvent = NonClientEventFlag + PointerEventFlag + 4, KeyEvent = KeyEventFlag + 1, KeyDownEvent = KeyEventFlag + KeyDownEventFlag + 1, - KeyboardLayoutChangeEvent = KeyEventFlag + 2, + InputLanguageChangeEvent = KeyEventFlag + 2, InputMethodKeyEvent = InputMethodEventFlag + KeyEventFlag + 1, InputMethodKeyDownEvent = InputMethodEventFlag + KeyEventFlag + KeyDownEventFlag + 1, ClipboardEvent = ClipboardEventFlag + 1, @@ -146,20 +147,31 @@ enum WindowsEventType // Simplify event types GestureEvent = 124, UnknownEvent = 542 }; +Q_DECLARE_MIXED_ENUM_OPERATORS(bool, WindowsEventTypeFlags, WindowsEventType); +Q_DECLARE_MIXED_ENUM_OPERATORS(bool, WindowsEventType, WindowsEventTypeFlags); -// Matches Process_DPI_Awareness (Windows 8.1 onwards), used for SetProcessDpiAwareness() -enum ProcessDpiAwareness +enum class DpiAwareness { - ProcessDpiUnaware, - ProcessSystemDpiAware, - ProcessPerMonitorDpiAware, - ProcessPerMonitorV2DpiAware // Qt extension (not in Process_DPI_Awareness) + Invalid = -1, + Unaware, + System, + PerMonitor, + PerMonitorVersion2, + Unaware_GdiScaled }; } // namespace QtWindows inline QtWindows::WindowsEventType windowsEventType(UINT message, WPARAM wParamIn, LPARAM lParamIn) { + static const UINT WM_TASKBAR_BUTTON_CREATED = []{ + UINT message = RegisterWindowMessage(L"TaskbarButtonCreated"); + // In case the application is run elevated, allow the + // TaskbarButtonCreated message through. + ChangeWindowMessageFilter(message, MSGFLT_ADD); + return message; + }(); + switch (message) { case WM_PAINT: case WM_ERASEBKGND: @@ -218,7 +230,7 @@ inline QtWindows::WindowsEventType windowsEventType(UINT message, WPARAM wParamI return QtWindows::InputMethodKeyDownEvent; #ifdef WM_INPUTLANGCHANGE case WM_INPUTLANGCHANGE: - return QtWindows::KeyboardLayoutChangeEvent; + return QtWindows::InputLanguageChangeEvent; #endif // WM_INPUTLANGCHANGE case WM_TOUCH: return QtWindows::TouchEvent; @@ -310,9 +322,15 @@ inline QtWindows::WindowsEventType windowsEventType(UINT message, WPARAM wParamI return QtWindows::NonClientPointerEvent; if (message >= WM_POINTERUPDATE && message <= WM_POINTERHWHEEL) return QtWindows::PointerEvent; + if (message == WM_TASKBAR_BUTTON_CREATED) + return QtWindows::TaskbarButtonCreated; return QtWindows::UnknownEvent; } +#ifndef QT_NO_DEBUG_STREAM +extern QDebug operator<<(QDebug, QtWindows::DpiAwareness); +#endif + QT_END_NAMESPACE #endif // QTWINDOWSGLOBAL_H diff --git a/src/plugins/platforms/windows/qwindowsapplication.cpp b/src/plugins/platforms/windows/qwindowsapplication.cpp index ce52cc817e..42e34ac99f 100644 --- a/src/plugins/platforms/windows/qwindowsapplication.cpp +++ b/src/plugins/platforms/windows/qwindowsapplication.cpp @@ -4,11 +4,17 @@ #include "qwindowsapplication.h" #include "qwindowsclipboard.h" #include "qwindowscontext.h" -#include "qwindowsmime.h" +#include "qwindowsmimeregistry.h" #include "qwin10helpers.h" #include "qwindowsopengltester.h" +#include "qwindowswindow.h" +#include "qwindowsintegration.h" +#include "qwindowstheme.h" -#include <QtCore/QVariant> +#include <QtCore/qvariant.h> +#include <QtCore/private/qfunctions_win_p.h> + +#include <QtGui/qpalette.h> QT_BEGIN_NAMESPACE @@ -36,6 +42,11 @@ void QWindowsApplication::setWindowActivationBehavior(WindowActivationBehavior b m_windowActivationBehavior = behavior; } +void QWindowsApplication::setHasBorderInFullScreenDefault(bool border) +{ + QWindowsWindow::setHasBorderInFullScreenDefault(border); +} + bool QWindowsApplication::isTabletMode() const { #if QT_CONFIG(clipboard) @@ -61,11 +72,6 @@ bool QWindowsApplication::setWinTabEnabled(bool enabled) return enabled ? ctx->initTablet() : ctx->disposeTablet(); } -bool QWindowsApplication::isDarkMode() const -{ - return QWindowsContext::isDarkMode(); -} - QWindowsApplication::DarkModeHandling QWindowsApplication::darkModeHandling() const { return m_darkModeHandling; @@ -76,13 +82,13 @@ void QWindowsApplication::setDarkModeHandling(QWindowsApplication::DarkModeHandl m_darkModeHandling = handling; } -void QWindowsApplication::registerMime(QNativeInterface::Private::QWindowsMime *mime) +void QWindowsApplication::registerMime(QWindowsMimeConverter *mime) { if (auto ctx = QWindowsContext::instance()) ctx->mimeConverter().registerMime(mime); } -void QWindowsApplication::unregisterMime(QNativeInterface::Private::QWindowsMime *mime) +void QWindowsApplication::unregisterMime(QWindowsMimeConverter *mime) { if (auto ctx = QWindowsContext::instance()) ctx->mimeConverter().unregisterMime(mime); @@ -90,7 +96,7 @@ void QWindowsApplication::unregisterMime(QNativeInterface::Private::QWindowsMime int QWindowsApplication::registerMimeType(const QString &mime) { - return QWindowsMimeConverter::registerMimeType(mime); + return QWindowsMimeRegistry::registerMimeType(mime); } HWND QWindowsApplication::createMessageWindow(const QString &classNameTemplate, @@ -132,4 +138,9 @@ QVariant QWindowsApplication::gpuList() const return result; } +void QWindowsApplication::populateLightSystemPalette(QPalette &result) const +{ + result = QWindowsTheme::systemPalette(Qt::ColorScheme::Light); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/qwindowsapplication.h b/src/plugins/platforms/windows/qwindowsapplication.h index fc6d8b5b76..0918df91af 100644 --- a/src/plugins/platforms/windows/qwindowsapplication.h +++ b/src/plugins/platforms/windows/qwindowsapplication.h @@ -17,17 +17,18 @@ public: WindowActivationBehavior windowActivationBehavior() const override; void setWindowActivationBehavior(WindowActivationBehavior behavior) override; + void setHasBorderInFullScreenDefault(bool border) override; + bool isTabletMode() const override; bool isWinTabEnabled() const override; bool setWinTabEnabled(bool enabled) override; - bool isDarkMode() const override; DarkModeHandling darkModeHandling() const override; void setDarkModeHandling(DarkModeHandling handling) override; - void registerMime(QNativeInterface::Private::QWindowsMime *mime) override; - void unregisterMime(QNativeInterface::Private::QWindowsMime *mime) override; + void registerMime(QWindowsMimeConverter *mime) override; + void unregisterMime(QWindowsMimeConverter *mime) override; int registerMimeType(const QString &mime) override; @@ -41,6 +42,8 @@ public: QVariant gpu() const override; QVariant gpuList() const override; + void populateLightSystemPalette(QPalette &palette) const override; + private: WindowActivationBehavior m_windowActivationBehavior = DefaultActivateWindow; TouchWindowTouchTypes m_touchWindowTouchTypes = NormalTouch; diff --git a/src/plugins/platforms/windows/qwindowsbackingstore.cpp b/src/plugins/platforms/windows/qwindowsbackingstore.cpp index 0f9d0172d9..07918f6094 100644 --- a/src/plugins/platforms/windows/qwindowsbackingstore.cpp +++ b/src/plugins/platforms/windows/qwindowsbackingstore.cpp @@ -117,7 +117,7 @@ void QWindowsBackingStore::resize(const QSize &size, const QRegion ®ion) if (QImage::toPixelFormat(format).alphaUsage() == QPixelFormat::UsesAlpha) m_alphaNeedsFill = true; else // upgrade but here we know app painting does not rely on alpha hence no need to fill - format = qt_maybeAlphaVersionWithSameDepth(format); + format = qt_maybeDataCompatibleAlphaVersion(format); QWindowsNativeImage *oldwni = m_image.data(); auto *newwni = new QWindowsNativeImage(size.width(), size.height(), format); diff --git a/src/plugins/platforms/windows/qwindowsclipboard.cpp b/src/plugins/platforms/windows/qwindowsclipboard.cpp index 9bdd331467..7a6d41e0b3 100644 --- a/src/plugins/platforms/windows/qwindowsclipboard.cpp +++ b/src/plugins/platforms/windows/qwindowsclipboard.cpp @@ -4,7 +4,6 @@ #include "qwindowsclipboard.h" #include "qwindowscontext.h" #include "qwindowsole.h" -#include "qwindowsmime.h" #include <QtGui/qguiapplication.h> #include <QtGui/qclipboard.h> @@ -17,6 +16,7 @@ #include <QtCore/qthread.h> #include <QtCore/qvariant.h> #include <QtCore/qurl.h> +#include <QtCore/private/qsystemerror_p.h> #include <QtGui/private/qwindowsguieventdispatcher_p.h> @@ -301,7 +301,7 @@ void QWindowsClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) mimeData->formats().join(u", ") : QString(QStringLiteral("NULL")); qErrnoWarning("OleSetClipboard: Failed to set mime data (%s) on clipboard: %s", qPrintable(mimeDataFormats), - QWindowsContext::comErrorString(src).constData()); + qPrintable(QSystemError::windowsComString(src))); releaseIData(); return; } diff --git a/src/plugins/platforms/windows/qwindowscombase.h b/src/plugins/platforms/windows/qwindowscombase.h deleted file mode 100644 index 04d4dc51cf..0000000000 --- a/src/plugins/platforms/windows/qwindowscombase.h +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2017 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QWINDOWSCOMBASE_H -#define QWINDOWSCOMBASE_H - -#include <QtCore/qglobal.h> - -#include <unknwn.h> - -QT_BEGIN_NAMESPACE - -// The __uuidof operator of MinGW does not work for all interfaces (for example, -// IAccessible2). Specializations of this function can be provides to work -// around this. -template <class DesiredInterface> -static IID qUuidOf() { return __uuidof(DesiredInterface); } - -// Helper for implementing IUnknown::QueryInterface. -template <class DesiredInterface, class Derived> -bool qWindowsComQueryInterface(Derived *d, REFIID id, LPVOID *iface) -{ - if (id == qUuidOf<DesiredInterface>()) { - *iface = static_cast<DesiredInterface *>(d); - d->AddRef(); - return true; - } - return false; -} - -// Helper for implementing IUnknown::QueryInterface for IUnknown -// in the case of multiple inheritance via the first inherited class. -template <class FirstInheritedInterface, class Derived> -bool qWindowsComQueryUnknownInterfaceMulti(Derived *d, REFIID id, LPVOID *iface) -{ - if (id == __uuidof(IUnknown)) { - *iface = static_cast<FirstInheritedInterface *>(d); - d->AddRef(); - return true; - } - return false; -} - -// Helper base class to provide IUnknown methods for COM classes (single inheritance) -template <class ComInterface> class QWindowsComBase : public ComInterface -{ - Q_DISABLE_COPY_MOVE(QWindowsComBase) -public: - explicit QWindowsComBase(ULONG initialRefCount = 1) : m_ref(initialRefCount) {} - virtual ~QWindowsComBase() = default; - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id, LPVOID *iface) override - { - *iface = nullptr; - return qWindowsComQueryInterface<IUnknown>(this, id, iface) || qWindowsComQueryInterface<ComInterface>(this, id, iface) - ? S_OK : E_NOINTERFACE; - } - - ULONG STDMETHODCALLTYPE AddRef() override { return ++m_ref; } - - ULONG STDMETHODCALLTYPE Release() override - { - if (!--m_ref) { - delete this; - return 0; - } - return m_ref; - } - -private: - ULONG m_ref; -}; - -// Clang does not consider __declspec(nothrow) as nothrow -QT_WARNING_DISABLE_CLANG("-Wmicrosoft-exception-spec") -QT_WARNING_DISABLE_CLANG("-Wmissing-exception-spec") - -QT_END_NAMESPACE - -#endif // QWINDOWSCOMBASE_H diff --git a/src/plugins/platforms/windows/qwindowscontext.cpp b/src/plugins/platforms/windows/qwindowscontext.cpp index 9397c0213e..8c0261d568 100644 --- a/src/plugins/platforms/windows/qwindowscontext.cpp +++ b/src/plugins/platforms/windows/qwindowscontext.cpp @@ -11,7 +11,7 @@ #include "qwindowspointerhandler.h" #include "qtwindowsglobal.h" #include "qwindowsmenu.h" -#include "qwindowsmime.h" +#include "qwindowsmimeregistry.h" #include "qwindowsinputcontext.h" #if QT_CONFIG(tabletevent) # include "qwindowstabletsupport.h" @@ -45,13 +45,17 @@ #include <QtCore/qscopedpointer.h> #include <QtCore/quuid.h> #include <QtCore/private/qwinregistry_p.h> +#if QT_CONFIG(cpp_winrt) +# include <QtCore/private/qfactorycacheregistration_p.h> +#endif +#include <QtCore/private/qsystemerror_p.h> #include <QtGui/private/qwindowsguieventdispatcher_p.h> +#include <QtGui/private/qwindowsthemecache_p.h> #include <stdlib.h> #include <stdio.h> #include <windowsx.h> -#include <comdef.h> #include <dbt.h> #include <wtsapi32.h> #include <shellscalingapi.h> @@ -60,7 +64,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -Q_LOGGING_CATEGORY(lcQpaWindows, "qt.qpa.windows") +Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window") Q_LOGGING_CATEGORY(lcQpaEvents, "qt.qpa.events") Q_LOGGING_CATEGORY(lcQpaGl, "qt.qpa.gl") Q_LOGGING_CATEGORY(lcQpaMime, "qt.qpa.mime") @@ -117,26 +121,6 @@ static inline bool sessionManagerInteractionBlocked() static inline bool sessionManagerInteractionBlocked() { return false; } #endif -static inline int windowDpiAwareness(HWND hwnd) -{ - return static_cast<int>(GetAwarenessFromDpiAwarenessContext(GetWindowDpiAwarenessContext(hwnd))); -} - -// Note: This only works within WM_NCCREATE -static bool enableNonClientDpiScaling(HWND hwnd) -{ - bool result = false; - if (windowDpiAwareness(hwnd) == 2) { - result = EnableNonClientDpiScaling(hwnd) != FALSE; - if (!result) { - const DWORD errorCode = GetLastError(); - qErrnoWarning(int(errorCode), "EnableNonClientDpiScaling() failed for HWND %p (%lu)", - hwnd, errorCode); - } - } - return result; -} - QWindowsContext *QWindowsContext::m_instance = nullptr; /*! @@ -159,7 +143,7 @@ struct QWindowsContextPrivate { QWindowsKeyMapper m_keyMapper; QWindowsMouseHandler m_mouseHandler; QWindowsPointerHandler m_pointerHandler; - QWindowsMimeConverter m_mimeConverter; + QWindowsMimeRegistry m_mimeConverter; QWindowsScreenManager m_screenManager; QSharedPointer<QWindowCreationContext> m_creationContext; #if QT_CONFIG(tabletevent) @@ -170,11 +154,9 @@ struct QWindowsContextPrivate { bool m_asyncExpose = false; HPOWERNOTIFY m_powerNotification = nullptr; HWND m_powerDummyWindow = nullptr; - static bool m_darkMode; static bool m_v2DpiAware; }; -bool QWindowsContextPrivate::m_darkMode = false; bool QWindowsContextPrivate::m_v2DpiAware = false; QWindowsContextPrivate::QWindowsContextPrivate() @@ -188,10 +170,9 @@ QWindowsContextPrivate::QWindowsContextPrivate() m_systemInfo |= QWindowsContext::SI_RTL_Extensions; m_keyMapper.setUseRTLExtensions(true); } - m_darkMode = QWindowsTheme::queryDarkMode(); if (FAILED(m_oleInitializeResult)) { qWarning() << "QWindowsContext: OleInitialize() failed: " - << QWindowsContext::comErrorString(m_oleInitializeResult); + << QSystemError::windowsComString(m_oleInitializeResult); } } @@ -220,9 +201,15 @@ QWindowsContext::~QWindowsContext() if (d->m_powerDummyWindow) DestroyWindow(d->m_powerDummyWindow); + d->m_screenManager.destroyWindow(); + unregisterWindowClasses(); - if (d->m_oleInitializeResult == S_OK || d->m_oleInitializeResult == S_FALSE) + if (d->m_oleInitializeResult == S_OK || d->m_oleInitializeResult == S_FALSE) { +#ifdef QT_USE_FACTORY_CACHE_REGISTRATION + detail::QWinRTFactoryCacheRegistration::clearAllCaches(); +#endif OleUninitialize(); + } d->m_screenManager.clearScreens(); // Order: Potentially calls back to the windows. if (d->m_displayContext) @@ -264,7 +251,7 @@ void QWindowsContext::registerTouchWindows() { if (QGuiApplicationPrivate::is_app_running && (d->m_systemInfo & QWindowsContext::SI_SupportsTouch) != 0) { - for (QWindowsWindow *w : qAsConst(d->m_windows)) + for (QWindowsWindow *w : std::as_const(d->m_windows)) w->registerTouchWindow(); } } @@ -361,48 +348,121 @@ void QWindowsContext::setDetectAltGrModifier(bool a) d->m_keyMapper.setDetectAltGrModifier(a); } -int QWindowsContext::processDpiAwareness() -{ - PROCESS_DPI_AWARENESS result; - if (SUCCEEDED(GetProcessDpiAwareness(nullptr, &result))) { - return static_cast<int>(result); +[[nodiscard]] static inline QtWindows::DpiAwareness + dpiAwarenessContextToQtDpiAwareness(DPI_AWARENESS_CONTEXT context) +{ + // IsValidDpiAwarenessContext() will handle the NULL pointer case. + if (!IsValidDpiAwarenessContext(context)) + return QtWindows::DpiAwareness::Invalid; + if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED)) + return QtWindows::DpiAwareness::Unaware_GdiScaled; + if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) + return QtWindows::DpiAwareness::PerMonitorVersion2; + if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) + return QtWindows::DpiAwareness::PerMonitor; + if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_SYSTEM_AWARE)) + return QtWindows::DpiAwareness::System; + if (AreDpiAwarenessContextsEqual(context, DPI_AWARENESS_CONTEXT_UNAWARE)) + return QtWindows::DpiAwareness::Unaware; + return QtWindows::DpiAwareness::Invalid; +} + +QtWindows::DpiAwareness QWindowsContext::windowDpiAwareness(HWND hwnd) +{ + if (!hwnd) + return QtWindows::DpiAwareness::Invalid; + const auto context = GetWindowDpiAwarenessContext(hwnd); + return dpiAwarenessContextToQtDpiAwareness(context); +} + +QtWindows::DpiAwareness QWindowsContext::processDpiAwareness() +{ + // Although we have GetDpiAwarenessContextForProcess(), however, + // it's only available on Win10 1903+, which is a little higher + // than Qt's minimum supported version (1809), so we can't use it. + // Luckily, MS docs said GetThreadDpiAwarenessContext() will also + // return the default DPI_AWARENESS_CONTEXT for the process if + // SetThreadDpiAwarenessContext() was never called. So we can use + // it as an equivalent. + const auto context = GetThreadDpiAwarenessContext(); + return dpiAwarenessContextToQtDpiAwareness(context); +} + +[[nodiscard]] static inline DPI_AWARENESS_CONTEXT + qtDpiAwarenessToDpiAwarenessContext(QtWindows::DpiAwareness dpiAwareness) +{ + switch (dpiAwareness) { + case QtWindows::DpiAwareness::Invalid: + return nullptr; + case QtWindows::DpiAwareness::Unaware: + return DPI_AWARENESS_CONTEXT_UNAWARE; + case QtWindows::DpiAwareness::System: + return DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; + case QtWindows::DpiAwareness::PerMonitor: + return DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE; + case QtWindows::DpiAwareness::PerMonitorVersion2: + return DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2; + case QtWindows::DpiAwareness::Unaware_GdiScaled: + return DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED; } - return -1; + return nullptr; } -void QWindowsContext::setProcessDpiAwareness(QtWindows::ProcessDpiAwareness dpiAwareness) +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, QtWindows::DpiAwareness dpiAwareness) { - qCDebug(lcQpaWindows) << __FUNCTION__ << dpiAwareness; - const HRESULT hr = SetProcessDpiAwareness(static_cast<PROCESS_DPI_AWARENESS>(dpiAwareness)); - // E_ACCESSDENIED means set externally (MSVC manifest or external app loading Qt plugin). - // Silence warning in that case unless debug is enabled. - if (FAILED(hr) && (hr != E_ACCESSDENIED || lcQpaWindows().isDebugEnabled())) { - qWarning().noquote().nospace() << "SetProcessDpiAwareness(" - << dpiAwareness << ") failed: " << QWindowsContext::comErrorString(hr) - << ", using " << QWindowsContext::processDpiAwareness(); + const QDebugStateSaver saver(d); + QString message = u"QtWindows::DpiAwareness::"_s; + switch (dpiAwareness) { + case QtWindows::DpiAwareness::Invalid: + message += u"Invalid"_s; + break; + case QtWindows::DpiAwareness::Unaware: + message += u"Unaware"_s; + break; + case QtWindows::DpiAwareness::System: + message += u"System"_s; + break; + case QtWindows::DpiAwareness::PerMonitor: + message += u"PerMonitor"_s; + break; + case QtWindows::DpiAwareness::PerMonitorVersion2: + message += u"PerMonitorVersion2"_s; + break; + case QtWindows::DpiAwareness::Unaware_GdiScaled: + message += u"Unaware_GdiScaled"_s; + break; } + d.nospace().noquote() << message; + return d; } +#endif -bool QWindowsContext::setProcessDpiV2Awareness() +bool QWindowsContext::setProcessDpiAwareness(QtWindows::DpiAwareness dpiAwareness) { - qCDebug(lcQpaWindows) << __FUNCTION__; - const BOOL ok = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - if (!ok) { - const HRESULT errorCode = GetLastError(); - qCWarning(lcQpaWindows).noquote().nospace() << "setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: " - << QWindowsContext::comErrorString(errorCode); + qCDebug(lcQpaWindow) << __FUNCTION__ << dpiAwareness; + if (processDpiAwareness() == dpiAwareness) + return true; + const auto context = qtDpiAwarenessToDpiAwarenessContext(dpiAwareness); + if (!IsValidDpiAwarenessContext(context)) { + qCWarning(lcQpaWindow) << dpiAwareness << "is not supported by current system."; return false; } - - QWindowsContextPrivate::m_v2DpiAware = true; + if (!SetProcessDpiAwarenessContext(context)) { + qCWarning(lcQpaWindow).noquote().nospace() + << "SetProcessDpiAwarenessContext() failed: " + << QSystemError::windowsString() + << "\nQt's default DPI awareness context is " + << "DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2. If you know what you " + << "are doing, you can overwrite this default using qt.conf " + << "(https://doc.qt.io/qt-6/highdpi.html#configuring-windows)."; + return false; + } + QWindowsContextPrivate::m_v2DpiAware + = processDpiAwareness() == QtWindows::DpiAwareness::PerMonitorVersion2; return true; } -bool QWindowsContext::isDarkMode() -{ - return QWindowsContextPrivate::m_darkMode; -} - QWindowsContext *QWindowsContext::instance() { return m_instance; @@ -418,9 +478,9 @@ bool QWindowsContext::useRTLExtensions() const return d->m_keyMapper.useRTLExtensions(); } -QList<int> QWindowsContext::possibleKeys(const QKeyEvent *e) const +QPlatformKeyMapper *QWindowsContext::keyMapper() const { - return d->m_keyMapper.possibleKeys(e); + return &d->m_keyMapper; } QWindowsContext::HandleBaseWindowHash &QWindowsContext::windows() @@ -472,6 +532,8 @@ QString QWindowsContext::classNamePrefix() # define xstr(s) str(s) # define str(s) #s str << xstr(QT_NAMESPACE); +# undef str +# undef xstr #endif } return result; @@ -532,7 +594,7 @@ QString QWindowsContext::registerWindowClass(const QWindow *w) if (icon) cname += "Icon"_L1; - return registerWindowClass(cname, qWindowsWndProc, style, GetSysColorBrush(COLOR_WINDOW), icon); + return registerWindowClass(cname, qWindowsWndProc, style, nullptr, icon); } QString QWindowsContext::registerWindowClass(QString cname, @@ -591,7 +653,7 @@ QString QWindowsContext::registerWindowClass(QString cname, qPrintable(cname)); d->m_registeredWindowClassNames.insert(cname); - qCDebug(lcQpaWindows).nospace() << __FUNCTION__ << ' ' << cname + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << ' ' << cname << " style=0x" << Qt::hex << style << Qt::dec << " brush=" << brush << " icon=" << icon << " atom=" << atom; return cname; @@ -601,7 +663,7 @@ void QWindowsContext::unregisterWindowClasses() { const auto appInstance = static_cast<HINSTANCE>(GetModuleHandle(nullptr)); - for (const QString &name : qAsConst(d->m_registeredWindowClassNames)) { + for (const QString &name : std::as_const(d->m_registeredWindowClassNames)) { if (!UnregisterClass(reinterpret_cast<LPCWSTR>(name.utf16()), appInstance) && QWindowsContext::verbose) qErrnoWarning("UnregisterClass failed for '%s'", qPrintable(name)); } @@ -613,23 +675,6 @@ int QWindowsContext::screenDepth() const return GetDeviceCaps(d->m_displayContext, BITSPIXEL); } -QString QWindowsContext::windowsErrorMessage(unsigned long errorCode) -{ - QString rc = QString::fromLatin1("#%1: ").arg(errorCode); - char16_t *lpMsgBuf; - - const DWORD len = FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, errorCode, 0, reinterpret_cast<LPTSTR>(&lpMsgBuf), 0, nullptr); - if (len) { - rc = QString::fromUtf16(lpMsgBuf, int(len)); - LocalFree(lpMsgBuf); - } else { - rc += QString::fromLatin1("<unknown error>"); - } - return rc; -} - void QWindowsContext::addWindow(HWND hwnd, QWindowsWindow *w) { d->m_windows.insert(hwnd, w); @@ -733,6 +778,8 @@ static inline bool findPlatformWindowHelper(const POINT &screenPoint, unsigned c if (!(cwexFlags & CWP_SKIPTRANSPARENT) && (GetWindowLongPtr(child, GWL_EXSTYLE) & WS_EX_TRANSPARENT)) { const HWND nonTransparentChild = ChildWindowFromPointEx(*hwnd, point, cwexFlags | CWP_SKIPTRANSPARENT); + if (!nonTransparentChild || nonTransparentChild == *hwnd) + return false; if (QWindowsWindow *nonTransparentWindow = context->findPlatformWindow(nonTransparentChild)) { *result = nonTransparentWindow; *hwnd = nonTransparentChild; @@ -790,7 +837,7 @@ bool QWindowsContext::isSessionLocked() return result; } -QWindowsMimeConverter &QWindowsContext::mimeConverter() const +QWindowsMimeRegistry &QWindowsContext::mimeConverter() const { return d->m_mimeConverter; } @@ -828,79 +875,6 @@ HWND QWindowsContext::createDummyWindow(const QString &classNameIn, HWND_MESSAGE, nullptr, static_cast<HINSTANCE>(GetModuleHandle(nullptr)), nullptr); } -/*! - \brief Common COM error strings. -*/ - -QByteArray QWindowsContext::comErrorString(HRESULT hr) -{ - QByteArray result = QByteArrayLiteral("COM error 0x") - + QByteArray::number(quintptr(hr), 16) + ' '; - switch (hr) { - case S_OK: - result += QByteArrayLiteral("S_OK"); - break; - case S_FALSE: - result += QByteArrayLiteral("S_FALSE"); - break; - case E_UNEXPECTED: - result += QByteArrayLiteral("E_UNEXPECTED"); - break; - case E_ACCESSDENIED: - result += QByteArrayLiteral("E_ACCESSDENIED"); - break; - case CO_E_ALREADYINITIALIZED: - result += QByteArrayLiteral("CO_E_ALREADYINITIALIZED"); - break; - case CO_E_NOTINITIALIZED: - result += QByteArrayLiteral("CO_E_NOTINITIALIZED"); - break; - case RPC_E_CHANGED_MODE: - result += QByteArrayLiteral("RPC_E_CHANGED_MODE"); - break; - case OLE_E_WRONGCOMPOBJ: - result += QByteArrayLiteral("OLE_E_WRONGCOMPOBJ"); - break; - case CO_E_NOT_SUPPORTED: - result += QByteArrayLiteral("CO_E_NOT_SUPPORTED"); - break; - case E_NOTIMPL: - result += QByteArrayLiteral("E_NOTIMPL"); - break; - case E_INVALIDARG: - result += QByteArrayLiteral("E_INVALIDARG"); - break; - case E_NOINTERFACE: - result += QByteArrayLiteral("E_NOINTERFACE"); - break; - case E_POINTER: - result += QByteArrayLiteral("E_POINTER"); - break; - case E_HANDLE: - result += QByteArrayLiteral("E_HANDLE"); - break; - case E_ABORT: - result += QByteArrayLiteral("E_ABORT"); - break; - case E_FAIL: - result += QByteArrayLiteral("E_FAIL"); - break; - case RPC_E_WRONG_THREAD: - result += QByteArrayLiteral("RPC_E_WRONG_THREAD"); - break; - case RPC_E_THREAD_NOT_INIT: - result += QByteArrayLiteral("RPC_E_THREAD_NOT_INIT"); - break; - default: - break; - } - _com_error error(hr); - result += QByteArrayLiteral(" ("); - result += QString::fromWCharArray(error.ErrorMessage()).toUtf8(); - result += ')'; - return result; -} - void QWindowsContext::forceNcCalcSize(HWND hwnd) { // Force WM_NCCALCSIZE to adjust margin @@ -995,6 +969,21 @@ static inline bool isInputMessage(UINT m) || (m >= WM_KEYFIRST && m <= WM_KEYLAST); } +// Note: This only works within WM_NCCREATE +static bool enableNonClientDpiScaling(HWND hwnd) +{ + bool result = false; + if (QWindowsContext::windowDpiAwareness(hwnd) == QtWindows::DpiAwareness::PerMonitor) { + result = EnableNonClientDpiScaling(hwnd) != FALSE; + if (!result) { + const DWORD errorCode = GetLastError(); + qErrnoWarning(int(errorCode), "EnableNonClientDpiScaling() failed for HWND %p (%lu)", + hwnd, errorCode); + } + } + return result; +} + /*! \brief Main windows procedure registered for windows. @@ -1011,9 +1000,10 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, MSG msg; msg.hwnd = hwnd; // re-create MSG structure - msg.message = message; // time and pt fields ignored + msg.message = message; msg.wParam = wParam; msg.lParam = lParam; + msg.time = GetMessageTime(); msg.pt.x = msg.pt.y = 0; if (et != QtWindows::CursorEvent && (et & (QtWindows::MouseEventFlag | QtWindows::NonClientEventFlag))) { msg.pt.x = GET_X_LPARAM(lParam); @@ -1097,19 +1087,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, // Only refresh the window theme if the user changes the personalize settings. if ((wParam == 0) && (lParam != 0) // lParam sometimes may be NULL. && (wcscmp(reinterpret_cast<LPCWSTR>(lParam), L"ImmersiveColorSet") == 0)) { - const bool darkMode = QWindowsTheme::queryDarkMode(); - if (darkMode != QWindowsContextPrivate::m_darkMode) { - QWindowsContextPrivate::m_darkMode = darkMode; - auto integration = QWindowsIntegration::instance(); - if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeWindowFrames)) { - for (QWindowsWindow *w : d->m_windows) - w->setDarkBorder(QWindowsContextPrivate::m_darkMode); - } - if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle)) { - QWindowsTheme::instance()->refresh(); - QWindowSystemInterface::handleThemeChange(); - } - } + QWindowsTheme::handleSettingsChanged(); } return d->m_screenManager.handleScreenChanges(); } @@ -1126,7 +1104,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, d->m_creationContext->applyToMinMaxInfo(reinterpret_cast<MINMAXINFO *>(lParam)); return true; case QtWindows::ResizeEvent: - d->m_creationContext->obtainedSize = QSize(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + d->m_creationContext->obtainedSize = QSize(LOWORD(lParam), HIWORD(lParam)); return true; case QtWindows::MoveEvent: d->m_creationContext->obtainedPos = QPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); @@ -1169,7 +1147,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, if (wParam == DBT_DEVNODES_CHANGED) initTouch(); break; - case QtWindows::KeyboardLayoutChangeEvent: + case QtWindows::InputLanguageChangeEvent: if (QWindowsInputContext *wic = windowsInputContext()) wic->handleInputLanguageChanged(wParam, lParam); Q_FALLTHROUGH(); @@ -1199,7 +1177,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, platformWindow->handleMoved(); return true; case QtWindows::ResizeEvent: - platformWindow->handleResized(static_cast<int>(wParam)); + platformWindow->handleResized(static_cast<int>(wParam), lParam); return true; case QtWindows::QuerySizeHints: platformWindow->getSizeHints(reinterpret_cast<MINMAXINFO *>(lParam)); @@ -1213,21 +1191,29 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, case QtWindows::ExposeEvent: return platformWindow->handleWmPaint(hwnd, message, wParam, lParam, result); case QtWindows::NonClientMouseEvent: - if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer) && platformWindow->frameStrutEventsEnabled()) + if (!platformWindow->frameStrutEventsEnabled()) + break; + if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer)) return sessionManagerInteractionBlocked() || d->m_pointerHandler.translateMouseEvent(platformWindow->window(), hwnd, et, msg, result); else return sessionManagerInteractionBlocked() || d->m_mouseHandler.translateMouseEvent(platformWindow->window(), hwnd, et, msg, result); case QtWindows::NonClientPointerEvent: - if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer) && platformWindow->frameStrutEventsEnabled()) + if (!platformWindow->frameStrutEventsEnabled()) + break; + if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer)) return sessionManagerInteractionBlocked() || d->m_pointerHandler.translatePointerEvent(platformWindow->window(), hwnd, et, msg, result); break; case QtWindows::EnterSizeMoveEvent: platformWindow->setFlag(QWindowsWindow::ResizeMoveActive); + if (!IsZoomed(hwnd)) + platformWindow->updateRestoreGeometry(); return true; case QtWindows::ExitSizeMoveEvent: platformWindow->clearFlag(QWindowsWindow::ResizeMoveActive); platformWindow->checkForScreenChanged(); handleExitSizeMove(platformWindow->window()); + if (!IsZoomed(hwnd)) + platformWindow->updateRestoreGeometry(); return true; case QtWindows::ScrollEvent: if (!(d->m_systemInfo & QWindowsContext::SI_SupportsPointer)) @@ -1273,6 +1259,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, QWindowSystemInterface::handleCloseEvent(platformWindow->window()); return true; case QtWindows::ThemeChanged: { + QWindowsThemeCache::clearThemeCache(platformWindow->handle()); // Switch from Aero to Classic changes margins. if (QWindowsTheme *theme = QWindowsTheme::instance()) theme->windowsThemeChanged(platformWindow->window()); @@ -1364,6 +1351,11 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, return true; } #endif // !defined(QT_NO_SESSIONMANAGER) + case QtWindows::TaskbarButtonCreated: + // Apply application badge if this is the first time we have a taskbar + // button, or after Explorer restart. + QWindowsIntegration::instance()->updateApplicationBadge(); + break; default: break; } @@ -1406,7 +1398,7 @@ void QWindowsContext::handleFocusEvent(QtWindows::WindowsEventType et, } if (nextActiveWindow != d->m_lastActiveWindow) { d->m_lastActiveWindow = nextActiveWindow; - QWindowSystemInterface::handleWindowActivated(nextActiveWindow, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(nextActiveWindow, Qt::ActiveWindowFocusReason); } } @@ -1436,7 +1428,7 @@ bool QWindowsContext::handleContextMenuEvent(QWindow *window, const MSG &msg) } QWindowSystemInterface::handleContextMenuEvent(window, mouseTriggered, pos, globalPos, - QWindowsKeyMapper::queryKeyboardModifiers()); + keyMapper()->queryKeyboardModifiers()); return true; } #endif @@ -1457,7 +1449,7 @@ void QWindowsContext::handleExitSizeMove(QWindow *window) const Qt::MouseButtons appButtons = QGuiApplication::mouseButtons(); if (currentButtons == appButtons) return; - const Qt::KeyboardModifiers keyboardModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const Qt::KeyboardModifiers keyboardModifiers = keyMapper()->queryKeyboardModifiers(); const QPoint globalPos = QWindowsCursor::mousePosition(); const QPlatformWindow *platWin = window->handle(); const QPoint localPos = platWin->mapFromGlobal(globalPos); @@ -1466,8 +1458,7 @@ void QWindowsContext::handleExitSizeMove(QWindow *window) for (Qt::MouseButton button : {Qt::LeftButton, Qt::RightButton, Qt::MiddleButton}) { if (appButtons.testFlag(button) && !currentButtons.testFlag(button)) { QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, - currentButtons, button, type, - keyboardModifiers); + currentButtons, button, type, keyboardModifiers); } } if (d->m_systemInfo & QWindowsContext::SI_SupportsPointer) @@ -1561,7 +1552,7 @@ extern "C" LRESULT QT_WIN_CALLBACK qWindowsWndProc(HWND hwnd, UINT message, WPAR marginsFromRects(ncCalcSizeFrame, rectFromNcCalcSize(message, wParam, lParam, 0)); if (margins.left() >= 0) { if (platformWindow) { - qCDebug(lcQpaWindows) << __FUNCTION__ << "WM_NCCALCSIZE for" << hwnd << margins; + qCDebug(lcQpaWindow) << __FUNCTION__ << "WM_NCCALCSIZE for" << hwnd << margins; platformWindow->setFullFrameMargins(margins); } else { const QSharedPointer<QWindowCreationContext> ctx = QWindowsContext::instance()->windowCreationContext(); diff --git a/src/plugins/platforms/windows/qwindowscontext.h b/src/plugins/platforms/windows/qwindowscontext.h index af92690b3b..0539a22afc 100644 --- a/src/plugins/platforms/windows/qwindowscontext.h +++ b/src/plugins/platforms/windows/qwindowscontext.h @@ -15,12 +15,9 @@ #include <shlobj.h> #include <shlwapi.h> -struct IBindCtx; -struct _SHSTOCKICONINFO; - QT_BEGIN_NAMESPACE -Q_DECLARE_LOGGING_CATEGORY(lcQpaWindows) +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow) Q_DECLARE_LOGGING_CATEGORY(lcQpaEvents) Q_DECLARE_LOGGING_CATEGORY(lcQpaGl) Q_DECLARE_LOGGING_CATEGORY(lcQpaMime) @@ -36,17 +33,17 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen) class QWindow; class QPlatformScreen; class QPlatformWindow; +class QPlatformKeyMapper; class QWindowsMenuBar; class QWindowsScreenManager; class QWindowsTabletSupport; class QWindowsWindow; -class QWindowsMimeConverter; +class QWindowsMimeRegistry; struct QWindowCreationContext; struct QWindowsContextPrivate; class QPoint; class QKeyEvent; class QPointingDevice; - class QWindowsContext { Q_DISABLE_COPY_MOVE(QWindowsContext) @@ -91,8 +88,6 @@ public: static QWindowsContext *instance(); - static QString windowsErrorMessage(unsigned long errorCode); - void addWindow(HWND, QWindowsWindow *w); void removeWindow(HWND); @@ -120,11 +115,10 @@ public: QSharedPointer<QWindowCreationContext> windowCreationContext() const; static void setTabletAbsoluteRange(int a); - void setProcessDpiAwareness(QtWindows::ProcessDpiAwareness dpiAwareness); - static int processDpiAwareness(); - bool setProcessDpiV2Awareness(); - static bool isDarkMode(); + static bool setProcessDpiAwareness(QtWindows::DpiAwareness dpiAwareness); + static QtWindows::DpiAwareness processDpiAwareness(); + static QtWindows::DpiAwareness windowDpiAwareness(HWND hwnd); void setDetectAltGrModifier(bool a); @@ -132,17 +126,16 @@ public: unsigned systemInfo() const; bool useRTLExtensions() const; - QList<int> possibleKeys(const QKeyEvent *e) const; + QPlatformKeyMapper *keyMapper() const; HandleBaseWindowHash &windows(); static bool isSessionLocked(); - QWindowsMimeConverter &mimeConverter() const; + QWindowsMimeRegistry &mimeConverter() const; QWindowsScreenManager &screenManager(); QWindowsTabletSupport *tabletSupport() const; - static QByteArray comErrorString(HRESULT hr); bool asyncExpose() const; void setAsyncExpose(bool value); diff --git a/src/plugins/platforms/windows/qwindowscursor.cpp b/src/plugins/platforms/windows/qwindowscursor.cpp index a923b48f6c..b416886120 100644 --- a/src/plugins/platforms/windows/qwindowscursor.cpp +++ b/src/plugins/platforms/windows/qwindowscursor.cpp @@ -14,6 +14,7 @@ #include <QtGui/qscreen.h> #include <QtGui/private/qguiapplication_p.h> // getPixmapCursor() #include <QtGui/private/qhighdpiscaling_p.h> +#include <QtGui/private/qpixmap_win_p.h> #include <QtCore/private/qwinregistry_p.h> #include <QtCore/qdebug.h> @@ -29,8 +30,7 @@ static bool initResources() QT_BEGIN_NAMESPACE -Q_GUI_EXPORT HBITMAP qt_pixmapToWinHBITMAP(const QPixmap &p, int hbitmapFormat = 0); -Q_GUI_EXPORT HBITMAP qt_createIconMask(const QBitmap &bitmap); +using namespace Qt::Literals::StringLiterals; /*! \class QWindowsCursorCacheKey @@ -43,10 +43,10 @@ QWindowsPixmapCursorCacheKey::QWindowsPixmapCursorCacheKey(const QCursor &c) : bitmapCacheKey(c.pixmap().cacheKey()), maskCacheKey(0) { if (!bitmapCacheKey) { - Q_ASSERT(!c.bitmap(Qt::ReturnByValue).isNull()); - Q_ASSERT(!c.mask(Qt::ReturnByValue).isNull()); - bitmapCacheKey = c.bitmap(Qt::ReturnByValue).cacheKey(); - maskCacheKey = c.mask(Qt::ReturnByValue).cacheKey(); + Q_ASSERT(!c.bitmap().isNull()); + Q_ASSERT(!c.mask().isNull()); + bitmapCacheKey = c.bitmap().cacheKey(); + maskCacheKey = c.mask().cacheKey(); } } @@ -105,14 +105,16 @@ static HCURSOR createBitmapCursor(const QImage &bbits, const QImage &mbits, hotSpot.setX(width / 2); if (hotSpot.y() < 0) hotSpot.setY(height / 2); - const int n = qMax(1, width / 8); - QScopedArrayPointer<uchar> xBits(new uchar[height * n]); - QScopedArrayPointer<uchar> xMask(new uchar[height * n]); + // a ddb is word aligned, QImage depends on bow it was created + const auto bplDdb = qMax(1, ((width + 15) >> 4) << 1); + const auto bplImg = int(bbits.bytesPerLine()); + QScopedArrayPointer<uchar> xBits(new uchar[height * bplDdb]); + QScopedArrayPointer<uchar> xMask(new uchar[height * bplDdb]); int x = 0; for (int i = 0; i < height; ++i) { const uchar *bits = bbits.constScanLine(i); const uchar *mask = mbits.constScanLine(i); - for (int j = 0; j < n; ++j) { + for (int j = 0; j < bplImg && j < bplDdb; ++j) { uchar b = bits[j]; uchar m = mask[j]; if (invb) @@ -123,6 +125,11 @@ static HCURSOR createBitmapCursor(const QImage &bbits, const QImage &mbits, xMask[x] = b ^ m; ++x; } + for (int i = bplImg; i < bplDdb; ++i) { + xBits[x] = 0; + xMask[x] = 0; + ++x; + } } return CreateCursor(GetModuleHandle(nullptr), hotSpot.x(), hotSpot.y(), width, height, xBits.data(), xMask.data()); @@ -131,17 +138,17 @@ static HCURSOR createBitmapCursor(const QImage &bbits, const QImage &mbits, // Create a cursor from image and mask of the format QImage::Format_Mono. static HCURSOR createBitmapCursor(const QCursor &cursor, qreal scaleFactor = 1) { - Q_ASSERT(cursor.shape() == Qt::BitmapCursor && !cursor.bitmap(Qt::ReturnByValue).isNull()); - QImage bbits = cursor.bitmap(Qt::ReturnByValue).toImage(); - QImage mbits = cursor.mask(Qt::ReturnByValue).toImage(); + Q_ASSERT(cursor.shape() == Qt::BitmapCursor && !cursor.bitmap().isNull()); + QImage bbits = cursor.bitmap().toImage(); + QImage mbits = cursor.mask().toImage(); scaleFactor /= bbits.devicePixelRatio(); if (!qFuzzyCompare(scaleFactor, 1)) { const QSize scaledSize = (QSizeF(bbits.size()) * scaleFactor).toSize(); bbits = bbits.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); mbits = mbits.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } - bbits = bbits.convertToFormat(QImage::Format_Mono); - mbits = mbits.convertToFormat(QImage::Format_Mono); + bbits = std::move(bbits).convertToFormat(QImage::Format_Mono); + mbits = std::move(mbits).convertToFormat(QImage::Format_Mono); const bool invb = bbits.colorCount() > 1 && qGray(bbits.color(0)) < qGray(bbits.color(1)); const bool invm = mbits.colorCount() > 1 && qGray(mbits.color(0)) < qGray(mbits.color(1)); return createBitmapCursor(bbits, mbits, cursor.hotSpot(), invb, invm); @@ -438,8 +445,8 @@ QWindowsCursor::PixmapCursor QWindowsCursor::customCursor(Qt::CursorShape cursor if (!bestFit) return PixmapCursor(); - const QPixmap rawImage(QStringLiteral(":/qt-project.org/windows/cursors/images/") + - QString::fromLatin1(bestFit->fileName)); + const QPixmap rawImage(":/qt-project.org/windows/cursors/images/"_L1 + + QLatin1StringView(bestFit->fileName)); return PixmapCursor(rawImage, QPoint(bestFit->hotSpotX, bestFit->hotSpotY)); } #endif // !QT_NO_IMAGEFORMAT_PNG diff --git a/src/plugins/platforms/windows/qwindowsdialoghelpers.cpp b/src/plugins/platforms/windows/qwindowsdialoghelpers.cpp index d851964b9d..0ce0b0e2a7 100644 --- a/src/plugins/platforms/windows/qwindowsdialoghelpers.cpp +++ b/src/plugins/platforms/windows/qwindowsdialoghelpers.cpp @@ -3,12 +3,7 @@ #define QT_NO_URL_CAST_FROM_STRING 1 -#ifndef _WIN32_WINNT -#define _WIN32_WINNT 0x0A00 -#endif - #include <QtCore/qt_windows.h> -#include "qwindowscombase.h" #include "qwindowsdialoghelpers.h" #include "qwindowscontext.h" @@ -35,6 +30,9 @@ #include <QtCore/qmutex.h> #include <QtCore/quuid.h> #include <QtCore/qtemporaryfile.h> +#include <QtCore/private/qfunctions_win_p.h> +#include <QtCore/private/qsystemerror_p.h> +#include <QtCore/private/qcomobject_p.h> #include <algorithm> #include <vector> @@ -45,34 +43,6 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -#ifndef QT_NO_DEBUG_STREAM -/* Output UID (IID, CLSID) as C++ constants. - * The constants are contained in the Windows SDK libs, but not for MinGW. */ -static inline QString guidToString(const GUID &g) -{ - QString rc; - QTextStream str(&rc); - str.setIntegerBase(16); - str.setNumberFlags(str.numberFlags() | QTextStream::ShowBase); - str << '{' << g.Data1 << ", " << g.Data2 << ", " << g.Data3; - str.setFieldWidth(2); - str.setFieldAlignment(QTextStream::AlignRight); - str.setPadChar(u'0'); - str << ",{" << g.Data4[0] << ", " << g.Data4[1] << ", " << g.Data4[2] << ", " << g.Data4[3] - << ", " << g.Data4[4] << ", " << g.Data4[5] << ", " << g.Data4[6] << ", " << g.Data4[7] - << "}};"; - return rc; -} - -inline QDebug operator<<(QDebug d, const GUID &g) -{ - QDebugStateSaver saver(d); - d.nospace(); - d << guidToString(g); - return d; -} -#endif // !QT_NO_DEBUG_STREAM - // Return an allocated wchar_t array from a QString, reserve more memory if desired. static wchar_t *qStringToWCharArray(const QString &s, size_t reserveSize = 0) { @@ -239,7 +209,7 @@ QWindowsNativeDialogBase *QWindowsDialogHelperBase<BaseClass>::ensureNativeDialo // Create dialog and apply common settings. Check "executed" flag as well // since for example IFileDialog::Show() works only once. if (m_nativeDialog.isNull() || m_nativeDialog->executed()) - m_nativeDialog = QWindowsNativeDialogBasePtr(createNativeDialog()); + m_nativeDialog = QWindowsNativeDialogBasePtr(createNativeDialog(), &QObject::deleteLater); return m_nativeDialog.data(); } @@ -268,6 +238,7 @@ private: void QWindowsDialogThread::run() { qCDebug(lcQpaDialogs) << '>' << __FUNCTION__; + QComHelper comInit(COINIT_APARTMENTTHREADED); m_dialog->exec(m_owner); qCDebug(lcQpaDialogs) << '<' << __FUNCTION__; } @@ -324,12 +295,13 @@ void QWindowsDialogHelperBase<BaseClass>::stopTimer() } } - template <class BaseClass> void QWindowsDialogHelperBase<BaseClass>::hide() { - if (m_nativeDialog) + if (m_nativeDialog) { m_nativeDialog->close(); + m_nativeDialog.clear(); + } m_ownerWindow = nullptr; } @@ -451,7 +423,7 @@ inline void QWindowsFileDialogSharedData::fromOptions(const QSharedPointer<QFile class QWindowsNativeFileDialogBase; -class QWindowsNativeFileDialogEventHandler : public QWindowsComBase<IFileDialogEvents> +class QWindowsNativeFileDialogEventHandler : public QComObject<IFileDialogEvents> { Q_DISABLE_COPY_MOVE(QWindowsNativeFileDialogEventHandler) public: @@ -544,8 +516,18 @@ QWindowsShellItem::QWindowsShellItem(IShellItem *item) : m_item(item) , m_attributes(0) { - if (FAILED(item->GetAttributes(SFGAO_CAPABILITYMASK | SFGAO_DISPLAYATTRMASK | SFGAO_CONTENTSMASK | SFGAO_STORAGECAPMASK, &m_attributes))) + SFGAOF mask = (SFGAO_CAPABILITYMASK | SFGAO_CONTENTSMASK | SFGAO_STORAGECAPMASK); + + // Check for attributes which might be expensive to enumerate for subfolders + if (FAILED(item->GetAttributes((SFGAO_STREAM | SFGAO_COMPRESSED), &m_attributes))) { m_attributes = 0; + } else { + // If the item is compressed or stream, skip expensive subfolder test + if (m_attributes & (SFGAO_STREAM | SFGAO_COMPRESSED)) + mask &= ~SFGAO_HASSUBFOLDER; + if (FAILED(item->GetAttributes(mask, &m_attributes))) + m_attributes = 0; + } } QString QWindowsShellItem::path() const @@ -584,7 +566,7 @@ QUrl QWindowsShellItem::url() const if (urlV.isValid()) return urlV; // Last resort: encode the absolute desktop parsing id as data URL - const QString data = QStringLiteral("data:text/plain;base64,") + const QString data = "data:text/plain;base64,"_L1 + QLatin1StringView(desktopAbsoluteParsing().toLatin1().toBase64()); return QUrl(data); } @@ -625,7 +607,7 @@ bool QWindowsShellItem::copyData(QIODevice *out, QString *errorMessage) HRESULT hr = m_item->BindToHandler(nullptr, BHID_Stream, IID_PPV_ARGS(&istream)); if (FAILED(hr)) { *errorMessage = "BindToHandler() failed: "_L1 - + QLatin1StringView(QWindowsContext::comErrorString(hr)); + + QSystemError::windowsComString(hr); return false; } enum : ULONG { bufSize = 102400 }; @@ -642,7 +624,7 @@ bool QWindowsShellItem::copyData(QIODevice *out, QString *errorMessage) istream->Release(); if (hr != S_OK && hr != S_FALSE) { *errorMessage = "Read() failed: "_L1 - + QLatin1StringView(QWindowsContext::comErrorString(hr)); + + QSystemError::windowsComString(hr); return false; } return true; @@ -1355,7 +1337,7 @@ Q_GLOBAL_STATIC(QStringList, temporaryItemCopies) static void cleanupTemporaryItemCopies() { - for (const QString &file : qAsConst(*temporaryItemCopies())) + for (const QString &file : std::as_const(*temporaryItemCopies())) QFile::remove(file); } @@ -1377,7 +1359,7 @@ QString tempFilePattern(QString name) int lastDot = name.lastIndexOf(u'.'); if (lastDot < 0) lastDot = name.size(); - name.insert(lastDot, QStringLiteral("_XXXXXX")); + name.insert(lastDot, "_XXXXXX"_L1); for (int i = lastDot - 1; i >= 0; --i) { if (!validFileNameCharacter(name.at(i))) @@ -1569,7 +1551,7 @@ QWindowsNativeDialogBase *QWindowsFileDialogHelper::createNativeDialog() if (!info.isDir()) result->selectFile(info.fileName()); } else { - result->selectFile(url.path()); // TODO url.fileName() once it exists + result->selectFile(url.fileName()); } } // No need to select initialNameFilter if mode is Dir @@ -1603,7 +1585,7 @@ void QWindowsFileDialogHelper::selectFile(const QUrl &fileName) qCDebug(lcQpaDialogs) << __FUNCTION__ << fileName.toString(); if (hasNativeDialog()) // Might be invoked from the QFileDialog constructor. - nativeFileDialog()->selectFile(fileName.toLocalFile()); // ## should use QUrl::fileName() once it exists + nativeFileDialog()->selectFile(fileName.fileName()); } QList<QUrl> QWindowsFileDialogHelper::selectedFiles() const @@ -1711,14 +1693,6 @@ static int QT_WIN_CALLBACK xpFileDialogGetExistingDirCallbackProc(HWND hwnd, UIN return dialog->existingDirCallback(hwnd, uMsg, lParam); } -/* The correct declaration of the SHGetPathFromIDList symbol is - * being used in mingw-w64 as of r6215, which is a v3 snapshot. */ -#if defined(Q_CC_MINGW) && (!defined(__MINGW64_VERSION_MAJOR) || __MINGW64_VERSION_MAJOR < 3) -typedef ITEMIDLIST *qt_LpItemIdList; -#else -using qt_LpItemIdList = PIDLIST_ABSOLUTE; -#endif - int QWindowsXpNativeFileDialog::existingDirCallback(HWND hwnd, UINT uMsg, LPARAM lParam) { switch (uMsg) { @@ -1732,7 +1706,7 @@ int QWindowsXpNativeFileDialog::existingDirCallback(HWND hwnd, UINT uMsg, LPARAM break; case BFFM_SELCHANGED: { wchar_t path[MAX_PATH]; - const bool ok = SHGetPathFromIDList(reinterpret_cast<qt_LpItemIdList>(lParam), path) + const bool ok = SHGetPathFromIDList(reinterpret_cast<PIDLIST_ABSOLUTE>(lParam), path) && path[0]; SendMessage(hwnd, BFFM_ENABLEOK, ok ? 1 : 0, 1); } @@ -1754,7 +1728,7 @@ QList<QUrl> QWindowsXpNativeFileDialog::execExistingDir(HWND owner) bi.lpfn = xpFileDialogGetExistingDirCallbackProc; bi.lParam = LPARAM(this); QList<QUrl> selectedFiles; - if (qt_LpItemIdList pItemIDList = SHBrowseForFolder(&bi)) { + if (const auto pItemIDList = SHBrowseForFolder(&bi)) { wchar_t path[MAX_PATH]; path[0] = 0; if (SHGetPathFromIDList(pItemIDList, path) && path[0]) diff --git a/src/plugins/platforms/windows/qwindowsdrag.cpp b/src/plugins/platforms/windows/qwindowsdrag.cpp index 33b1cf2abd..c6f55c3509 100644 --- a/src/plugins/platforms/windows/qwindowsdrag.cpp +++ b/src/plugins/platforms/windows/qwindowsdrag.cpp @@ -28,6 +28,8 @@ #include <QtCore/qdebug.h> #include <QtCore/qbuffer.h> #include <QtCore/qpoint.h> +#include <QtCore/qpointer.h> +#include <QtCore/private/qcomobject_p.h> #include <shlobj.h> @@ -167,7 +169,7 @@ static Qt::MouseButtons lastButtons = Qt::NoButton; \internal */ -class QWindowsOleDropSource : public QWindowsComBase<IDropSource> +class QWindowsOleDropSource : public QComObject<IDropSource> { public: enum Mode { @@ -348,7 +350,7 @@ QWindowsOleDropSource::QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState) } else { if (buttons && !m_currentButtons) { m_currentButtons = buttons; - } else if (!(m_currentButtons & buttons)) { // Button changed: Complete Drop operation. + } else if (m_currentButtons != buttons) { // Button changed: Complete Drop operation. result = DRAGDROP_S_DROP; } } @@ -526,7 +528,8 @@ QWindowsOleDropTarget::DragLeave() qCDebug(lcQpaMime) << __FUNCTION__ << ' ' << m_window; - lastModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + lastModifiers = keyMapper->queryKeyboardModifiers(); lastButtons = QWindowsMouseHandler::queryMouseButtons(); QWindowSystemInterface::handleDrag(m_window, nullptr, QPoint(), Qt::IgnoreAction, @@ -611,7 +614,6 @@ QWindowsOleDropTarget::Drop(LPDATAOBJECT pDataObj, DWORD grfKeyState, */ bool QWindowsDrag::m_canceled = false; -bool QWindowsDrag::m_dragging = false; QWindowsDrag::QWindowsDrag() = default; @@ -644,6 +646,86 @@ IDropTargetHelper* QWindowsDrag::dropHelper() { return m_cachedDropTargetHelper; } +// Workaround for DoDragDrop() not working with touch/pen input, causing DnD to hang until the mouse is moved. +// We process pointer messages for touch/pen and generate mouse input through SendInput() to trigger DoDragDrop() +static HRESULT startDoDragDrop(LPDATAOBJECT pDataObj, LPDROPSOURCE pDropSource, DWORD dwOKEffects, LPDWORD pdwEffect) +{ + QWindow *underMouse = QWindowsContext::instance()->windowUnderMouse(); + const bool hasMouseCapture = underMouse && static_cast<QWindowsWindow *>(underMouse->handle())->hasMouseCapture(); + const HWND hwnd = hasMouseCapture ? reinterpret_cast<HWND>(underMouse->winId()) : ::GetFocus(); + bool starting = false; + + for (;;) { + MSG msg{}; + if (::GetMessage(&msg, hwnd, 0, 0) > 0) { + + if (msg.message == WM_MOUSEMOVE) { + + // Only consider the first simulated event, or actual mouse messages. + if (!starting && (msg.wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON | MK_XBUTTON1 | MK_XBUTTON2)) == 0) + return E_FAIL; + + return ::DoDragDrop(pDataObj, pDropSource, dwOKEffects, pdwEffect); + } + + if (msg.message == WM_POINTERUPDATE) { + + const quint32 pointerId = GET_POINTERID_WPARAM(msg.wParam); + + POINTER_INFO pointerInfo{}; + if (!GetPointerInfo(pointerId, &pointerInfo)) + return E_FAIL; + + if (pointerInfo.pointerFlags & POINTER_FLAG_PRIMARY) { + + DWORD flags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK | MOUSEEVENTF_MOVE; + if (IS_POINTER_FIRSTBUTTON_WPARAM(msg.wParam)) + flags |= MOUSEEVENTF_LEFTDOWN; + if (IS_POINTER_SECONDBUTTON_WPARAM(msg.wParam)) + flags |= MOUSEEVENTF_RIGHTDOWN; + if (IS_POINTER_THIRDBUTTON_WPARAM(msg.wParam)) + flags |= MOUSEEVENTF_MIDDLEDOWN; + + if (!starting) { + POINT pt{}; + if (::GetCursorPos(&pt)) { + + // Send mouse input that can generate a WM_MOUSEMOVE message. + if ((flags & MOUSEEVENTF_LEFTDOWN || flags & MOUSEEVENTF_RIGHTDOWN || flags & MOUSEEVENTF_MIDDLEDOWN) + && (pt.x != pointerInfo.ptPixelLocation.x || pt.y != pointerInfo.ptPixelLocation.y)) { + + const int origin_x = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + const int origin_y = ::GetSystemMetrics(SM_YVIRTUALSCREEN); + const int virt_w = ::GetSystemMetrics(SM_CXVIRTUALSCREEN); + const int virt_h = ::GetSystemMetrics(SM_CYVIRTUALSCREEN); + const int virt_x = pointerInfo.ptPixelLocation.x - origin_x; + const int virt_y = pointerInfo.ptPixelLocation.y - origin_y; + + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dx = static_cast<DWORD>(virt_x * (65535.0 / virt_w)); + input.mi.dy = static_cast<DWORD>(virt_y * (65535.0 / virt_h)); + input.mi.dwFlags = flags; + + ::SendInput(1, &input, sizeof(input)); + starting = true; + } + } + } + } + } else { + // Handle other messages. + qWindowsWndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam); + + if (msg.message == WM_POINTERLEAVE) + return E_FAIL; + } + } else { + return E_FAIL; + } + } +} + Qt::DropAction QWindowsDrag::drag(QDrag *drag) { // TODO: Accessibility handling? @@ -659,10 +741,7 @@ Qt::DropAction QWindowsDrag::drag(QDrag *drag) const DWORD allowedEffects = translateToWinDragEffects(possibleActions); qCDebug(lcQpaMime) << '>' << __FUNCTION__ << "possible Actions=0x" << Qt::hex << int(possibleActions) << "effects=0x" << allowedEffects << Qt::dec; - // Indicate message handlers we are in DoDragDrop() event loop. - QWindowsDrag::m_dragging = true; - const HRESULT r = DoDragDrop(dropDataObject, windowDropSource, allowedEffects, &resultEffect); - QWindowsDrag::m_dragging = false; + const HRESULT r = startDoDragDrop(dropDataObject, windowDropSource, allowedEffects, &resultEffect); const DWORD reportedPerformedEffect = dropDataObject->reportedPerformedEffect(); if (r == DRAGDROP_S_DROP) { if (reportedPerformedEffect == DROPEFFECT_MOVE && resultEffect != DROPEFFECT_MOVE) { diff --git a/src/plugins/platforms/windows/qwindowsdrag.h b/src/plugins/platforms/windows/qwindowsdrag.h index cf5d43fb84..a2d0e54044 100644 --- a/src/plugins/platforms/windows/qwindowsdrag.h +++ b/src/plugins/platforms/windows/qwindowsdrag.h @@ -4,12 +4,12 @@ #ifndef QWINDOWSDRAG_H #define QWINDOWSDRAG_H -#include "qwindowscombase.h" #include "qwindowsinternalmimedata.h" #include <qpa/qplatformdrag.h> #include <QtGui/qpixmap.h> #include <QtGui/qdrag.h> +#include <QtCore/private/qcomobject_p.h> struct IDropTargetHelper; @@ -23,7 +23,7 @@ public: IDataObject *retrieveDataObject() const override; }; -class QWindowsOleDropTarget : public QWindowsComBase<IDropTarget> +class QWindowsOleDropTarget : public QComObject<IDropTarget> { public: explicit QWindowsOleDropTarget(QWindow *w); @@ -58,7 +58,6 @@ public: static QWindowsDrag *instance(); void cancelDrag() override { QWindowsDrag::m_canceled = true; } static bool isCanceled() { return QWindowsDrag::m_canceled; } - static bool isDragging() { return QWindowsDrag::m_dragging; } IDataObject *dropDataObject() const { return m_dropDataObject; } void setDropDataObject(IDataObject *dataObject) { m_dropDataObject = dataObject; } @@ -69,7 +68,6 @@ public: private: static bool m_canceled; - static bool m_dragging; QWindowsDropMimeData m_dropData; IDataObject *m_dropDataObject = nullptr; diff --git a/src/plugins/platforms/windows/qwindowsdropdataobject.cpp b/src/plugins/platforms/windows/qwindowsdropdataobject.cpp index 4f5e1f5b85..629291640f 100644 --- a/src/plugins/platforms/windows/qwindowsdropdataobject.cpp +++ b/src/plugins/platforms/windows/qwindowsdropdataobject.cpp @@ -5,10 +5,12 @@ #include <QtCore/qurl.h> #include <QtCore/qmimedata.h> -#include "qwindowsmime.h" +#include "qwindowsmimeregistry.h" QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + /*! \class QWindowsDropDataObject \brief QWindowsOleDataObject subclass specialized for handling Drag&Drop. @@ -53,11 +55,11 @@ bool QWindowsDropDataObject::shouldIgnore(LPFORMATETC pformatetc) const QMimeData *dropData = mimeData(); if (dropData && dropData->formats().size() == 1 && dropData->hasUrls()) { - QString formatName = QWindowsMimeConverter::clipboardFormatName(pformatetc->cfFormat); + QString formatName = QWindowsMimeRegistry::clipboardFormatName(pformatetc->cfFormat); if (pformatetc->cfFormat == CF_UNICODETEXT || pformatetc->cfFormat == CF_TEXT - || formatName == QStringLiteral("UniformResourceLocator") - || formatName == QStringLiteral("UniformResourceLocatorW")) { + || formatName == "UniformResourceLocator"_L1 + || formatName == "UniformResourceLocatorW"_L1) { const auto urls = dropData->urls(); return std::all_of(urls.cbegin(), urls.cend(), [] (const QUrl &u) { return u.isLocalFile(); }); } diff --git a/src/plugins/platforms/windows/qwindowsglcontext.cpp b/src/plugins/platforms/windows/qwindowsglcontext.cpp index 65740d69da..5ca52c2c19 100644 --- a/src/plugins/platforms/windows/qwindowsglcontext.cpp +++ b/src/plugins/platforms/windows/qwindowsglcontext.cpp @@ -544,11 +544,14 @@ static int choosePixelFormat(HDC hdc, iAttributes[i++] = WGL_NUMBER_OVERLAYS_ARB; iAttributes[i++] = 1; } + // must be the one before the last one const int samples = format.samples(); const bool sampleBuffersRequested = samples > 1 && testFlag(staticContext.extensions, QOpenGLStaticContext::SampleBuffers); + int sampleBuffersKeyPosition = 0; int samplesValuePosition = 0; if (sampleBuffersRequested) { + sampleBuffersKeyPosition = i; iAttributes[i++] = WGL_SAMPLE_BUFFERS_ARB; iAttributes[i++] = TRUE; iAttributes[i++] = WGL_SAMPLES_ARB; @@ -560,9 +563,9 @@ static int choosePixelFormat(HDC hdc, } // must be the last bool srgbRequested = format.colorSpace() == QColorSpace::SRgb; - int srgbValuePosition = 0; + int srgbCapableKeyPosition = 0; if (srgbRequested) { - srgbValuePosition = i; + srgbCapableKeyPosition = i; iAttributes[i++] = WGL_FRAMEBUFFER_SRGB_CAPABLE_EXT; iAttributes[i++] = TRUE; } @@ -576,8 +579,9 @@ static int choosePixelFormat(HDC hdc, && numFormats >= 1; if (valid || (!sampleBuffersRequested && !srgbRequested)) break; + // NB reductions must be done in reverse order (disable the last first, then move on to the one before that, etc.) if (srgbRequested) { - iAttributes[srgbValuePosition] = 0; + iAttributes[srgbCapableKeyPosition] = 0; srgbRequested = false; } else if (sampleBuffersRequested) { if (iAttributes[samplesValuePosition] > 1) { @@ -585,11 +589,8 @@ static int choosePixelFormat(HDC hdc, } else if (iAttributes[samplesValuePosition] == 1) { // Fallback in case it is unable to initialize with any // samples to avoid falling back to the GDI path - // NB: The sample attributes needs to be at the end for this - // to work correctly - iAttributes[samplesValuePosition - 1] = FALSE; + iAttributes[sampleBuffersKeyPosition] = 0; iAttributes[samplesValuePosition] = 0; - iAttributes[samplesValuePosition + 1] = 0; } else { break; } @@ -1261,7 +1262,6 @@ bool QWindowsGLContext::makeCurrent(QPlatformSurface *surface) // Do we already have a DC entry for that window? auto *window = static_cast<QWindowsWindow *>(surface); - window->aboutToMakeCurrent(); const HWND hwnd = window->handle(); if (const QOpenGLContextData *contextData = findByHWND(m_windowContexts, hwnd)) { // Repeated calls to wglMakeCurrent when vsync is enabled in the driver will diff --git a/src/plugins/platforms/windows/qwindowsiconengine.cpp b/src/plugins/platforms/windows/qwindowsiconengine.cpp new file mode 100644 index 0000000000..5e5ca22ec1 --- /dev/null +++ b/src/plugins/platforms/windows/qwindowsiconengine.cpp @@ -0,0 +1,394 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsiconengine.h" + +#include <QtCore/qoperatingsystemversion.h> +#include <QtGui/qguiapplication.h> +#include <QtGui/qpainter.h> +#include <QtGui/qpalette.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QString QWindowsIconEngine::glyphs() const +{ + if (!QFontInfo(m_iconFont).exactMatch()) + return {}; + + static constexpr std::pair<QLatin1StringView, QStringView> glyphMap[] = { + {"address-book-new"_L1, u"\ue780"}, + {"application-exit"_L1, u"\ue8bb"}, + {"appointment-new"_L1, u"\ue878"}, + {"call-start"_L1, u"\uf715"}, + {"call-stop"_L1, u"\uf405"}, + {"contact-new"_L1, u"\ue8fa"}, + {"document-new"_L1, u"\ue8a5"}, + {"document-open"_L1, u"\ue8e5"}, + {"document-open-recent"_L1, u"\ue823"}, + {"document-page-setup"_L1, u"\ue7c3"}, + {"document-print"_L1, u"\ue749"}, + {"document-print-preview"_L1, u"\ue956"}, + {"document-properties"_L1, u"\ue90f"}, + {"document-revert"_L1, u"\ue7a7"}, // ? + {"document-save"_L1, u"\ue74e"}, // or e78c? + {"document-save-as"_L1, u"\ue792"}, + {"document-send"_L1, u"\ue724"}, + {"edit-clear"_L1, u"\ue894"}, + {"edit-copy"_L1, u"\ue8c8"}, + {"edit-cut"_L1, u"\ue8c6"}, + {"edit-delete"_L1, u"\ue74d"}, + {"edit-find"_L1, u"\ue721"}, + //{"edit-find-replace"_L1, u"\u"}, + {"edit-paste"_L1, u"\ue77f"}, + {"edit-redo"_L1, u"\ue7a6"}, + {"edit-select-all"_L1, u"\ue8b3"}, + {"edit-undo"_L1, u"\ue7a7"}, + {"folder-new"_L1, u"\ue8f4"}, + //{"format-indent-less"_L1, u"\u"}, + //{"format-indent-more"_L1, u"\u"}, + {"format-justify-center"_L1, u"\ue8e3"}, + //{"format-justify-fill"_L1, u"\ue235"}, + {"format-justify-left"_L1, u"\ue8e4"}, + {"format-justify-right"_L1, u"\ue8e2"}, + //{"format-text-direction-ltr"_L1, u"\ue247"}, + //{"format-text-direction-rtl"_L1, u"\ue248"}, + {"format-text-bold"_L1, u"\ue8dd"}, + {"format-text-italic"_L1, u"\ue8db"}, + {"format-text-underline"_L1, u"\ue8dc"}, + {"format-text-strikethrough"_L1, u"\uede0"}, + //{"go-bottom"_L1,u"\ue258"}, + {"go-down"_L1,u"\ue74b"}, + //{"go-first"_L1, u"\ue5dc"}, + {"go-home"_L1, u"\ue80f"}, + // {"go-jump"_L1, u"\uf719"}, + //{"go-last"_L1, u"\ue5dd"}, + {"go-next"_L1, u"\ue893"}, + {"go-previous"_L1, u"\ue892"}, + //{"go-top"_L1, u"\ue25a"}, + {"go-up"_L1, u"\ue74a"}, + {"help-about"_L1, u"\ue946"}, + //{"help-contents"_L1, u"\ue8de"}, + {"help-faq"_L1, u"\ue897"}, + {"insert-image"_L1, u"\ue946"}, + {"insert-link"_L1, u"\ue71b"}, + //{"insert-object"_L1, u"\u"}, + //{"insert-text"_L1, u"\uf827"}, + {"list-add"_L1, u"\ue710"}, + {"list-remove"_L1, u"\ue738"}, + {"mail-forward"_L1, u"\ue89c"}, + //{"mail-mark-important"_L1, u"\ue937"}, + //{"mail-mark-junk"_L1, u"\u"}, + //{"mail-mark-notjunk"_L1, u"\u"}, + {"mail-mark-read"_L1, u"\ue8c3"}, + //{"mail-mark-unread"_L1, u"\ue9bc"}, + {"mail-message-new"_L1, u"\ue70f"}, + {"mail-reply-all"_L1, u"\ue8c2"}, + {"mail-reply-sender"_L1, u"\ue8ca"}, + {"mail-send"_L1, u"\ue724"}, + //{"mail-send-receive"_L1, u"\u"}, + {"media-eject"_L1, u"\uf847"}, + {"media-playback-pause"_L1, u"\ue769"}, + {"media-playback-start"_L1, u"\ue768"}, + {"media-playback-stop"_L1, u"\ue71a"}, + {"media-record"_L1, u"\ue7c8"}, + {"media-seek-backward"_L1, u"\ueb9e"}, + {"media-seek-forward"_L1, u"\ueb9d"}, + {"media-skip-backward"_L1, u"\ue892"}, + {"media-skip-forward"_L1, u"\ue893"}, + //{"object-flip-horizontal"_L1, u"\u"}, + //{"object-flip-vertical"_L1, u"\u"}, + {"object-rotate-left"_L1, u"\ue80c"}, + {"object-rotate-right"_L1, u"\ue80d"}, + //{"process-stop"_L1, u"\ue5c9"}, + {"system-lock-screen"_L1, u"\uee3f"}, + {"system-log-out"_L1, u"\uf3b1"}, + //{"system-run"_L1, u"\u"}, + {"system-search"_L1, u"\ue721"}, + {"system-reboot"_L1, u"\ue777"}, // unsure? + {"system-shutdown"_L1, u"\ue7e8"}, + {"tools-check-spelling"_L1, u"\uf87b"}, + {"view-fullscreen"_L1, u"\ue740"}, + {"view-refresh"_L1, u"\ue72c"}, + {"view-restore"_L1, u"\ue777"}, + //{"view-sort-ascending"_L1, u"\ue25a"}, + //{"view-sort-descending"_L1, u"\ue258"}, + {"window-close"_L1, u"\ue8bb"}, + {"window-new"_L1, u"\ue78b"}, + {"zoom-fit-best"_L1, u"\ue9a6"}, + {"zoom-in"_L1, u"\ue8a3"}, + //{"zoom-original"_L1, u"\u"}, + {"zoom-out"_L1, u"\ue71f"}, + + {"process-working"_L1, u"\ue9f3"}, + + {"accessories-calculator"_L1, u"\ue8ef"}, + {"accessories-character-map"_L1, u"\uf2b7"}, + {"accessories-dictionary"_L1, u"\ue82d"}, + //{"accessories-text-editor"_L1, u"\ue262"}, + {"help-browser"_L1, u"\ue897"}, + {"multimedia-volume-control"_L1, u"\ue767"}, + {"preferences-desktop-accessibility"_L1, u"\ue776"}, + {"preferences-desktop-font"_L1, u"\ue8d2"}, + {"preferences-desktop-keyboard"_L1, u"\ue765"}, + {"preferences-desktop-locale"_L1, u"\uf2b7"}, + //{"preferences-desktop-multimedia"_L1, u"\uea75"}, + {"preferences-desktop-screensaver"_L1, u"\uf182"}, + //{"preferences-desktop-theme"_L1, u"\uf560"}, + //{"preferences-desktop-wallpaper"_L1, u"\ue1bc"}, + {"system-file-manager"_L1, u"\uec50"}, + //{"system-software-install"_L1, u"\ueb71"}, + {"system-software-update"_L1, u"\uecc5"}, + {"utilities-system-monitor"_L1, u"\ue7f4"}, + //{"utilities-terminal"_L1, u"\ueb8e"}, + + //{"applications-accessories"_L1, u"\u"}, + {"applications-development"_L1, u"\uec7a"}, + //{"applications-engineering"_L1, u"\uea3d"}, + {"applications-games"_L1, u"\ue7fc"}, + //{"applications-graphics"_L1, u"\u"}, + {"applications-internet"_L1, u"\ue774"}, + {"applications-multimedia"_L1, u"\uea69"}, + //{"applications-office"_L1, u"\u"}, + //{"applications-other"_L1, u"\u"}, + //{"applications-science"_L1, u"\uea4b"}, + {"applications-system"_L1, u"\ue770"}, + //{"applications-utilities"_L1, u"\u"}, + //{"preferences-desktop"_L1, u"\ueb97"}, + //{"preferences-desktop-peripherals"_L1, u"\u"}, + //{"preferences-desktop-personal"_L1, u"\uf835"}, + //{"preferences-other"_L1, u"\u"}, + //{"preferences-system"_L1, u"\ue8b8"}, + //{"preferences-system-network"_L1, u"\ue894"}, + {"system-help"_L1, u"\ue946"}, + + {"audio-card"_L1, u"\ue8d6"}, + {"audio-input-microphone"_L1, u"\ue720"}, + {"battery"_L1, u"\ue83f"}, + {"camera-photo"_L1, u"\ue722"}, + {"camera-video"_L1, u"\ue714"}, + {"camera-web"_L1, u"\ue8b8"}, + {"computer"_L1, u"\ue7f8"}, // or e7fb? + {"drive-harddisk"_L1, u"\ueda2"}, + {"drive-optical"_L1, u"\ue958"}, + //{"drive-removable-media"_L1, u"\u"}, + //{"input-gaming"_L1, u"\u"}, + {"input-keyboard"_L1, u"\ue92e"}, + {"input-mouse"_L1, u"\ue962"}, + {"input-tablet"_L1, u"\ue70a"}, + {"media-flash"_L1, u"\ue88e"}, + //{"media-floppy"_L1, u"\u"}, + {"media-optical"_L1, u"\ue958"}, + {"media-tape"_L1, u"\ue96a"}, + //{"modem"_L1, u"\u"}, + //{"multimedia-player"_L1, u"\u"}, + {"network-wired"_L1, u"\ue968"}, + {"network-wireless"_L1, u"\ue701"}, + //{"pda"_L1, u"\u"}, + {"phone"_L1, u"\ue717"}, + {"printer"_L1, u"\ue749"}, + {"scanner"_L1, u"\ue8fe"}, + //{"video-display"_L1, u"\uf06a"}, + + {"emblem-default"_L1, u"\uf56d"}, + {"emblem-documents"_L1, u"\ue8a5"}, + {"emblem-downloads"_L1, u"\ue896"}, + {"emblem-favorite"_L1, u"\ue734"}, + {"emblem-important"_L1, u"\ue8c9"}, + {"emblem-mail"_L1, u"\ue715"}, + {"emblem-photos"_L1, u"\ue91b"}, + //{"emblem-readonly"_L1, u"\u"}, + {"emblem-shared"_L1, u"\ue902"}, + {"emblem-symbolic-link"_L1, u"\ue71b"}, + {"emblem-synchronized"_L1, u"\uedab"}, + {"emblem-system"_L1, u"\ue770"}, + //{"emblem-unreadable"_L1, u"\u"}, + + {"folder"_L1, u"\ue8b7"}, + //{"folder-remote"_L1, u"\u"}, + //{"network-server"_L1, u"\ue875"}, + //{"network-workgroup"_L1, u"\ue1a0"}, + {"start-here"_L1, u"\ue8fc"}, // unsure + {"user-bookmarks"_L1, u"\ue8a4"}, + //{"user-desktop"_L1, u"\ue30a"}, + {"user-home"_L1, u"\ue80f"}, + {"user-trash"_L1, u"\ue74d"}, + + //{"appointment-missed"_L1, u"\ue615"}, + //{"appointment-soon"_L1, u"\uf540"}, + {"audio-volume-high"_L1, u"\ue995"}, + {"audio-volume-low"_L1, u"\ue993"}, + {"audio-volume-medium"_L1, u"\ue994"}, + {"audio-volume-muted"_L1, u"\ue992"}, + //{"battery-caution"_L1, u"\ue19c"}, + {"battery-low"_L1, u"\ue851"}, // ? + {"dialog-error"_L1, u"\ue783"}, + {"dialog-information"_L1, u"\ue946"}, + //{"dialog-password"_L1, u"\uf042"}, + {"dialog-question"_L1, u"\uf142"}, // unsure + {"dialog-warning"_L1, u"\ue7ba"}, + //{"folder-drag-accept"_L1, @u"\ue9a3"}, + {"folder-open"_L1, u"\ue838"}, + //{"folder-visiting"_L1, u"\ue8a7"}, + //{"image-loading"_L1, u"\ue41a"}, + //{"image-missing"_L1, u"\ue3ad"}, + {"mail-attachment"_L1, u"\ue723"}, + //{"mail-unread"_L1, u"\uf18a"}, + //{"mail-read"_L1, u"\uf18c"}, + {"mail-replied"_L1, u"\ue8ca"}, + //{"mail-signed"_L1, u"\u"}, + //{"mail-signed-verified"_L1, u"\u"}, + {"media-playlist-repeat"_L1, u"\ue8ee"}, + {"media-playlist-shuffle"_L1, u"\ue8b1"}, + //{"network-error"_L1, u"\uead9"}, + //{"network-idle"_L1, u"\ue51f"}, + {"network-offline"_L1, u"\uf384"}, + //{"network-receive"_L1, u"\ue2c0"}, + //{"network-transmit"_L1, u"\ue2c3"}, + //{"network-transmit-receive"_L1, u"\uca18"}, + //{"printer-error"_L1, u"\uf7a0"}, + //{"printer-printing"_L1, u"\uf7a1"}, + //{"security-high"_L1, u"\ue32a"}, + //{"security-medium"_L1, u"\ue9e0"}, + //{"security-low"_L1, u"\uf012"}, + //{"software-update-available"_L1, u"\ue923"}, + //{"software-update-urgent"_L1, u"\uf05a"}, + {"sync-error"_L1, u"\uea6a"}, + {"sync-synchronizing"_L1, u"\ue895"}, + //{"task-due"_L1, u"\u"}, + //{"task-past-due"_L1, u"\u"}, + {"user-available"_L1, u"\ue8cf"}, + //{"user-away"_L1, u"\ue510"}, + //{"user-idle"_L1, u"\u"}, + //{"user-offline"_L1, u"\uf7b3"}, + //{"user-trash-full"_L1, u"\ue872"}, //delete + //{"user-trash-full"_L1, u"\ue92b"}, //delete_forever + {"weather-clear"_L1, u"\ue706"}, + //{"weather-clear-night"_L1, u"\uf159"}, + //{"weather-few-clouds"_L1, u"\uf172"}, + //{"weather-few-clouds-night"_L1, u"\uf174"}, + //{"weather-fog"_L1, u"\ue818"}, + {"weather-overcast"_L1, u"\ue753"}, + //{"weather-severe-alert"_L1, u"\ue002"}, //warning + //{"weather-showers"_L1, u"\uf176"}, + //{"weather-showers-scattered"_L1, u"\u"}, + //{"weather-snow"_L1, u"\ue80f"}, //snowing + //{"weather-storm"_L1, u"\uf070"}, + }; + + const auto it = std::find_if(std::begin(glyphMap), std::end(glyphMap), [this](const auto &c){ + return c.first == m_iconName; + }); + + return it != std::end(glyphMap) ? it->second.toString() + : (m_iconName.length() == 1 ? m_iconName : QString()); +} + +namespace { +auto iconFontFamily() +{ + static const bool isWindows11 = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11; + return isWindows11 ? u"Segoe Fluent Icons"_s + : u"Segoe MDL2 Assets"_s; +} +} + +QWindowsIconEngine::QWindowsIconEngine(const QString &iconName) + : m_iconName(iconName), m_iconFont(iconFontFamily()) + , m_glyphs(glyphs()) +{ +} + +QWindowsIconEngine::~QWindowsIconEngine() +{} + +QIconEngine *QWindowsIconEngine::clone() const +{ + return new QWindowsIconEngine(m_iconName); +} + +QString QWindowsIconEngine::key() const +{ + return u"QWindowsIconEngine"_s; +} + +QString QWindowsIconEngine::iconName() +{ + return m_iconName; +} + +bool QWindowsIconEngine::isNull() +{ + if (m_glyphs.isEmpty()) + return true; + + const QChar c0 = m_glyphs.at(0); + const QFontMetrics fontMetrics(m_iconFont); + if (c0.category() == QChar::Other_Surrogate && m_glyphs.size() > 1) + return !fontMetrics.inFontUcs4(QChar::surrogateToUcs4(c0, m_glyphs.at(1))); + return !fontMetrics.inFont(c0); +} + +QList<QSize> QWindowsIconEngine::availableSizes(QIcon::Mode, QIcon::State) +{ + return {{16, 16}, {24, 24}, {48, 48}, {128, 128}}; +} + +QSize QWindowsIconEngine::actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return QIconEngine::actualSize(size, mode, state); +} + +QPixmap QWindowsIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return scaledPixmap(size, mode, state, 1.0); +} + +QPixmap QWindowsIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) +{ + const quint64 cacheKey = calculateCacheKey(mode, state); + if (cacheKey != m_cacheKey || m_pixmap.size() != size || m_pixmap.devicePixelRatio() != scale) { + m_pixmap = QPixmap(size * scale); + m_pixmap.fill(Qt::transparent); + m_pixmap.setDevicePixelRatio(scale); + + QPainter painter(&m_pixmap); + paint(&painter, QRect(QPoint(), size), mode, state); + + m_cacheKey = cacheKey; + } + + return m_pixmap; +} + +void QWindowsIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) +{ + Q_UNUSED(state); + + painter->save(); + QFont renderFont(m_iconFont); + renderFont.setPixelSize(rect.height()); + painter->setFont(renderFont); + + QPalette palette; + switch (mode) { + case QIcon::Active: + painter->setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Normal: + painter->setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Disabled: + painter->setPen(palette.color(QPalette::Disabled, QPalette::Text)); + break; + case QIcon::Selected: + painter->setPen(palette.color(QPalette::Active, QPalette::HighlightedText)); + break; + } + + painter->drawText(rect, Qt::AlignCenter, m_glyphs); + painter->restore(); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/qwindowsiconengine.h b/src/plugins/platforms/windows/qwindowsiconengine.h new file mode 100644 index 0000000000..3c6cbddb8b --- /dev/null +++ b/src/plugins/platforms/windows/qwindowsiconengine.h @@ -0,0 +1,47 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSICONENGINE_H +#define QWINDOWSICONENGINE_H + +#include <QtCore/qt_windows.h> + +#include <QtGui/qfont.h> +#include <QtGui/qiconengine.h> + +QT_BEGIN_NAMESPACE + +class QWindowsIconEngine : public QIconEngine +{ +public: + QWindowsIconEngine(const QString &iconName); + ~QWindowsIconEngine(); + QIconEngine *clone() const override; + QString key() const override; + QString iconName() override; + bool isNull() override; + + QList<QSize> availableSizes(QIcon::Mode, QIcon::State) override; + QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) override; + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + +private: + static constexpr quint64 calculateCacheKey(QIcon::Mode mode, QIcon::State state) + { + return (quint64(mode) << 32) | state; + } + + QString glyphs() const; + + const QString m_iconName; + const QFont m_iconFont; + const QString m_glyphs; + mutable QPixmap m_pixmap; + mutable quint64 m_cacheKey = {}; +}; + +QT_END_NAMESPACE + +#endif // QWINDOWSICONENGINE_H diff --git a/src/plugins/platforms/windows/qwindowsinputcontext.cpp b/src/plugins/platforms/windows/qwindowsinputcontext.cpp index 758490ffb7..0281025b5b 100644 --- a/src/plugins/platforms/windows/qwindowsinputcontext.cpp +++ b/src/plugins/platforms/windows/qwindowsinputcontext.cpp @@ -676,7 +676,7 @@ int QWindowsInputContext::reconvertString(RECONVERTSTRING *reconv) reconv->dwTargetStrOffset = reconv->dwCompStrOffset; auto *pastReconv = reinterpret_cast<ushort *>(reconv + 1); std::copy(surroundingText.utf16(), surroundingText.utf16() + surroundingText.size(), - QT_MAKE_UNCHECKED_ARRAY_ITERATOR(pastReconv)); + pastReconv); return memSize; } diff --git a/src/plugins/platforms/windows/qwindowsintegration.cpp b/src/plugins/platforms/windows/qwindowsintegration.cpp index f1874b37bb..aa6be266da 100644 --- a/src/plugins/platforms/windows/qwindowsintegration.cpp +++ b/src/plugins/platforms/windows/qwindowsintegration.cpp @@ -48,6 +48,11 @@ #include <QtCore/qdebug.h> #include <QtCore/qvariant.h> +#include <QtCore/qoperatingsystemversion.h> +#include <QtCore/private/qfunctions_win_p.h> + +#include <wrl.h> + #include <limits.h> #if !defined(QT_NO_OPENGL) @@ -56,6 +61,14 @@ #include "qwindowsopengltester.h" +#if QT_CONFIG(cpp_winrt) +# include <QtCore/private/qt_winrtbase_p.h> +# include <winrt/Windows.UI.Notifications.h> +# include <winrt/Windows.Data.Xml.Dom.h> +# include <winrt/Windows.Foundation.h> +# include <winrt/Windows.UI.ViewManagement.h> +#endif + #include <memory> static inline void initOpenGlBlacklistResources() @@ -67,31 +80,6 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -/*! - \class QWindowsIntegration - \brief QPlatformIntegration implementation for Windows. - \internal - - \section1 Programming Considerations - - The platform plugin should run on Desktop Windows from Windows XP onwards - and Windows Embedded. - - It should compile with: - \list - \li Microsoft Visual Studio 2013 or later (using the Microsoft Windows SDK, - (\c Q_CC_MSVC). - \li Stock \l{http://mingw.org/}{MinGW} (\c Q_CC_MINGW). - This version ships with headers that are missing a lot of WinAPI. - \li MinGW distributions using GCC 4.7 or higher and a recent MinGW-w64 runtime API, - such as \l{http://tdm-gcc.tdragon.net/}{TDM-GCC}, or - \l{http://mingwbuilds.sourceforge.net/}{MinGW-builds} - (\c Q_CC_MINGW and \c __MINGW64_VERSION_MAJOR indicating the version). - MinGW-w64 provides more complete headers (compared to stock MinGW from mingw.org), - including a considerable part of the Windows SDK. - \endlist -*/ - struct QWindowsIntegrationPrivate { Q_DISABLE_COPY_MOVE(QWindowsIntegrationPrivate) @@ -131,7 +119,7 @@ bool parseIntOption(const QString ¶meter,const QLatin1StringView &option, const auto valueRef = QStringView{parameter}.right(valueLength); const int value = valueRef.toInt(&ok); if (ok) { - if (value >= minimumValue && value <= maximumValue) + if (value >= int(minimumValue) && value <= int(maximumValue)) *target = static_cast<IntType>(value); else { qWarning() << "Value" << value << "for option" << option << "out of range" @@ -148,14 +136,14 @@ using DarkModeHandling = QNativeInterface::Private::QWindowsApplication::DarkMod static inline unsigned parseOptions(const QStringList ¶mList, int *tabletAbsoluteRange, - QtWindows::ProcessDpiAwareness *dpiAwareness, + QtWindows::DpiAwareness *dpiAwareness, DarkModeHandling *darkModeHandling) { unsigned options = 0; for (const QString ¶m : paramList) { if (param.startsWith(u"fontengine=")) { - if (param.endsWith(u"directwrite")) { - options |= QWindowsIntegration::FontDatabaseDirectWrite; + if (param.endsWith(u"gdi")) { + options |= QWindowsIntegration::FontDatabaseGDI; } else if (param.endsWith(u"freetype")) { options |= QWindowsIntegration::FontDatabaseFreeType; } else if (param.endsWith(u"native")) { @@ -179,7 +167,8 @@ static inline unsigned parseOptions(const QStringList ¶mList, options |= QWindowsIntegration::DontPassOsMouseEventsSynthesizedFromTouch; } else if (parseIntOption(param, "verbose"_L1, 0, INT_MAX, &QWindowsContext::verbose) || parseIntOption(param, "tabletabsoluterange"_L1, 0, INT_MAX, tabletAbsoluteRange) - || parseIntOption(param, "dpiawareness"_L1, QtWindows::ProcessDpiUnaware, QtWindows::ProcessPerMonitorV2DpiAware, dpiAwareness)) { + || parseIntOption(param, "dpiawareness"_L1, QtWindows::DpiAwareness::Invalid, + QtWindows::DpiAwareness::PerMonitorVersion2, dpiAwareness)) { } else if (param == u"menus=native") { options |= QWindowsIntegration::AlwaysUseNativeMenus; } else if (param == u"menus=none") { @@ -188,8 +177,11 @@ static inline unsigned parseOptions(const QStringList ¶mList, options |= QWindowsIntegration::DontUseWMPointer; } else if (param == u"reverse") { options |= QWindowsIntegration::RtlEnabled; + } else if (param == u"darkmode=0") { + *darkModeHandling = {}; } else if (param == u"darkmode=1") { darkModeHandling->setFlag(DarkModeHandlingFlag::DarkModeWindowFrames); + darkModeHandling->setFlag(DarkModeHandlingFlag::DarkModeStyle, false); } else if (param == u"darkmode=2") { darkModeHandling->setFlag(DarkModeHandlingFlag::DarkModeWindowFrames); darkModeHandling->setFlag(DarkModeHandlingFlag::DarkModeStyle); @@ -206,10 +198,11 @@ void QWindowsIntegrationPrivate::parseOptions(QWindowsIntegration *q, const QStr static bool dpiAwarenessSet = false; // Default to per-monitor-v2 awareness (if available) - QtWindows::ProcessDpiAwareness dpiAwareness = QtWindows::ProcessPerMonitorV2DpiAware; + QtWindows::DpiAwareness dpiAwareness = QtWindows::DpiAwareness::PerMonitorVersion2; int tabletAbsoluteRange = -1; - DarkModeHandling darkModeHandling; + DarkModeHandling darkModeHandling = DarkModeHandlingFlag::DarkModeWindowFrames + | DarkModeHandlingFlag::DarkModeStyle; m_options = ::parseOptions(paramList, &tabletAbsoluteRange, &dpiAwareness, &darkModeHandling); q->setDarkModeHandling(darkModeHandling); QWindowsFontDatabase::setFontOptions(m_options); @@ -220,25 +213,13 @@ void QWindowsIntegrationPrivate::parseOptions(QWindowsIntegration *q, const QStr QCoreApplication::setAttribute(Qt::AA_CompressHighFrequencyEvents); else m_context.initTablet(); + QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); if (!dpiAwarenessSet) { // Set only once in case of repeated instantiations of QGuiApplication. if (!QCoreApplication::testAttribute(Qt::AA_PluginApplication)) { - if (dpiAwareness == QtWindows::ProcessPerMonitorV2DpiAware) { - // DpiAwareV2 requires using new API - if (m_context.setProcessDpiV2Awareness()) { - qCDebug(lcQpaWindows, "DpiAwareness: DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2"); - dpiAwarenessSet = true; - } else { - // fallback to old API - dpiAwareness = QtWindows::ProcessPerMonitorDpiAware; - } - } - - if (!dpiAwarenessSet) { - m_context.setProcessDpiAwareness(dpiAwareness); - qCDebug(lcQpaWindows) << "DpiAwareness=" << dpiAwareness - << "effective process DPI awareness=" << QWindowsContext::processDpiAwareness(); - } + m_context.setProcessDpiAwareness(dpiAwareness); + qCDebug(lcQpaWindow) << "DpiAwareness=" << dpiAwareness + << "effective process DPI awareness=" << QWindowsContext::processDpiAwareness(); } dpiAwarenessSet = true; } @@ -275,9 +256,9 @@ QWindowsIntegration::~QWindowsIntegration() void QWindowsIntegration::initialize() { - QString icStr = QPlatformInputContextFactory::requested(); - icStr.isNull() ? d->m_inputContext.reset(new QWindowsInputContext) - : d->m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); + auto icStrs = QPlatformInputContextFactory::requested(); + icStrs.isEmpty() ? d->m_inputContext.reset(new QWindowsInputContext) + : d->m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); } bool QWindowsIntegration::hasCapability(QPlatformIntegration::Capability cap) const @@ -305,6 +286,8 @@ bool QWindowsIntegration::hasCapability(QPlatformIntegration::Capability cap) co return true; case SwitchableWidgetComposition: return false; // QTBUG-68329 QTBUG-53515 QTBUG-54734 + case BackingStoreStaticContents: + return true; default: return QPlatformIntegration::hasCapability(cap); } @@ -315,7 +298,7 @@ QPlatformWindow *QWindowsIntegration::createPlatformWindow(QWindow *window) cons { if (window->type() == Qt::Desktop) { auto *result = new QWindowsDesktopWindow(window); - qCDebug(lcQpaWindows) << "Desktop window:" << window + qCDebug(lcQpaWindow) << "Desktop window:" << window << Qt::showbase << Qt::hex << result->winId() << Qt::noshowbase << Qt::dec << result->geometry(); return result; } @@ -325,15 +308,17 @@ QPlatformWindow *QWindowsIntegration::createPlatformWindow(QWindow *window) cons requested.geometry = window->isTopLevel() ? QHighDpi::toNativePixels(window->geometry(), window) : QHighDpi::toNativeLocalPosition(window->geometry(), window); - // Apply custom margins (see QWindowsWindow::setCustomMargins())). - const QVariant customMarginsV = window->property("_q_windowsCustomMargins"); - if (customMarginsV.isValid()) - requested.customMargins = qvariant_cast<QMargins>(customMarginsV); + if (!(requested.flags & Qt::FramelessWindowHint)) { + // Apply custom margins (see QWindowsWindow::setCustomMargins())). + const QVariant customMarginsV = window->property("_q_windowsCustomMargins"); + if (customMarginsV.isValid()) + requested.customMargins = qvariant_cast<QMargins>(customMarginsV); + } QWindowsWindowData obtained = QWindowsWindowData::create(window, requested, QWindowsWindow::formatWindowTitle(window->title())); - qCDebug(lcQpaWindows).nospace() + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << ' ' << window << "\n Requested: " << requested.geometry << " frame incl.=" << QWindowsGeometryHint::positionIncludesFrame(window) @@ -370,7 +355,7 @@ QPlatformWindow *QWindowsIntegration::createForeignWindow(QWindow *window, WId n screen = pScreen->screen(); if (screen && screen != window->screen()) window->setScreen(screen); - qCDebug(lcQpaWindows) << "Foreign window:" << window << Qt::showbase << Qt::hex + qCDebug(lcQpaWindow) << "Foreign window:" << window << Qt::showbase << Qt::hex << result->winId() << Qt::noshowbase << Qt::dec << obtainedGeometry << screen; return result; } @@ -499,17 +484,17 @@ QWindowsStaticOpenGLContext *QWindowsIntegration::staticOpenGLContext() QPlatformFontDatabase *QWindowsIntegration::fontDatabase() const { if (!d->m_fontDatabase) { -#if QT_CONFIG(directwrite3) - if (d->m_options & QWindowsIntegration::FontDatabaseDirectWrite) - d->m_fontDatabase = new QWindowsDirectWriteFontDatabase; - else -#endif #ifndef QT_NO_FREETYPE if (d->m_options & QWindowsIntegration::FontDatabaseFreeType) d->m_fontDatabase = new QWindowsFontDatabaseFT; else #endif // QT_NO_FREETYPE - d->m_fontDatabase = new QWindowsFontDatabase(); +#if QT_CONFIG(directwrite3) + if (!(d->m_options & (QWindowsIntegration::FontDatabaseGDI | QWindowsIntegration::DontUseDirectWriteFonts))) + d->m_fontDatabase = new QWindowsDirectWriteFontDatabase; + else +#endif + d->m_fontDatabase = new QWindowsFontDatabase; } return d->m_fontDatabase; } @@ -557,14 +542,9 @@ QVariant QWindowsIntegration::styleHint(QPlatformIntegration::StyleHint hint) co return QPlatformIntegration::styleHint(hint); } -Qt::KeyboardModifiers QWindowsIntegration::queryKeyboardModifiers() const -{ - return QWindowsKeyMapper::queryKeyboardModifiers(); -} - -QList<int> QWindowsIntegration::possibleKeys(const QKeyEvent *e) const +QPlatformKeyMapper *QWindowsIntegration::keyMapper() const { - return d->m_context.possibleKeys(e); + return d->m_context.keyMapper(); } #if QT_CONFIG(clipboard) @@ -631,6 +611,161 @@ void QWindowsIntegration::beep() const MessageBeep(MB_OK); // For QApplication } +void QWindowsIntegration::setApplicationBadge(qint64 number) +{ + // Clamp to positive numbers, as the Windows API doesn't support negative numbers + number = qMax(0, number); + + // Persist, so we can re-apply it on setting changes and Explorer restart + m_applicationBadgeNumber = number; + + static const bool isWindows11 = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11; + +#if QT_CONFIG(cpp_winrt) + // We prefer the native BadgeUpdater API, that allows us to set a number directly, + // but it requires that the application has a package identity, and also doesn't + // seem to work in all cases on < Windows 11. + QT_TRY { + if (isWindows11 && qt_win_hasPackageIdentity()) { + using namespace winrt::Windows::UI::Notifications; + auto badgeXml = BadgeUpdateManager::GetTemplateContent(BadgeTemplateType::BadgeNumber); + badgeXml.SelectSingleNode(L"//badge/@value").NodeValue(winrt::box_value(winrt::to_hstring(number))); + BadgeUpdateManager::CreateBadgeUpdaterForApplication().Update(BadgeNotification(badgeXml)); + return; + } + } QT_CATCH(...) { + // fall back to win32 implementation + } +#endif + + // Fallback for non-packaged apps, Windows 10, or Qt builds without WinRT/C++ support + + if (!number) { + // Clear badge + setApplicationBadge(QImage()); + return; + } + + const bool isDarkMode = QWindowsTheme::instance()->colorScheme() + == Qt::ColorScheme::Dark; + + QColor badgeColor; + QColor textColor; + +#if QT_CONFIG(cpp_winrt) + if (isWindows11) { + // Match colors used by BadgeUpdater + static const auto fromUIColor = [](winrt::Windows::UI::Color &&color) { + return QColor(color.R, color.G, color.B, color.A); + }; + using namespace winrt::Windows::UI::ViewManagement; + const auto settings = UISettings(); + badgeColor = fromUIColor(settings.GetColorValue(isDarkMode ? + UIColorType::AccentLight2 : UIColorType::Accent)); + textColor = fromUIColor(settings.GetColorValue(UIColorType::Background)); + } +#endif + + if (!badgeColor.isValid()) { + // Fall back to basic badge colors, based on Windows 10 look + badgeColor = isDarkMode ? Qt::black : QColor(220, 220, 220); + badgeColor.setAlphaF(0.5f); + textColor = isDarkMode ? Qt::white : Qt::black; + } + + const auto devicePixelRatio = qApp->devicePixelRatio(); + + static const QSize iconBaseSize(16, 16); + QImage image(iconBaseSize * devicePixelRatio, + QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + + QPainter painter(&image); + + QRect badgeRect = image.rect(); + QPen badgeBorderPen = Qt::NoPen; + if (!isWindows11) { + QColor badgeBorderColor = textColor; + badgeBorderColor.setAlphaF(0.5f); + badgeBorderPen = badgeBorderColor; + badgeRect.adjust(1, 1, -1, -1); + } + painter.setBrush(badgeColor); + painter.setPen(badgeBorderPen); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawEllipse(badgeRect); + + auto pixelSize = qCeil(10.5 * devicePixelRatio); + // Unlike the BadgeUpdater API we're limited by a square + // badge, so adjust the font size when above two digits. + const bool textOverflow = number > 99; + if (textOverflow) + pixelSize *= 0.8; + + QFont font = painter.font(); + font.setPixelSize(pixelSize); + font.setWeight(isWindows11 ? QFont::Medium : QFont::DemiBold); + painter.setFont(font); + + painter.setRenderHint(QPainter::TextAntialiasing, devicePixelRatio > 1); + painter.setPen(textColor); + + auto text = textOverflow ? u"99+"_s : QString::number(number); + painter.translate(textOverflow ? 1 : 0, textOverflow ? 0 : -1); + painter.drawText(image.rect(), Qt::AlignCenter, text); + + painter.end(); + + setApplicationBadge(image); +} + +void QWindowsIntegration::setApplicationBadge(const QImage &image) +{ + QComHelper comHelper; + + using Microsoft::WRL::ComPtr; + + ComPtr<ITaskbarList3> taskbarList; + CoCreateInstance(CLSID_TaskbarList, nullptr, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&taskbarList)); + if (!taskbarList) { + // There may not be any windows with a task bar button yet, + // in which case we'll apply the badge once a window with + // a button has been created. + return; + } + + const auto hIcon = image.toHICON(); + + // Apply the icon to all top level windows, since the badge is + // set on an application level. If one of the windows go away + // the other windows will take over in showing the badge. + const auto topLevelWindows = QGuiApplication::topLevelWindows(); + for (auto *topLevelWindow : topLevelWindows) { + if (!topLevelWindow->handle()) + continue; + auto hwnd = reinterpret_cast<HWND>(topLevelWindow->winId()); + taskbarList->SetOverlayIcon(hwnd, hIcon, L""); + } + + DestroyIcon(hIcon); + + // FIXME: Update icon when the application scale factor changes. + // Doing so in response to screen DPI changes is too soon, as the + // task bar is not yet ready for an updated icon, and will just + // result in a blurred icon even if our icon is high-DPI. +} + +void QWindowsIntegration::updateApplicationBadge() +{ + // The system color settings have changed, or we are reacting + // to a task bar button being created for the fist time or after + // Explorer had crashed and re-started. In any case, re-apply the + // badge so that everything is up to date. + if (m_applicationBadgeNumber) + setApplicationBadge(m_applicationBadgeNumber); +} + #if QT_CONFIG(vulkan) QPlatformVulkanInstance *QWindowsIntegration::createPlatformVulkanInstance(QVulkanInstance *instance) const { diff --git a/src/plugins/platforms/windows/qwindowsintegration.h b/src/plugins/platforms/windows/qwindowsintegration.h index 265ef719a6..c271207741 100644 --- a/src/plugins/platforms/windows/qwindowsintegration.h +++ b/src/plugins/platforms/windows/qwindowsintegration.h @@ -10,7 +10,9 @@ #include <qpa/qplatformintegration.h> #include <QtCore/qscopedpointer.h> #include <QtGui/private/qwindowsfontdatabase_p.h> +#ifndef QT_NO_OPENGL #include <QtGui/private/qopenglcontext_p.h> +#endif #include <qpa/qplatformopenglcontext.h> QT_BEGIN_NAMESPACE @@ -43,7 +45,7 @@ public: DontUseWMPointer = 0x400, DetectAltGrModifier = 0x800, RtlEnabled = 0x1000, - FontDatabaseDirectWrite = 0x2000 + FontDatabaseGDI = 0x2000 }; explicit QWindowsIntegration(const QStringList ¶mList); @@ -80,8 +82,7 @@ public: QPlatformServices *services() const override; QVariant styleHint(StyleHint hint) const override; - Qt::KeyboardModifiers queryKeyboardModifiers() const override; - QList<int> possibleKeys(const QKeyEvent *e) const override; + QPlatformKeyMapper *keyMapper() const override; static QWindowsIntegration *instance() { return m_instance; } @@ -89,6 +90,10 @@ public: void beep() const override; + void setApplicationBadge(qint64 number) override; + void setApplicationBadge(const QImage &image); + void updateApplicationBadge(); + #if QT_CONFIG(sessionmanager) QPlatformSessionManager *createPlatformSessionManager(const QString &id, const QString &key) const override; #endif @@ -104,6 +109,8 @@ private: QScopedPointer<QWindowsIntegrationPrivate> d; static QWindowsIntegration *m_instance; + + qint64 m_applicationBadgeNumber = 0; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/qwindowsinternalmimedata.cpp b/src/plugins/platforms/windows/qwindowsinternalmimedata.cpp index 11d5bc1c36..0542473a4b 100644 --- a/src/plugins/platforms/windows/qwindowsinternalmimedata.cpp +++ b/src/plugins/platforms/windows/qwindowsinternalmimedata.cpp @@ -3,7 +3,7 @@ #include "qwindowsinternalmimedata.h" #include "qwindowscontext.h" -#include "qwindowsmime.h" +#include "qwindowsmimeregistry.h" #include <QtCore/qdebug.h> #include <QtCore/qvariant.h> @@ -22,9 +22,9 @@ The base class introduces new virtuals to obtain and release the instances IDataObject from the clipboard or Drag and Drop and - does conversion using QWindowsMime classes. + does conversion using QWindowsMimeConverter classes. - \sa QInternalMimeData, QWindowsMime, QWindowsMimeConverter + \sa QInternalMimeData, QWindowsMimeConverter, QWindowsMimeRegistry \internal */ @@ -34,7 +34,7 @@ bool QWindowsInternalMimeData::hasFormat_sys(const QString &mime) const if (!pDataObj) return false; - const QWindowsMimeConverter &mc = QWindowsContext::instance()->mimeConverter(); + const QWindowsMimeRegistry &mc = QWindowsContext::instance()->mimeConverter(); const bool has = mc.converterToMime(mime, pDataObj) != nullptr; releaseDataObject(pDataObj); qCDebug(lcQpaMime) << __FUNCTION__ << mime << has; @@ -47,7 +47,7 @@ QStringList QWindowsInternalMimeData::formats_sys() const if (!pDataObj) return QStringList(); - const QWindowsMimeConverter &mc = QWindowsContext::instance()->mimeConverter(); + const QWindowsMimeRegistry &mc = QWindowsContext::instance()->mimeConverter(); const QStringList fmts = mc.allMimesForFormats(pDataObj); releaseDataObject(pDataObj); qCDebug(lcQpaMime) << __FUNCTION__ << fmts; @@ -61,7 +61,7 @@ QVariant QWindowsInternalMimeData::retrieveData_sys(const QString &mimeType, QMe return QVariant(); QVariant result; - const QWindowsMimeConverter &mc = QWindowsContext::instance()->mimeConverter(); + const QWindowsMimeRegistry &mc = QWindowsContext::instance()->mimeConverter(); if (auto converter = mc.converterToMime(mimeType, pDataObj)) result = converter->convertToMime(mimeType, pDataObj, type); releaseDataObject(pDataObj); diff --git a/src/plugins/platforms/windows/qwindowskeymapper.cpp b/src/plugins/platforms/windows/qwindowskeymapper.cpp index 006396d92a..ba76cda40b 100644 --- a/src/plugins/platforms/windows/qwindowskeymapper.cpp +++ b/src/plugins/platforms/windows/qwindowskeymapper.cpp @@ -15,6 +15,7 @@ #include <QtGui/qevent.h> #include <QtGui/private/qwindowsguieventdispatcher_p.h> #include <QtCore/private/qdebug_p.h> +#include <QtCore/private/qtools_p.h> #if defined(WM_APPCOMMAND) # ifndef FAPPCOMMAND_MOUSE @@ -87,9 +88,17 @@ QWindowsKeyMapper::~QWindowsKeyMapper()= default; #define VK_OEM_3 0xC0 #endif -// We not only need the scancode itself but also the extended bit of key messages. Thus we need -// the additional bit when masking the scancode. -enum { scancodeBitmask = 0x1ff }; +// Get scancode from the given message +static constexpr quint32 getScancode(const MSG &msg) +{ + const auto keyFlags = HIWORD(msg.lParam); + quint32 scancode = LOBYTE(keyFlags); + // if extended-key flag is on, the scan code consists of a sequence of two bytes, + // where the first byte has a value of 0xe0. + if ((keyFlags & KF_EXTENDED) != 0) + scancode |= 0xE000; + return scancode; +} // Key recorder ------------------------------------------------------------------------[ start ] -- struct KeyRecord { @@ -531,33 +540,6 @@ QDebug operator<<(QDebug d, const KeyboardLayoutItem &k) d << ')'; return d; } - -// Helpers to format a list of int as Qt key sequence -class formatKeys -{ -public: - explicit formatKeys(const QList<int> &keys) : m_keys(keys) {} - -private: - friend QDebug operator<<(QDebug d, const formatKeys &keys); - const QList<int> &m_keys; -}; - -QDebug operator<<(QDebug d, const formatKeys &k) -{ - QDebugStateSaver saver(d); - d.nospace(); - d << '('; - for (int i =0, size = k.m_keys.size(); i < size; ++i) { - if (i) - d << ", "; - d << QKeySequence(k.m_keys.at(i)); - } - d << ')'; - return d; -} -#else // !QT_NO_DEBUG_STREAM -static int formatKeys(const QList<int> &) { return 0; } #endif // QT_NO_DEBUG_STREAM /** @@ -599,8 +581,7 @@ static inline quint32 toKeyOrUnicode(quint32 vk, quint32 scancode, unsigned char static inline int asciiToKeycode(char a, int state) { - if (a >= 'a' && a <= 'z') - a = toupper(a); + a = QtMiscUtils::toAsciiUpper(a); if ((state & Qt::ControlModifier) != 0) { if (a >= 0 && a <= 31) // Ctrl+@..Ctrl+A..CTRL+Z..Ctrl+_ a += '@'; // to @..A..Z.._ @@ -656,7 +637,7 @@ void QWindowsKeyMapper::updateKeyMap(const MSG &msg) { unsigned char kbdBuffer[256]; // Will hold the complete keyboard state GetKeyboardState(kbdBuffer); - const quint32 scancode = (msg.lParam >> 16) & scancodeBitmask; + const quint32 scancode = getScancode(msg); updatePossibleKeyCodes(kbdBuffer, scancode, quint32(msg.wParam)); } @@ -778,50 +759,49 @@ static void showSystemMenu(QWindow* w) if (!menu) return; // no menu for this window -#define enabled (MF_BYCOMMAND | MF_ENABLED) -#define disabled (MF_BYCOMMAND | MF_GRAYED) +#define enabled (MF_BYCOMMAND | MFS_ENABLED) +#define disabled (MF_BYCOMMAND | MFS_GRAYED) - EnableMenuItem(menu, SC_MINIMIZE, (topLevel->flags() & Qt::WindowMinimizeButtonHint)?enabled:disabled); - bool maximized = IsZoomed(topLevelHwnd); + EnableMenuItem(menu, SC_MINIMIZE, (topLevel->flags() & Qt::WindowMinimizeButtonHint) ? enabled : disabled); + const bool maximized = IsZoomed(topLevelHwnd); - EnableMenuItem(menu, SC_MAXIMIZE, ! (topLevel->flags() & Qt::WindowMaximizeButtonHint) || maximized?disabled:enabled); + EnableMenuItem(menu, SC_MAXIMIZE, !(topLevel->flags() & Qt::WindowMaximizeButtonHint) || maximized ? disabled : enabled); // We should _not_ check with the setFixedSize(x,y) case here, since Windows is not able to check // this and our menu here would be out-of-sync with the menu produced by mouse-click on the // System Menu, or right-click on the title bar. - EnableMenuItem(menu, SC_SIZE, (topLevel->flags() & Qt::MSWindowsFixedSizeDialogHint) || maximized?disabled:enabled); - EnableMenuItem(menu, SC_MOVE, maximized?disabled:enabled); + EnableMenuItem(menu, SC_SIZE, (topLevel->flags() & Qt::MSWindowsFixedSizeDialogHint) || maximized ? disabled : enabled); + EnableMenuItem(menu, SC_MOVE, maximized ? disabled : enabled); EnableMenuItem(menu, SC_CLOSE, enabled); + EnableMenuItem(menu, SC_RESTORE, maximized ? enabled : disabled); // Highlight the first entry in the menu, this is what native Win32 applications usually do. - MENUITEMINFOW restoreItem; - SecureZeroMemory(&restoreItem, sizeof(restoreItem)); - restoreItem.cbSize = sizeof(restoreItem); - restoreItem.fMask = MIIM_STATE; - restoreItem.fState = MFS_HILITE | (maximized ? MFS_ENABLED : MFS_GRAYED); - SetMenuItemInfoW(menu, SC_RESTORE, FALSE, &restoreItem); + HiliteMenuItem(topLevelHwnd, menu, SC_RESTORE, MF_BYCOMMAND | MFS_HILITE); // Set bold on close menu item - MENUITEMINFO closeItem; - closeItem.cbSize = sizeof(MENUITEMINFO); - closeItem.fMask = MIIM_STATE; - closeItem.fState = MFS_DEFAULT; - SetMenuItemInfo(menu, SC_CLOSE, FALSE, &closeItem); + SetMenuDefaultItem(menu, SC_CLOSE, FALSE); #undef enabled #undef disabled + const QPoint pos = QHighDpi::toNativePixels(topLevel->geometry().topLeft(), topLevel); const int titleBarOffset = isSystemMenuOffsetNeeded(topLevel->flags()) ? getTitleBarHeight(topLevelHwnd) : 0; const int ret = TrackPopupMenuEx(menu, - TPM_LEFTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD, + TPM_LEFTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD, pos.x(), pos.y() + titleBarOffset, topLevelHwnd, nullptr); + + // Remove the highlight of the restore menu item, otherwise when the user right-clicks + // on the title bar, the popuped system menu will also highlight the restore item, which + // is not appropriate, it should only be highlighted if the menu is brought up by keyboard. + HiliteMenuItem(topLevelHwnd, menu, SC_RESTORE, MF_BYCOMMAND | MFS_UNHILITE); + if (ret) qWindowsWndProc(topLevelHwnd, WM_SYSCOMMAND, WPARAM(ret), 0); } -static inline void sendExtendedPressRelease(QWindow *w, int k, +static inline void sendExtendedPressRelease(QWindow *w, unsigned long timestamp, int k, Qt::KeyboardModifiers mods, quint32 nativeScanCode, quint32 nativeVirtualKey, @@ -830,8 +810,8 @@ static inline void sendExtendedPressRelease(QWindow *w, int k, bool autorep = false, ushort count = 1) { - QWindowSystemInterface::handleExtendedKeyEvent(w, QEvent::KeyPress, k, mods, nativeScanCode, nativeVirtualKey, nativeModifiers, text, autorep, count); - QWindowSystemInterface::handleExtendedKeyEvent(w, QEvent::KeyRelease, k, mods, nativeScanCode, nativeVirtualKey, nativeModifiers, text, autorep, count); + QWindowSystemInterface::handleExtendedKeyEvent(w, timestamp, QEvent::KeyPress, k, mods, nativeScanCode, nativeVirtualKey, nativeModifiers, text, autorep, count); + QWindowSystemInterface::handleExtendedKeyEvent(w, timestamp, QEvent::KeyRelease, k, mods, nativeScanCode, nativeVirtualKey, nativeModifiers, text, autorep, count); } /*! @@ -898,7 +878,7 @@ bool QWindowsKeyMapper::translateMultimediaKeyEventInternal(QWindow *window, con const int qtKey = int(CmdTbl[cmd]); if (!skipPressRelease) - sendExtendedPressRelease(receiver, qtKey, Qt::KeyboardModifier(state), 0, 0, 0); + sendExtendedPressRelease(receiver, msg.time, qtKey, Qt::KeyboardModifier(state), 0, 0, 0); // QTBUG-43343: Make sure to return false if Qt does not handle the key, otherwise, // the keys are not passed to the active media player. # if QT_CONFIG(shortcut) @@ -943,7 +923,7 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, m_seenAltGr = true; const UINT msgType = msg.message; - const quint32 scancode = (msg.lParam >> 16) & scancodeBitmask; + const quint32 scancode = getScancode(msg); auto vk_key = quint32(msg.wParam); quint32 nModifiers = 0; @@ -978,7 +958,7 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, // A multi-character key or a Input method character // not found by our look-ahead if (msgType == WM_CHAR || msgType == WM_IME_CHAR) { - sendExtendedPressRelease(receiver, 0, Qt::KeyboardModifier(state), scancode, vk_key, nModifiers, messageKeyText(msg), false); + sendExtendedPressRelease(receiver, msg.time, 0, Qt::KeyboardModifier(state), scancode, 0, nModifiers, messageKeyText(msg), false); return true; } @@ -1013,14 +993,14 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, if (dirStatus == VK_LSHIFT && ((msg.wParam == VK_SHIFT && GetKeyState(VK_LCONTROL)) || (msg.wParam == VK_CONTROL && GetKeyState(VK_LSHIFT)))) { - sendExtendedPressRelease(receiver, Qt::Key_Direction_L, {}, + sendExtendedPressRelease(receiver, msg.time, Qt::Key_Direction_L, {}, scancode, vk_key, nModifiers, QString(), false); result = true; dirStatus = 0; } else if (dirStatus == VK_RSHIFT && ( (msg.wParam == VK_SHIFT && GetKeyState(VK_RCONTROL)) || (msg.wParam == VK_CONTROL && GetKeyState(VK_RSHIFT)))) { - sendExtendedPressRelease(receiver, Qt::Key_Direction_R, {}, + sendExtendedPressRelease(receiver, msg.time, Qt::Key_Direction_R, {}, scancode, vk_key, nModifiers, QString(), false); result = true; dirStatus = 0; @@ -1233,9 +1213,9 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, // so, we have an auto-repeating key if (rec) { if (code < Qt::Key_Shift || code > Qt::Key_ScrollLock) { - QWindowSystemInterface::handleExtendedKeyEvent(receiver, QEvent::KeyRelease, code, + QWindowSystemInterface::handleExtendedKeyEvent(receiver, msg.time, QEvent::KeyRelease, code, Qt::KeyboardModifier(state), scancode, quint32(msg.wParam), nModifiers, rec->text, true); - QWindowSystemInterface::handleExtendedKeyEvent(receiver, QEvent::KeyPress, code, + QWindowSystemInterface::handleExtendedKeyEvent(receiver, msg.time, QEvent::KeyPress, code, Qt::KeyboardModifier(state), scancode, quint32(msg.wParam), nModifiers, rec->text, true); result = true; } @@ -1261,7 +1241,7 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, if (msg.wParam == VK_PACKET) code = asciiToKeycode(char(uch.cell()), state); - QWindowSystemInterface::handleExtendedKeyEvent(receiver, QEvent::KeyPress, code, + QWindowSystemInterface::handleExtendedKeyEvent(receiver, msg.time, QEvent::KeyPress, code, modifiers, scancode, quint32(msg.wParam), nModifiers, text, false); result =true; bool store = true; @@ -1303,10 +1283,10 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, if ((msg.lParam & 0x40000000) == 0 && Qt::KeyboardModifier(state) == Qt::NoModifier && ((code == Qt::Key_F18) || (code == Qt::Key_F19) || (code == Qt::Key_F20))) { - QWindowSystemInterface::handleExtendedKeyEvent(receiver, QEvent::KeyPress, code, + QWindowSystemInterface::handleExtendedKeyEvent(receiver, msg.time, QEvent::KeyPress, code, Qt::MetaModifier, scancode, quint32(msg.wParam), MetaLeft); - QWindowSystemInterface::handleExtendedKeyEvent(receiver, QEvent::KeyRelease, code, + QWindowSystemInterface::handleExtendedKeyEvent(receiver, msg.time, QEvent::KeyRelease, code, Qt::NoModifier, scancode, quint32(msg.wParam), 0); result = true; @@ -1318,7 +1298,7 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, // Map SHIFT + Tab to SHIFT + BackTab, QShortcutMap knows about this translation if (code == Qt::Key_Tab && (state & Qt::ShiftModifier) == Qt::ShiftModifier) code = Qt::Key_Backtab; - QWindowSystemInterface::handleExtendedKeyEvent(receiver, QEvent::KeyRelease, code, + QWindowSystemInterface::handleExtendedKeyEvent(receiver, msg.time, QEvent::KeyRelease, code, Qt::KeyboardModifier(state), scancode, quint32(msg.wParam), nModifiers, (rec ? rec->text : QString()), false); @@ -1340,7 +1320,7 @@ bool QWindowsKeyMapper::translateKeyEventInternal(QWindow *window, MSG msg, return result; } -Qt::KeyboardModifiers QWindowsKeyMapper::queryKeyboardModifiers() +Qt::KeyboardModifiers QWindowsKeyMapper::queryKeyboardModifiers() const { Qt::KeyboardModifiers modifiers = Qt::NoModifier; if (GetKeyState(VK_SHIFT) < 0) @@ -1354,9 +1334,9 @@ Qt::KeyboardModifiers QWindowsKeyMapper::queryKeyboardModifiers() return modifiers; } -QList<int> QWindowsKeyMapper::possibleKeys(const QKeyEvent *e) const +QList<QKeyCombination> QWindowsKeyMapper::possibleKeyCombinations(const QKeyEvent *e) const { - QList<int> result; + QList<QKeyCombination> result; const quint32 nativeVirtualKey = e->nativeVirtualKey(); @@ -1370,31 +1350,34 @@ QList<int> QWindowsKeyMapper::possibleKeys(const QKeyEvent *e) const quint32 baseKey = kbItem.qtKey[0]; Qt::KeyboardModifiers keyMods = e->modifiers(); if (baseKey == Qt::Key_Return && (e->nativeModifiers() & ExtendedKey)) { - result << (Qt::Key_Enter | keyMods).toCombined(); + result << (Qt::Key_Enter | keyMods); return result; } - result << int(baseKey) + int(keyMods); // The base key is _always_ valid, of course + + // The base key is _always_ valid, of course + result << QKeyCombination::fromCombined(int(baseKey) + int(keyMods)); for (size_t i = 1; i < NumMods; ++i) { Qt::KeyboardModifiers neededMods = ModsTbl[i]; quint32 key = kbItem.qtKey[i]; if (key && key != baseKey && ((keyMods & neededMods) == neededMods)) { const Qt::KeyboardModifiers missingMods = keyMods & ~neededMods; - const int matchedKey = int(key) + int(missingMods); - const auto it = - std::find_if(result.begin(), result.end(), - [key] (int k) { return (k & ~Qt::KeyboardModifierMask) == key; }); + const auto matchedKey = QKeyCombination::fromCombined(int(key) + int(missingMods)); + const auto it = std::find_if(result.begin(), result.end(), + [key](auto keyCombination) { + return keyCombination.key() == key; + }); // QTBUG-67200: Use the match with the least modifiers (prefer // Shift+9 over Alt + Shift + 9) resulting in more missing modifiers. if (it == result.end()) result << matchedKey; - else if (missingMods > (*it & Qt::KeyboardModifierMask)) + else if (missingMods > it->keyboardModifiers()) *it = matchedKey; } } qCDebug(lcQpaEvents) << __FUNCTION__ << e << "nativeVirtualKey=" << Qt::showbase << Qt::hex << e->nativeVirtualKey() << Qt::dec << Qt::noshowbase - << e->modifiers() << kbItem << "\n returns" << formatKeys(result); + << e->modifiers() << kbItem << "\n returns" << result; return result; } diff --git a/src/plugins/platforms/windows/qwindowskeymapper.h b/src/plugins/platforms/windows/qwindowskeymapper.h index 6e13c47323..72b2536ad7 100644 --- a/src/plugins/platforms/windows/qwindowskeymapper.h +++ b/src/plugins/platforms/windows/qwindowskeymapper.h @@ -8,6 +8,8 @@ #include <QtCore/qlocale.h> +#include <qpa/qplatformkeymapper.h> + QT_BEGIN_NAMESPACE class QKeyEvent; @@ -33,7 +35,7 @@ struct KeyboardLayoutItem { quint32 qtKey[NumQtKeys]; // Can by any Qt::Key_<foo>, or unicode character }; -class QWindowsKeyMapper +class QWindowsKeyMapper : public QPlatformKeyMapper { Q_DISABLE_COPY_MOVE(QWindowsKeyMapper) public: @@ -53,8 +55,8 @@ public: QWindow *keyGrabber() const { return m_keyGrabber; } void setKeyGrabber(QWindow *w) { m_keyGrabber = w; } - static Qt::KeyboardModifiers queryKeyboardModifiers(); - QList<int> possibleKeys(const QKeyEvent *e) const; + Qt::KeyboardModifiers queryKeyboardModifiers() const override; + QList<QKeyCombination> possibleKeyCombinations(const QKeyEvent *e) const override; private: bool translateKeyEventInternal(QWindow *receiver, MSG msg, bool grab, LRESULT *lResult); diff --git a/src/plugins/platforms/windows/qwindowsmenu.cpp b/src/plugins/platforms/windows/qwindowsmenu.cpp index 403ce52cb0..79deeeaea2 100644 --- a/src/plugins/platforms/windows/qwindowsmenu.cpp +++ b/src/plugins/platforms/windows/qwindowsmenu.cpp @@ -6,6 +6,7 @@ #include "qwindowswindow.h" #include <QtGui/qwindow.h> +#include <QtGui/private/qpixmap_win_p.h> #include <QtCore/qdebug.h> #include <QtCore/qvariant.h> #include <QtCore/qmetaobject.h> @@ -229,8 +230,6 @@ void QWindowsMenuItem::setIcon(const QIcon &icon) updateBitmap(); } -Q_GUI_EXPORT HBITMAP qt_pixmapToWinHBITMAP(const QPixmap &p, int hbitmapFormat = 0); - void QWindowsMenuItem::updateBitmap() { freeBitmap(); diff --git a/src/plugins/platforms/windows/qwindowsmenu.h b/src/plugins/platforms/windows/qwindowsmenu.h index 646ab64894..6f66180d82 100644 --- a/src/plugins/platforms/windows/qwindowsmenu.h +++ b/src/plugins/platforms/windows/qwindowsmenu.h @@ -9,7 +9,6 @@ #include <qpa/qplatformmenu.h> #include <QtCore/qlist.h> -#include <QtCore/qpair.h> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/windows/qwindowsmime.cpp b/src/plugins/platforms/windows/qwindowsmimeregistry.cpp index 8233e82d8e..8d147e8fa0 100644 --- a/src/plugins/platforms/windows/qwindowsmime.cpp +++ b/src/plugins/platforms/windows/qwindowsmimeregistry.cpp @@ -1,7 +1,7 @@ // Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#include "qwindowsmime.h" +#include "qwindowsmimeregistry.h" #include "qwindowscontext.h" #include <QtGui/private/qinternalmimedata_p.h> @@ -154,7 +154,7 @@ static bool qt_write_dibv5(QDataStream &s, QImage image) return false; if (image.format() != QImage::Format_ARGB32) - image = image.convertToFormat(QImage::Format_ARGB32); + image = std::move(image).convertToFormat(QImage::Format_ARGB32); auto *buf = new uchar[bpl_bmp]; @@ -300,7 +300,7 @@ QDebug operator<<(QDebug d, const FORMATETC &tc) d << "CF_ENHMETAFILE"; break; default: - d << QWindowsMimeConverter::clipboardFormatName(tc.cfFormat); + d << QWindowsMimeRegistry::clipboardFormatName(tc.cfFormat); break; } d << ", dwAspect=" << tc.dwAspect << ", lindex=" << tc.lindex @@ -333,106 +333,7 @@ QDebug operator<<(QDebug d, IDataObject *dataObj) } #endif // !QT_NO_DEBUG_STREAM -/*! - \class QWindowsMime - \brief The QWindowsMime class maps open-standard MIME to Window Clipboard formats. - \internal - - Qt's drag-and-drop and clipboard facilities use the MIME standard. - On X11, this maps trivially to the Xdnd protocol, but on Windows - although some applications use MIME types to describe clipboard - formats, others use arbitrary non-standardized naming conventions, - or unnamed built-in formats of Windows. - - By instantiating subclasses of QWindowsMime that provide conversions - between Windows Clipboard and MIME formats, you can convert - proprietary clipboard formats to MIME formats. - - Qt has predefined support for the following Windows Clipboard formats: - - \table - \header \li Windows Format \li Equivalent MIME type - \row \li \c CF_UNICODETEXT \li \c text/plain - \row \li \c CF_TEXT \li \c text/plain - \row \li \c CF_DIB \li \c{image/xyz}, where \c xyz is - a \l{QImageWriter::supportedImageFormats()}{Qt image format} - \row \li \c CF_HDROP \li \c text/uri-list - \row \li \c CF_INETURL \li \c text/uri-list - \row \li \c CF_HTML \li \c text/html - \endtable - - An example use of this class would be to map the Windows Metafile - clipboard format (\c CF_METAFILEPICT) to and from the MIME type - \c{image/x-wmf}. This conversion might simply be adding or removing - a header, or even just passing on the data. See \l{Drag and Drop} - for more information on choosing and definition MIME types. - - You can check if a MIME type is convertible using canConvertFromMime() and - can perform conversions with convertToMime() and convertFromMime(). - - \sa QWindowsMimeConverter -*/ - - -/*! -\fn bool QWindowsMime::canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const - - Returns \c true if the converter can convert from the \a mimeData to - the format specified in \a formatetc. - - All subclasses must reimplement this pure virtual function. -*/ - -/*! - \fn bool QWindowsMime::canConvertToMime(const QString &mimeType, IDataObject *pDataObj) const - - Returns \c true if the converter can convert to the \a mimeType from - the available formats in \a pDataObj. - - All subclasses must reimplement this pure virtual function. -*/ - -/*! -\fn QString QWindowsMime::mimeForFormat(const FORMATETC &formatetc) const - - Returns the mime type that will be created form the format specified - in \a formatetc, or an empty string if this converter does not support - \a formatetc. - - All subclasses must reimplement this pure virtual function. -*/ - -/*! -\fn QList<FORMATETC> QWindowsMime::formatsForMime(const QString &mimeType, const QMimeData *mimeData) const - - Returns a QList of FORMATETC structures representing the different windows clipboard - formats that can be provided for the \a mimeType from the \a mimeData. - - All subclasses must reimplement this pure virtual function. -*/ - -/*! - \fn QVariant QWindowsMime::convertToMime(const QString &mimeType, IDataObject *pDataObj, - QMetaType preferredType) const - - Returns a QVariant containing the converted data for \a mimeType from \a pDataObj. - If possible the QVariant should be of the \a preferredType to avoid needless conversions. - - All subclasses must reimplement this pure virtual function. -*/ - -/*! -\fn bool QWindowsMime::convertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData, STGMEDIUM * pmedium) const - - Convert the \a mimeData to the format specified in \a formatetc. - The converted data should then be placed in \a pmedium structure. - - Return true if the conversion was successful. - - All subclasses must reimplement this pure virtual function. -*/ - -class QWindowsMimeText : public QNativeInterface::Private::QWindowsMime +class QWindowsMimeText : public QWindowsMimeConverter { public: bool canConvertToMime(const QString &mimeType, IDataObject *pDataObj) const override; @@ -446,7 +347,7 @@ public: bool QWindowsMimeText::canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const { int cf = getCf(formatetc); - return (cf == CF_UNICODETEXT || cf == CF_TEXT) && mimeData->hasText(); + return (cf == CF_UNICODETEXT || (cf == CF_TEXT && GetACP() != CP_UTF8)) && mimeData->hasText(); } /* @@ -537,7 +438,7 @@ QString QWindowsMimeText::mimeForFormat(const FORMATETC &formatetc) const { int cf = getCf(formatetc); if (cf == CF_UNICODETEXT || cf == CF_TEXT) - return QStringLiteral("text/plain"); + return u"text/plain"_s; return QString(); } @@ -547,7 +448,8 @@ QList<FORMATETC> QWindowsMimeText::formatsForMime(const QString &mimeType, const QList<FORMATETC> formatics; if (mimeType.startsWith(u"text/plain") && mimeData->hasText()) { formatics += setCf(CF_UNICODETEXT); - formatics += setCf(CF_TEXT); + if (GetACP() != CP_UTF8) + formatics += setCf(CF_TEXT); } return formatics; } @@ -588,7 +490,7 @@ QVariant QWindowsMimeText::convertToMime(const QString &mime, LPDATAOBJECT pData return ret; } -class QWindowsMimeURI : public QNativeInterface::Private::QWindowsMime +class QWindowsMimeURI : public QWindowsMimeConverter { public: QWindowsMimeURI(); @@ -605,8 +507,8 @@ private: QWindowsMimeURI::QWindowsMimeURI() { - CF_INETURL_W = QWindowsMimeConverter::registerMimeType(QStringLiteral("UniformResourceLocatorW")); - CF_INETURL = QWindowsMimeConverter::registerMimeType(QStringLiteral("UniformResourceLocator")); + CF_INETURL_W = registerMimeType(u"UniformResourceLocatorW"_s); + CF_INETURL = registerMimeType(u"UniformResourceLocator"_s); } bool QWindowsMimeURI::canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const @@ -689,7 +591,7 @@ QString QWindowsMimeURI::mimeForFormat(const FORMATETC &formatetc) const { QString format; if (getCf(formatetc) == CF_HDROP || getCf(formatetc) == CF_INETURL_W || getCf(formatetc) == CF_INETURL) - format = QStringLiteral("text/uri-list"); + format = u"text/uri-list"_s; return format; } @@ -754,7 +656,7 @@ QVariant QWindowsMimeURI::convertToMime(const QString &mimeType, LPDATAOBJECT pD return QVariant(); } -class QWindowsMimeHtml : public QNativeInterface::Private::QWindowsMime +class QWindowsMimeHtml : public QWindowsMimeConverter { public: QWindowsMimeHtml(); @@ -775,7 +677,7 @@ private: QWindowsMimeHtml::QWindowsMimeHtml() { - CF_HTML = QWindowsMimeConverter::registerMimeType(QStringLiteral("HTML Format")); + CF_HTML = registerMimeType(u"HTML Format"_s); } QList<FORMATETC> QWindowsMimeHtml::formatsForMime(const QString &mimeType, const QMimeData *mimeData) const @@ -789,7 +691,7 @@ QList<FORMATETC> QWindowsMimeHtml::formatsForMime(const QString &mimeType, const QString QWindowsMimeHtml::mimeForFormat(const FORMATETC &formatetc) const { if (getCf(formatetc) == CF_HTML) - return QStringLiteral("text/html"); + return u"text/html"_s; return QString(); } @@ -892,7 +794,7 @@ bool QWindowsMimeHtml::convertFromMime(const FORMATETC &formatetc, const QMimeDa #ifndef QT_NO_IMAGEFORMAT_BMP -class QWindowsMimeImage : public QNativeInterface::Private::QWindowsMime +class QWindowsMimeImage : public QWindowsMimeConverter { public: QWindowsMimeImage(); @@ -906,6 +808,7 @@ public: QVariant convertToMime(const QString &mime, IDataObject *pDataObj, QMetaType preferredType) const override; QString mimeForFormat(const FORMATETC &formatetc) const override; private: + bool hasOriginalDIBV5(IDataObject *pDataObj) const; UINT CF_PNG; }; @@ -933,7 +836,7 @@ QString QWindowsMimeImage::mimeForFormat(const FORMATETC &formatetc) const { int cf = getCf(formatetc); if (cf == CF_DIB || cf == CF_DIBV5 || cf == int(CF_PNG)) - return QStringLiteral("application/x-qt-image"); + return u"application/x-qt-image"_s; return QString(); } @@ -967,7 +870,7 @@ bool QWindowsMimeImage::convertFromMime(const FORMATETC &formatetc, const QMimeD QByteArray ba; if (cf == CF_DIB) { if (img.format() > QImage::Format_ARGB32) - img = img.convertToFormat(QImage::Format_RGB32); + img = std::move(img).convertToFormat(QImage::Format_RGB32); const QByteArray ba = writeDib(img); if (!ba.isEmpty()) return setData(ba, pmedium); @@ -980,22 +883,48 @@ bool QWindowsMimeImage::convertFromMime(const FORMATETC &formatetc, const QMimeD } else { QDataStream s(&ba, QIODevice::WriteOnly); s.setByteOrder(QDataStream::LittleEndian);// Intel byte order #### - if (qt_write_dibv5(s, img)) + if (qt_write_dibv5(s, std::move(img))) return setData(ba, pmedium); } } return false; } +bool QWindowsMimeImage::hasOriginalDIBV5(IDataObject *pDataObj) const +{ + bool isSynthesized = true; + IEnumFORMATETC *pEnum = nullptr; + HRESULT res = pDataObj->EnumFormatEtc(1, &pEnum); + if (res == S_OK && pEnum) { + FORMATETC fc; + while ((res = pEnum->Next(1, &fc, nullptr)) == S_OK) { + if (fc.ptd) + CoTaskMemFree(fc.ptd); + if (fc.cfFormat == CF_DIB) + break; + if (fc.cfFormat == CF_DIBV5) { + isSynthesized = false; + break; + } + } + pEnum->Release(); + } + return !isSynthesized; +} + QVariant QWindowsMimeImage::convertToMime(const QString &mimeType, IDataObject *pDataObj, QMetaType preferredType) const { Q_UNUSED(preferredType); QVariant result; if (mimeType != u"application/x-qt-image") return result; - //Try to convert from DIBV5 as it is the most - //widespread format that support transparency - if (canGetData(CF_DIBV5, pDataObj)) { + // Try to convert from DIBV5 as it is the most widespread format that supports transparency, + // but avoid synthesizing it, as that typically loses transparency, e.g. from Office + const bool canGetDibV5 = canGetData(CF_DIBV5, pDataObj); + const bool hasOrigDibV5 = canGetDibV5 ? hasOriginalDIBV5(pDataObj) : false; + qCDebug(lcQpaMime) << "canGetDibV5:" << canGetDibV5 << "hasOrigDibV5:" << hasOrigDibV5; + if (hasOrigDibV5) { + qCDebug(lcQpaMime) << "Decoding DIBV5"; QImage img; QByteArray data = getData(CF_DIBV5, pDataObj); QBuffer buffer(&data); @@ -1004,6 +933,7 @@ QVariant QWindowsMimeImage::convertToMime(const QString &mimeType, IDataObject * } //PNG, MS Office place this (undocumented) if (canGetData(CF_PNG, pDataObj)) { + qCDebug(lcQpaMime) << "Decoding PNG"; QImage img; QByteArray data = getData(CF_PNG, pDataObj); if (img.loadFromData(data, "PNG")) { @@ -1012,6 +942,7 @@ QVariant QWindowsMimeImage::convertToMime(const QString &mimeType, IDataObject * } //Fallback to DIB if (canGetData(CF_DIB, pDataObj)) { + qCDebug(lcQpaMime) << "Decoding DIB"; QImage img; QByteArray data = getData(CF_DIBV5, pDataObj); QBuffer buffer(&data); @@ -1023,7 +954,7 @@ QVariant QWindowsMimeImage::convertToMime(const QString &mimeType, IDataObject * } #endif -class QBuiltInMimes : public QNativeInterface::Private::QWindowsMime +class QBuiltInMimes : public QWindowsMimeConverter { public: QBuiltInMimes(); @@ -1044,10 +975,10 @@ private: }; QBuiltInMimes::QBuiltInMimes() -: QWindowsMime() +: QWindowsMimeConverter() { - outFormats.insert(QWindowsMimeConverter::registerMimeType(QStringLiteral("application/x-color")), QStringLiteral("application/x-color")); - inFormats.insert(QWindowsMimeConverter::registerMimeType(QStringLiteral("application/x-color")), QStringLiteral("application/x-color")); + outFormats.insert(registerMimeType(u"application/x-color"_s), u"application/x-color"_s); + inFormats.insert(registerMimeType(u"application/x-color"_s), u"application/x-color"_s); } bool QBuiltInMimes::canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const @@ -1144,7 +1075,7 @@ QString QBuiltInMimes::mimeForFormat(const FORMATETC &formatetc) const } -class QLastResortMimes : public QNativeInterface::Private::QWindowsMime +class QLastResortMimes : public QWindowsMimeConverter { public: @@ -1172,25 +1103,25 @@ QLastResortMimes::QLastResortMimes() { //MIME Media-Types if (ianaTypes.isEmpty()) { - ianaTypes.append(QStringLiteral("application/")); - ianaTypes.append(QStringLiteral("audio/")); - ianaTypes.append(QStringLiteral("example/")); - ianaTypes.append(QStringLiteral("image/")); - ianaTypes.append(QStringLiteral("message/")); - ianaTypes.append(QStringLiteral("model/")); - ianaTypes.append(QStringLiteral("multipart/")); - ianaTypes.append(QStringLiteral("text/")); - ianaTypes.append(QStringLiteral("video/")); + ianaTypes.append(u"application/"_s); + ianaTypes.append(u"audio/"_s); + ianaTypes.append(u"example/"_s); + ianaTypes.append(u"image/"_s); + ianaTypes.append(u"message/"_s); + ianaTypes.append(u"model/"_s); + ianaTypes.append(u"multipart/"_s); + ianaTypes.append(u"text/"_s); + ianaTypes.append(u"video/"_s); } //Types handled by other classes if (excludeList.isEmpty()) { - excludeList.append(QStringLiteral("HTML Format")); - excludeList.append(QStringLiteral("UniformResourceLocator")); - excludeList.append(QStringLiteral("text/html")); - excludeList.append(QStringLiteral("text/plain")); - excludeList.append(QStringLiteral("text/uri-list")); - excludeList.append(QStringLiteral("application/x-qt-image")); - excludeList.append(QStringLiteral("application/x-color")); + excludeList.append(u"HTML Format"_s); + excludeList.append(u"UniformResourceLocator"_s); + excludeList.append(u"text/html"_s); + excludeList.append(u"text/plain"_s); + excludeList.append(u"text/uri-list"_s); + excludeList.append(u"application/x-qt-image"_s); + excludeList.append(u"application/x-color"_s); } } @@ -1228,7 +1159,7 @@ QList<FORMATETC> QLastResortMimes::formatsForMime(const QString &mimeType, const auto mit = std::find(formats.begin(), formats.end(), mimeType); // register any other available formats if (mit == formats.end() && !excludeList.contains(mimeType, Qt::CaseInsensitive)) - mit = formats.insert(QWindowsMimeConverter::registerMimeType(mimeType), mimeType); + mit = formats.insert(registerMimeType(mimeType), mimeType); if (mit != formats.end()) formatetcs += setCf(mit.key()); @@ -1272,7 +1203,7 @@ bool QLastResortMimes::canConvertToMime(const QString &mimeType, IDataObject *pD } // if it is not in there then register it and see if we can get it const auto mit = std::find(formats.cbegin(), formats.cend(), mimeType); - const int cf = mit != formats.cend() ? mit.key() : QWindowsMimeConverter::registerMimeType(mimeType); + const int cf = mit != formats.cend() ? mit.key() : registerMimeType(mimeType); return canGetData(cf, pDataObj); } @@ -1289,7 +1220,7 @@ QVariant QLastResortMimes::convertToMime(const QString &mimeType, IDataObject *p data = getData(int(cf), pDataObj, lindex); } else { const auto mit = std::find(formats.cbegin(), formats.cend(), mimeType); - const int cf = mit != formats.cend() ? mit.key() : QWindowsMimeConverter::registerMimeType(mimeType); + const int cf = mit != formats.cend() ? mit.key() : registerMimeType(mimeType); data = getData(cf, pDataObj); } if (!data.isEmpty()) @@ -1304,7 +1235,7 @@ QString QLastResortMimes::mimeForFormat(const FORMATETC &formatetc) const if (!format.isEmpty()) return format; - const QString clipFormat = QWindowsMimeConverter::clipboardFormatName(getCf(formatetc)); + const QString clipFormat = QWindowsMimeRegistry::clipboardFormatName(getCf(formatetc)); if (!clipFormat.isEmpty()) { #if QT_CONFIG(draganddrop) if (QInternalMimeData::canReadData(clipFormat)) @@ -1334,20 +1265,20 @@ QString QLastResortMimes::mimeForFormat(const FORMATETC &formatetc) const } /*! - \class QWindowsMimeConverter - \brief Manages the list of QWindowsMime instances. + \class QWindowsMimeRegistry + \brief Manages the list of QWindowsMimeConverter instances. \internal - \sa QWindowsMime + \sa QWindowsMimeConverter */ -QWindowsMimeConverter::QWindowsMimeConverter() = default; +QWindowsMimeRegistry::QWindowsMimeRegistry() = default; -QWindowsMimeConverter::~QWindowsMimeConverter() +QWindowsMimeRegistry::~QWindowsMimeRegistry() { qDeleteAll(m_mimes.begin(), m_mimes.begin() + m_internalMimeCount); } -QWindowsMimeConverter::QWindowsMime *QWindowsMimeConverter::converterToMime(const QString &mimeType, IDataObject *pDataObj) const +QWindowsMimeRegistry::QWindowsMimeConverter *QWindowsMimeRegistry::converterToMime(const QString &mimeType, IDataObject *pDataObj) const { ensureInitialized(); for (int i = m_mimes.size()-1; i >= 0; --i) { @@ -1357,9 +1288,9 @@ QWindowsMimeConverter::QWindowsMime *QWindowsMimeConverter::converterToMime(cons return nullptr; } -QStringList QWindowsMimeConverter::allMimesForFormats(IDataObject *pDataObj) const +QStringList QWindowsMimeRegistry::allMimesForFormats(IDataObject *pDataObj) const { - qCDebug(lcQpaMime) << "QWindowsMime::allMimesForFormats()"; + qCDebug(lcQpaMime) << "QWindowsMimeConverter::allMimesForFormats()"; ensureInitialized(); QStringList formats; LPENUMFORMATETC FAR fmtenum; @@ -1386,7 +1317,7 @@ QStringList QWindowsMimeConverter::allMimesForFormats(IDataObject *pDataObj) con return formats; } -QWindowsMimeConverter::QWindowsMime *QWindowsMimeConverter::converterFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const +QWindowsMimeRegistry::QWindowsMimeConverter *QWindowsMimeRegistry::converterFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const { ensureInitialized(); qCDebug(lcQpaMime) << __FUNCTION__ << formatetc; @@ -1397,7 +1328,7 @@ QWindowsMimeConverter::QWindowsMime *QWindowsMimeConverter::converterFromMime(co return nullptr; } -QList<FORMATETC> QWindowsMimeConverter::allFormatsForMime(const QMimeData *mimeData) const +QList<FORMATETC> QWindowsMimeRegistry::allFormatsForMime(const QMimeData *mimeData) const { ensureInitialized(); QList<FORMATETC> formatics; @@ -1414,34 +1345,37 @@ QList<FORMATETC> QWindowsMimeConverter::allFormatsForMime(const QMimeData *mimeD return formatics; } -void QWindowsMimeConverter::ensureInitialized() const +void QWindowsMimeRegistry::ensureInitialized() const { - if (m_mimes.isEmpty()) { - m_mimes + if (m_internalMimeCount == 0) { + m_internalMimeCount = -1; // prevent reentrancy when types register themselves #ifndef QT_NO_IMAGEFORMAT_BMP - << new QWindowsMimeImage + (void)new QWindowsMimeImage; #endif //QT_NO_IMAGEFORMAT_BMP - << new QLastResortMimes - << new QWindowsMimeText << new QWindowsMimeURI - << new QWindowsMimeHtml << new QBuiltInMimes; + (void)new QLastResortMimes; + (void)new QWindowsMimeText; + (void)new QWindowsMimeURI; + (void)new QWindowsMimeHtml; + (void)new QBuiltInMimes; m_internalMimeCount = m_mimes.size(); + Q_ASSERT(m_internalMimeCount > 0); } } -QString QWindowsMimeConverter::clipboardFormatName(int cf) +QString QWindowsMimeRegistry::clipboardFormatName(int cf) { wchar_t buf[256] = {0}; return GetClipboardFormatName(UINT(cf), buf, 255) ? QString::fromWCharArray(buf) : QString(); } -QVariant QWindowsMimeConverter::convertToMime(const QStringList &mimeTypes, +QVariant QWindowsMimeRegistry::convertToMime(const QStringList &mimeTypes, IDataObject *pDataObj, QMetaType preferredType, QString *formatIn /* = 0 */) const { for (const QString &format : mimeTypes) { - if (const QWindowsMime *converter = converterToMime(format, pDataObj)) { + if (const QWindowsMimeConverter *converter = converterToMime(format, pDataObj)) { if (converter->canConvertToMime(format, pDataObj)) { const QVariant dataV = converter->convertToMime(format, pDataObj, preferredType); if (dataV.isValid()) { @@ -1458,7 +1392,7 @@ QVariant QWindowsMimeConverter::convertToMime(const QStringList &mimeTypes, return QVariant(); } -void QWindowsMimeConverter::registerMime(QWindowsMime *mime) +void QWindowsMimeRegistry::registerMime(QWindowsMimeConverter *mime) { ensureInitialized(); m_mimes.append(mime); @@ -1467,12 +1401,18 @@ void QWindowsMimeConverter::registerMime(QWindowsMime *mime) /*! Registers the MIME type \a mime, and returns an ID number identifying the format on Windows. + + A mime type \c {application/x-qt-windows-mime;value="WindowsType"} will be + registered as the clipboard format for \c WindowsType. */ -int QWindowsMimeConverter::registerMimeType(const QString &mime) +int QWindowsMimeRegistry::registerMimeType(const QString &mime) { - const UINT f = RegisterClipboardFormat(reinterpret_cast<const wchar_t *> (mime.utf16())); - if (!f) - qErrnoWarning("QWindowsApplication::registerMimeType: Failed to register clipboard format"); + const QString mimeType = isCustomMimeType(mime) ? customMimeType(mime) : mime; + const UINT f = RegisterClipboardFormat(reinterpret_cast<const wchar_t *> (mimeType.utf16())); + if (!f) { + qErrnoWarning("QWindowsMimeRegistry::registerMimeType: Failed to register clipboard format " + "for %s", qPrintable(mime)); + } return int(f); } diff --git a/src/plugins/platforms/windows/qwindowsmime.h b/src/plugins/platforms/windows/qwindowsmimeregistry.h index 1f30e991d9..a0f4b4c60a 100644 --- a/src/plugins/platforms/windows/qwindowsmime.h +++ b/src/plugins/platforms/windows/qwindowsmimeregistry.h @@ -1,13 +1,13 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#ifndef QWINDOWSMIME_H -#define QWINDOWSMIME_H +#ifndef QWINDOWSMIMEREGISTRY_H +#define QWINDOWSMIMEREGISTRY_H -#include <QtGui/private/qwindowsmime_p.h> #include <QtCore/qt_windows.h> +#include <QtGui/qwindowsmimeconverter.h> #include <QtCore/qlist.h> #include <QtCore/qvariant.h> @@ -16,26 +16,26 @@ QT_BEGIN_NAMESPACE class QDebug; class QMimeData; -class QWindowsMimeConverter +class QWindowsMimeRegistry { - Q_DISABLE_COPY_MOVE(QWindowsMimeConverter) + Q_DISABLE_COPY_MOVE(QWindowsMimeRegistry) public: - using QWindowsMime = QNativeInterface::Private::QWindowsMime; + using QWindowsMimeConverter = QWindowsMimeConverter; - QWindowsMimeConverter(); - ~QWindowsMimeConverter(); + QWindowsMimeRegistry(); + ~QWindowsMimeRegistry(); - QWindowsMime *converterToMime(const QString &mimeType, IDataObject *pDataObj) const; + QWindowsMimeConverter *converterToMime(const QString &mimeType, IDataObject *pDataObj) const; QStringList allMimesForFormats(IDataObject *pDataObj) const; - QWindowsMime *converterFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const; + QWindowsMimeConverter *converterFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const; QList<FORMATETC> allFormatsForMime(const QMimeData *mimeData) const; // Convenience. QVariant convertToMime(const QStringList &mimeTypes, IDataObject *pDataObj, QMetaType preferredType, QString *format = nullptr) const; - void registerMime(QWindowsMime *mime); - void unregisterMime(QWindowsMime *mime) { m_mimes.removeOne(mime); } + void registerMime(QWindowsMimeConverter *mime); + void unregisterMime(QWindowsMimeConverter *mime) { m_mimes.removeOne(mime); } static int registerMimeType(const QString &mime); @@ -44,7 +44,7 @@ public: private: void ensureInitialized() const; - mutable QList<QWindowsMime *> m_mimes; + mutable QList<QWindowsMimeConverter *> m_mimes; mutable int m_internalMimeCount = 0; }; @@ -55,4 +55,4 @@ QDebug operator<<(QDebug d, IDataObject *); QT_END_NAMESPACE -#endif // QWINDOWSMIME_H +#endif // QWINDOWSMIMEREGISTRY_H diff --git a/src/plugins/platforms/windows/qwindowsmousehandler.cpp b/src/plugins/platforms/windows/qwindowsmousehandler.cpp index 6918f523ce..9af9fba408 100644 --- a/src/plugins/platforms/windows/qwindowsmousehandler.cpp +++ b/src/plugins/platforms/windows/qwindowsmousehandler.cpp @@ -16,7 +16,8 @@ #include <QtGui/qcursor.h> #include <QtCore/qdebug.h> -#include <QtCore/qscopedpointer.h> + +#include <memory> #include <windowsx.h> @@ -123,7 +124,7 @@ Qt::MouseButtons QWindowsMouseHandler::queryMouseButtons() return result; } -static QPoint lastMouseMovePos; +Q_CONSTINIT static QPoint lastMouseMovePos; namespace { struct MouseEvent { @@ -267,7 +268,8 @@ bool QWindowsMouseHandler::translateMouseEvent(QWindow *window, HWND hwnd, } } - const Qt::KeyboardModifiers keyModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + const Qt::KeyboardModifiers keyModifiers = keyMapper->queryKeyboardModifiers(); const MouseEvent mouseEvent = eventFromMsg(msg); Qt::MouseButtons buttons; @@ -285,19 +287,16 @@ bool QWindowsMouseHandler::translateMouseEvent(QWindow *window, HWND hwnd, if (m_lastEventType == QEvent::NonClientAreaMouseButtonPress && (mouseEvent.type == QEvent::NonClientAreaMouseMove || mouseEvent.type == QEvent::MouseMove) && (m_lastEventButton & buttons) == 0) { - if (mouseEvent.type == QEvent::NonClientAreaMouseMove) { - QWindowSystemInterface::handleFrameStrutMouseEvent(window, device, clientPosition, globalPosition, buttons, m_lastEventButton, - QEvent::NonClientAreaMouseButtonRelease, keyModifiers, source); - } else { - QWindowSystemInterface::handleMouseEvent(window, device, clientPosition, globalPosition, buttons, m_lastEventButton, - QEvent::MouseButtonRelease, keyModifiers, source); - } + auto releaseType = mouseEvent.type == QEvent::NonClientAreaMouseMove ? + QEvent::NonClientAreaMouseButtonRelease : QEvent::MouseButtonRelease; + QWindowSystemInterface::handleMouseEvent(window, msg.time, device, clientPosition, globalPosition, buttons, m_lastEventButton, + releaseType, keyModifiers, source); } m_lastEventType = mouseEvent.type; m_lastEventButton = mouseEvent.button; if (mouseEvent.type >= QEvent::NonClientAreaMouseMove && mouseEvent.type <= QEvent::NonClientAreaMouseButtonDblClick) { - QWindowSystemInterface::handleFrameStrutMouseEvent(window, device, clientPosition, + QWindowSystemInterface::handleMouseEvent(window, msg.time, device, clientPosition, globalPosition, buttons, mouseEvent.button, mouseEvent.type, keyModifiers, source); @@ -447,7 +446,7 @@ bool QWindowsMouseHandler::translateMouseEvent(QWindow *window, HWND hwnd, } if (!discardEvent && mouseEvent.type != QEvent::None) { - QWindowSystemInterface::handleMouseEvent(window, device, clientPosition, globalPosition, buttons, + QWindowSystemInterface::handleMouseEvent(window, msg.time, device, clientPosition, globalPosition, buttons, mouseEvent.button, mouseEvent.type, keyModifiers, source); } @@ -471,7 +470,7 @@ static bool isValidWheelReceiver(QWindow *candidate) return false; } -static void redirectWheelEvent(QWindow *window, const QPoint &globalPos, int delta, +static void redirectWheelEvent(QWindow *window, unsigned long timestamp, const QPoint &globalPos, int delta, Qt::Orientation orientation, Qt::KeyboardModifiers mods) { // Redirect wheel event to one of the following, in order of preference: @@ -492,6 +491,7 @@ static void redirectWheelEvent(QWindow *window, const QPoint &globalPos, int del if (handleEvent) { const QPoint point = (orientation == Qt::Vertical) ? QPoint(0, delta) : QPoint(delta, 0); QWindowSystemInterface::handleWheelEvent(receiver, + timestamp, QWindowsGeometryHint::mapFromGlobal(receiver, globalPos), globalPos, QPoint(), point, mods); } @@ -520,7 +520,7 @@ bool QWindowsMouseHandler::translateMouseWheelEvent(QWindow *window, HWND, delta = -delta; const QPoint globalPos(GET_X_LPARAM(msg.lParam), GET_Y_LPARAM(msg.lParam)); - redirectWheelEvent(window, globalPos, delta, orientation, mods); + redirectWheelEvent(window, msg.time, globalPos, delta, orientation, mods); return true; } @@ -551,7 +551,7 @@ bool QWindowsMouseHandler::translateScrollEvent(QWindow *window, HWND, return false; } - redirectWheelEvent(window, QCursor::pos(), delta, Qt::Horizontal, Qt::NoModifier); + redirectWheelEvent(window, msg.time, QCursor::pos(), delta, Qt::Horizontal, Qt::NoModifier); return true; } @@ -577,15 +577,14 @@ bool QWindowsMouseHandler::translateTouchEvent(QWindow *window, HWND, const QRect screenGeometry = screen->geometry(); const int winTouchPointCount = int(msg.wParam); - QScopedArrayPointer<TOUCHINPUT> winTouchInputs(new TOUCHINPUT[winTouchPointCount]); - memset(winTouchInputs.data(), 0, sizeof(TOUCHINPUT) * size_t(winTouchPointCount)); + const auto winTouchInputs = std::make_unique<TOUCHINPUT[]>(winTouchPointCount); QTouchPointList touchPoints; touchPoints.reserve(winTouchPointCount); QEventPoint::States allStates; GetTouchInputInfo(reinterpret_cast<HTOUCHINPUT>(msg.lParam), - UINT(msg.wParam), winTouchInputs.data(), sizeof(TOUCHINPUT)); + UINT(msg.wParam), winTouchInputs.get(), sizeof(TOUCHINPUT)); for (int i = 0; i < winTouchPointCount; ++i) { const TOUCHINPUT &winTouchInput = winTouchInputs[i]; int id = m_touchInputIDToTouchPointID.value(winTouchInput.dwID, -1); @@ -632,10 +631,12 @@ bool QWindowsMouseHandler::translateTouchEvent(QWindow *window, HWND, if (allStates == QEventPoint::State::Released) m_touchInputIDToTouchPointID.clear(); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); QWindowSystemInterface::handleTouchEvent(window, + msg.time, m_touchDevice.data(), touchPoints, - QWindowsKeyMapper::queryKeyboardModifiers()); + keyMapper->queryKeyboardModifiers()); return true; } diff --git a/src/plugins/platforms/windows/qwindowsole.cpp b/src/plugins/platforms/windows/qwindowsole.cpp index 31b851ce17..c01f716054 100644 --- a/src/plugins/platforms/windows/qwindowsole.cpp +++ b/src/plugins/platforms/windows/qwindowsole.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwindowsole.h" -#include "qwindowsmime.h" +#include "qwindowsmimeregistry.h" #include "qwindowscontext.h" \ #include <QtGui/qevent.h> @@ -66,7 +66,7 @@ QWindowsOleDataObject::GetData(LPFORMATETC pformatetc, LPSTGMEDIUM pmedium) HRESULT hr = ResultFromScode(DATA_E_FORMATETC); if (data) { - const QWindowsMimeConverter &mc = QWindowsContext::instance()->mimeConverter(); + const QWindowsMimeRegistry &mc = QWindowsContext::instance()->mimeConverter(); if (auto converter = mc.converterFromMime(*pformatetc, data)) if (converter->convertFromMime(*pformatetc, data, pmedium)) hr = ResultFromScode(S_OK); @@ -93,7 +93,7 @@ QWindowsOleDataObject::QueryGetData(LPFORMATETC pformatetc) qCDebug(lcQpaMime) << __FUNCTION__; if (data) { - const QWindowsMimeConverter &mc = QWindowsContext::instance()->mimeConverter(); + const QWindowsMimeRegistry &mc = QWindowsContext::instance()->mimeConverter(); hr = mc.converterFromMime(*pformatetc, data) ? ResultFromScode(S_OK) : ResultFromScode(S_FALSE); } @@ -144,7 +144,7 @@ QWindowsOleDataObject::EnumFormatEtc(DWORD dwDirection, LPENUMFORMATETC FAR* ppe QList<FORMATETC> fmtetcs; if (dwDirection == DATADIR_GET) { - QWindowsMimeConverter &mc = QWindowsContext::instance()->mimeConverter(); + QWindowsMimeRegistry &mc = QWindowsContext::instance()->mimeConverter(); fmtetcs = mc.allFormatsForMime(data); } else { FORMATETC formatetc; diff --git a/src/plugins/platforms/windows/qwindowsole.h b/src/plugins/platforms/windows/qwindowsole.h index b687384684..016f9dd04c 100644 --- a/src/plugins/platforms/windows/qwindowsole.h +++ b/src/plugins/platforms/windows/qwindowsole.h @@ -4,12 +4,12 @@ #ifndef QWINDOWSOLE_H #define QWINDOWSOLE_H -#include "qwindowscombase.h" #include <QtCore/qt_windows.h> #include <QtCore/qlist.h> #include <QtCore/qmap.h> #include <QtCore/qpointer.h> +#include <QtCore/private/qcomobject_p.h> #include <objidl.h> @@ -18,7 +18,7 @@ QT_BEGIN_NAMESPACE class QMimeData; class QWindow; -class QWindowsOleDataObject : public QWindowsComBase<IDataObject> +class QWindowsOleDataObject : public QComObject<IDataObject> { public: explicit QWindowsOleDataObject(QMimeData *mimeData); @@ -47,7 +47,7 @@ private: DWORD performedEffect = DROPEFFECT_NONE; }; -class QWindowsOleEnumFmtEtc : public QWindowsComBase<IEnumFORMATETC> +class QWindowsOleEnumFmtEtc : public QComObject<IEnumFORMATETC> { public: explicit QWindowsOleEnumFmtEtc(const QList<FORMATETC> &fmtetcs); diff --git a/src/plugins/platforms/windows/qwindowsopengltester.cpp b/src/plugins/platforms/windows/qwindowsopengltester.cpp index 19b60a4542..6a790bcc1b 100644 --- a/src/plugins/platforms/windows/qwindowsopengltester.cpp +++ b/src/plugins/platforms/windows/qwindowsopengltester.cpp @@ -15,6 +15,7 @@ #include <QtCore/qlibraryinfo.h> #include <QtCore/qhash.h> #include <private/qsystemlibrary_p.h> +#include <QtGui/qtgui-config.h> #ifndef QT_NO_OPENGL #include <private/qopengl_p.h> diff --git a/src/plugins/platforms/windows/qwindowspointerhandler.cpp b/src/plugins/platforms/windows/qwindowspointerhandler.cpp index 943eaa2b0a..71c7217671 100644 --- a/src/plugins/platforms/windows/qwindowspointerhandler.cpp +++ b/src/plugins/platforms/windows/qwindowspointerhandler.cpp @@ -1,9 +1,7 @@ // Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#ifndef WINVER -# define WINVER 0x0A00 // Enable pointer functions for MinGW -#endif +#include <QtCore/qt_windows.h> #include "qwindowspointerhandler.h" #include "qwindowsmousehandler.h" @@ -430,8 +428,9 @@ bool QWindowsPointerHandler::translateTouchEvent(QWindow *window, HWND hwnd, return false; if (msg.message == WM_POINTERCAPTURECHANGED) { - QWindowSystemInterface::handleTouchCancelEvent(window, m_touchDevice.data(), - QWindowsKeyMapper::queryKeyboardModifiers()); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + QWindowSystemInterface::handleTouchCancelEvent(window, msg.time, m_touchDevice.data(), + keyMapper->queryKeyboardModifiers()); m_lastTouchPoints.clear(); return true; } @@ -443,6 +442,8 @@ bool QWindowsPointerHandler::translateTouchEvent(QWindow *window, HWND hwnd, if (id != -1) m_lastTouchPoints.remove(id); } + // Send LeaveEvent to reset hover when the last finger leaves the touch screen (QTBUG-62912) + QWindowSystemInterface::handleEnterLeaveEvent(nullptr, window); } // Only handle down/up/update, ignore others like WM_POINTERENTER, WM_POINTERLEAVE, etc. @@ -524,7 +525,7 @@ bool QWindowsPointerHandler::translateTouchEvent(QWindow *window, HWND hwnd, // Some devices send touches for each finger in a different message/frame, instead of consolidating // them in the same frame as we were expecting. We account for missing unreleased touches here. - for (auto tp : qAsConst(m_lastTouchPoints)) { + for (auto tp : std::as_const(m_lastTouchPoints)) { if (!inputIds.contains(tp.id)) { tp.state = QEventPoint::State::Stationary; allStates |= tp.state; @@ -539,8 +540,9 @@ bool QWindowsPointerHandler::translateTouchEvent(QWindow *window, HWND hwnd, if (allStates == QEventPoint::State::Released) m_touchInputIDToTouchPointID.clear(); - QWindowSystemInterface::handleTouchEvent(window, m_touchDevice.data(), touchPoints, - QWindowsKeyMapper::queryKeyboardModifiers()); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + QWindowSystemInterface::handleTouchEvent(window, msg.time, m_touchDevice.data(), touchPoints, + keyMapper->queryKeyboardModifiers()); return false; // Allow mouse messages to be generated. } @@ -637,7 +639,7 @@ bool QWindowsPointerHandler::translatePenEvent(QWindow *window, HWND hwnd, QtWin switch (msg.message) { case WM_POINTERENTER: { - QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(window, device.data(), true); + QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(window, msg.time, device.data(), true); m_windowUnderPointer = window; // The local coordinates may fall outside the window. // Wait until the next update to send the enter event. @@ -650,7 +652,7 @@ bool QWindowsPointerHandler::translatePenEvent(QWindow *window, HWND hwnd, QtWin m_windowUnderPointer = nullptr; m_currentWindow = nullptr; } - QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(window, device.data(), false); + QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(window, msg.time, device.data(), false); break; case WM_POINTERDOWN: case WM_POINTERUP: @@ -673,9 +675,10 @@ bool QWindowsPointerHandler::translatePenEvent(QWindow *window, HWND hwnd, QtWin wumPlatformWindow->applyCursor(); } } - const Qt::KeyboardModifiers keyModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + const Qt::KeyboardModifiers keyModifiers = keyMapper->queryKeyboardModifiers(); - QWindowSystemInterface::handleTabletEvent(target, device.data(), + QWindowSystemInterface::handleTabletEvent(target, msg.time, device.data(), localPos, hiResGlobalPos, mouseButtons, pressure, xTilt, yTilt, tangentialPressure, rotation, z, keyModifiers); @@ -726,7 +729,7 @@ bool QWindowsPointerHandler::translateMouseWheelEvent(QWindow *window, QPoint localPos = QWindowsGeometryHint::mapFromGlobal(receiver, globalPos); - QWindowSystemInterface::handleWheelEvent(receiver, localPos, globalPos, QPoint(), angleDelta, keyModifiers); + QWindowSystemInterface::handleWheelEvent(receiver, msg.time, localPos, globalPos, QPoint(), angleDelta, keyModifiers); return true; } @@ -739,20 +742,20 @@ bool QWindowsPointerHandler::translateMouseEvent(QWindow *window, { *result = 0; - QPoint eventPos(GET_X_LPARAM(msg.lParam), GET_Y_LPARAM(msg.lParam)); - if ((et & QtWindows::NonClientEventFlag) == 0 && QWindowsBaseWindow::isRtlLayout(hwnd)) { - RECT clientArea; - GetClientRect(hwnd, &clientArea); - eventPos.setX(clientArea.right - eventPos.x()); - } - QPoint localPos; QPoint globalPos; + QPoint eventPos(GET_X_LPARAM(msg.lParam), GET_Y_LPARAM(msg.lParam)); if ((et == QtWindows::MouseWheelEvent) || (et & QtWindows::NonClientEventFlag)) { globalPos = eventPos; localPos = QWindowsGeometryHint::mapFromGlobal(hwnd, eventPos); } else { + if (QWindowsBaseWindow::isRtlLayout(hwnd)) { + RECT clientArea; + GetClientRect(hwnd, &clientArea); + eventPos.setX(clientArea.right - eventPos.x()); + } + globalPos = QWindowsGeometryHint::mapToGlobal(hwnd, eventPos); auto targetHwnd = hwnd; if (auto *pw = window->handle()) @@ -762,7 +765,8 @@ bool QWindowsPointerHandler::translateMouseEvent(QWindow *window, : QWindowsGeometryHint::mapFromGlobal(targetHwnd, globalPos); } - const Qt::KeyboardModifiers keyModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + const Qt::KeyboardModifiers keyModifiers = keyMapper->queryKeyboardModifiers(); QWindow *currentWindowUnderPointer = getWindowUnderPointer(window, globalPos); if (et == QtWindows::MouseWheelEvent) @@ -774,7 +778,7 @@ bool QWindowsPointerHandler::translateMouseEvent(QWindow *window, // X11 and macOS. bool discardEvent = false; if (msg.message == WM_MOUSEMOVE) { - static QPoint lastMouseMovePos; + Q_CONSTINIT static QPoint lastMouseMovePos; if (msg.wParam == 0 && (m_windowUnderPointer.isNull() || globalPos == lastMouseMovePos)) discardEvent = true; lastMouseMovePos = globalPos; @@ -798,8 +802,8 @@ bool QWindowsPointerHandler::translateMouseEvent(QWindow *window, break; case QT_PT_PEN: #if QT_CONFIG(tabletevent) - if (!m_activeTabletDevice.isNull()) - device = m_activeTabletDevice.data(); + qCDebug(lcQpaTablet) << "ignoring synth-mouse event for tablet event from" << device; + return false; #endif break; } @@ -822,19 +826,16 @@ bool QWindowsPointerHandler::translateMouseEvent(QWindow *window, if (m_lastEventType == QEvent::NonClientAreaMouseButtonPress && (mouseEvent.type == QEvent::NonClientAreaMouseMove || mouseEvent.type == QEvent::MouseMove) && (m_lastEventButton & mouseButtons) == 0) { - if (mouseEvent.type == QEvent::NonClientAreaMouseMove) { - QWindowSystemInterface::handleFrameStrutMouseEvent(window, device, localPos, globalPos, mouseButtons, m_lastEventButton, - QEvent::NonClientAreaMouseButtonRelease, keyModifiers, source); - } else { - QWindowSystemInterface::handleMouseEvent(window, device, localPos, globalPos, mouseButtons, m_lastEventButton, - QEvent::MouseButtonRelease, keyModifiers, source); - } + auto releaseType = mouseEvent.type == QEvent::NonClientAreaMouseMove ? + QEvent::NonClientAreaMouseButtonRelease : QEvent::MouseButtonRelease; + QWindowSystemInterface::handleMouseEvent(window, msg.time, device, localPos, globalPos, mouseButtons, m_lastEventButton, + releaseType, keyModifiers, source); } m_lastEventType = mouseEvent.type; m_lastEventButton = mouseEvent.button; if (mouseEvent.type >= QEvent::NonClientAreaMouseMove && mouseEvent.type <= QEvent::NonClientAreaMouseButtonDblClick) { - QWindowSystemInterface::handleFrameStrutMouseEvent(window, device, localPos, globalPos, mouseButtons, + QWindowSystemInterface::handleMouseEvent(window, msg.time, device, localPos, globalPos, mouseButtons, mouseEvent.button, mouseEvent.type, keyModifiers, source); return false; // Allow further event processing } @@ -854,7 +855,7 @@ bool QWindowsPointerHandler::translateMouseEvent(QWindow *window, handleEnterLeave(window, currentWindowUnderPointer, globalPos); if (!discardEvent && mouseEvent.type != QEvent::None) { - QWindowSystemInterface::handleMouseEvent(window, device, localPos, globalPos, mouseButtons, + QWindowSystemInterface::handleMouseEvent(window, msg.time, device, localPos, globalPos, mouseButtons, mouseEvent.button, mouseEvent.type, keyModifiers, source); } diff --git a/src/plugins/platforms/windows/qwindowsscreen.cpp b/src/plugins/platforms/windows/qwindowsscreen.cpp index 6f0b949b67..1f22fb4f60 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.cpp +++ b/src/plugins/platforms/windows/qwindowsscreen.cpp @@ -14,12 +14,22 @@ #include <QtGui/qpixmap.h> #include <QtGui/qguiapplication.h> #include <qpa/qwindowsysteminterface.h> +#include <QtCore/private/qsystemerror_p.h> +#include <QtGui/private/qedidparser_p.h> #include <private/qhighdpiscaling_p.h> #include <private/qwindowsfontdatabasebase_p.h> +#include <private/qpixmap_win_p.h> +#include <private/quniquehandle_p.h> + #include <QtGui/qscreen.h> #include <QtCore/qdebug.h> +#include <memory> +#include <type_traits> + +#include <cfgmgr32.h> +#include <setupapi.h> #include <shellscalingapi.h> QT_BEGIN_NAMESPACE @@ -40,7 +50,7 @@ static inline QDpi monitorDPI(HMONITOR hMonitor) return {0, 0}; } -static bool getPathInfo(const MONITORINFOEX &viewInfo, DISPLAYCONFIG_PATH_INFO *pathInfo) +static std::vector<DISPLAYCONFIG_PATH_INFO> getPathInfo(const MONITORINFOEX &viewInfo) { // We might want to consider storing adapterId/id from DISPLAYCONFIG_PATH_TARGET_INFO. std::vector<DISPLAYCONFIG_PATH_INFO> pathInfos; @@ -56,7 +66,7 @@ static bool getPathInfo(const MONITORINFOEX &viewInfo, DISPLAYCONFIG_PATH_INFO * // look up the needed buffer sizes. if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPathArrayElements, &numModeInfoArrayElements) != ERROR_SUCCESS) { - return false; + return {}; } pathInfos.resize(numPathArrayElements); modeInfos.resize(numModeInfoArrayElements); @@ -65,24 +75,25 @@ static bool getPathInfo(const MONITORINFOEX &viewInfo, DISPLAYCONFIG_PATH_INFO * } while (result == ERROR_INSUFFICIENT_BUFFER); if (result != ERROR_SUCCESS) - return false; - - // Find path matching monitor name - for (uint32_t p = 0; p < numPathArrayElements; p++) { - DISPLAYCONFIG_SOURCE_DEVICE_NAME deviceName; - deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; - deviceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME); - deviceName.header.adapterId = pathInfos[p].sourceInfo.adapterId; - deviceName.header.id = pathInfos[p].sourceInfo.id; - if (DisplayConfigGetDeviceInfo(&deviceName.header) == ERROR_SUCCESS) { - if (wcscmp(viewInfo.szDevice, deviceName.viewGdiDeviceName) == 0) { - *pathInfo = pathInfos[p]; + return {}; + + // Find paths matching monitor name + auto discardThese = + std::remove_if(pathInfos.begin(), pathInfos.end(), [&](const auto &path) -> bool { + DISPLAYCONFIG_SOURCE_DEVICE_NAME deviceName; + deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + deviceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME); + deviceName.header.adapterId = path.sourceInfo.adapterId; + deviceName.header.id = path.sourceInfo.id; + if (DisplayConfigGetDeviceInfo(&deviceName.header) == ERROR_SUCCESS) { + return wcscmp(viewInfo.szDevice, deviceName.viewGdiDeviceName) != 0; + } return true; - } - } - } + }); + + pathInfos.erase(discardThese, pathInfos.end()); - return false; + return pathInfos; } #if 0 @@ -106,6 +117,149 @@ static float getMonitorSDRWhiteLevel(DISPLAYCONFIG_PATH_TARGET_INFO *targetInfo) using WindowsScreenDataList = QList<QWindowsScreenData>; +namespace { + +struct DiRegKeyHandleTraits +{ + using Type = HKEY; + static Type invalidValue() + { + // The setupapi.h functions return INVALID_HANDLE_VALUE when failing to open a registry key + return reinterpret_cast<HKEY>(INVALID_HANDLE_VALUE); + } + static bool close(Type handle) { return RegCloseKey(handle) == ERROR_SUCCESS; } +}; + +using DiRegKeyHandle = QUniqueHandle<DiRegKeyHandleTraits>; + +} + +static void setMonitorDataFromSetupApi(QWindowsScreenData &data, + const std::vector<DISPLAYCONFIG_PATH_INFO> &pathGroup) +{ + if (pathGroup.empty()) { + return; + } + + // The only property shared among monitors in a clone group is deviceName + { + DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName = {}; + deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + deviceName.header.size = sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME); + // The first element in the clone group is the main monitor. + deviceName.header.adapterId = pathGroup[0].targetInfo.adapterId; + deviceName.header.id = pathGroup[0].targetInfo.id; + if (DisplayConfigGetDeviceInfo(&deviceName.header) == ERROR_SUCCESS) { + data.devicePath = QString::fromWCharArray(deviceName.monitorDevicePath); + } else { + qCWarning(lcQpaScreen) + << u"Unable to get device information for %1:"_s.arg(pathGroup[0].targetInfo.id) + << QSystemError::windowsString(); + } + } + + // The rest must be concatenated into the resulting property + QStringList names; + QStringList manufacturers; + QStringList models; + QStringList serialNumbers; + + for (const auto &path : pathGroup) { + DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName = {}; + deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + deviceName.header.size = sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME); + deviceName.header.adapterId = path.targetInfo.adapterId; + deviceName.header.id = path.targetInfo.id; + if (DisplayConfigGetDeviceInfo(&deviceName.header) != ERROR_SUCCESS) { + qCWarning(lcQpaScreen) + << u"Unable to get device information for %1:"_s.arg(path.targetInfo.id) + << QSystemError::windowsString(); + continue; + } + + // https://learn.microsoft.com/en-us/windows-hardware/drivers/install/guid-devinterface-monitor + constexpr GUID GUID_DEVINTERFACE_MONITOR = { + 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } + }; + const HDEVINFO devInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_MONITOR, nullptr, nullptr, + DIGCF_DEVICEINTERFACE); + + SP_DEVICE_INTERFACE_DATA deviceInterfaceData{}; + deviceInterfaceData.cbSize = sizeof(deviceInterfaceData); + + if (!SetupDiOpenDeviceInterfaceW(devInfo, deviceName.monitorDevicePath, DIODI_NO_ADD, + &deviceInterfaceData)) { + qCWarning(lcQpaScreen) + << u"Unable to open monitor interface to %1:"_s.arg(data.deviceName) + << QSystemError::windowsString(); + continue; + } + + DWORD requiredSize{ 0 }; + if (SetupDiGetDeviceInterfaceDetailW(devInfo, &deviceInterfaceData, nullptr, 0, + &requiredSize, nullptr) + || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + continue; + } + + const std::unique_ptr<std::byte[]> storage(new std::byte[requiredSize]); + auto *devicePath = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W *>(storage.get()); + devicePath->cbSize = sizeof(std::remove_pointer_t<decltype(devicePath)>); + SP_DEVINFO_DATA deviceInfoData{}; + deviceInfoData.cbSize = sizeof(deviceInfoData); + if (!SetupDiGetDeviceInterfaceDetailW(devInfo, &deviceInterfaceData, devicePath, + requiredSize, nullptr, &deviceInfoData)) { + qCDebug(lcQpaScreen) << u"Unable to get monitor metadata for %1:"_s.arg(data.deviceName) + << QSystemError::windowsString(); + continue; + } + + const DiRegKeyHandle edidRegistryKey{ SetupDiOpenDevRegKey( + devInfo, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ) }; + + if (!edidRegistryKey.isValid()) + continue; + + DWORD edidDataSize{ 0 }; + if (RegQueryValueExW(edidRegistryKey.get(), L"EDID", nullptr, nullptr, nullptr, + &edidDataSize) + != ERROR_SUCCESS) { + continue; + } + + QByteArray edidData; + edidData.resize(edidDataSize); + + if (RegQueryValueExW(edidRegistryKey.get(), L"EDID", nullptr, nullptr, + reinterpret_cast<unsigned char *>(edidData.data()), &edidDataSize) + != ERROR_SUCCESS) { + qCDebug(lcQpaScreen) << u"Unable to get EDID from the Registry for %1:"_s.arg( + data.deviceName) + << QSystemError::windowsString(); + continue; + } + + QEdidParser edid; + + if (!edid.parse(edidData)) { + qCDebug(lcQpaScreen) << "Invalid EDID blob for" << data.deviceName; + continue; + } + + // We skip edid.identifier because it is unreliable, and a better option + // is already available through DisplayConfigGetDeviceInfo (see below). + names << QString::fromWCharArray(deviceName.monitorFriendlyDeviceName); + manufacturers << edid.manufacturer; + models << edid.model; + serialNumbers << edid.serialNumber; + } + + data.name = names.join(u"|"_s); + data.manufacturer = manufacturers.join(u"|"_s); + data.model = models.join(u"|"_s); + data.serialNumber = serialNumbers.join(u"|"_s); +} + static bool monitorData(HMONITOR hMonitor, QWindowsScreenData *data) { MONITORINFOEX info; @@ -118,16 +272,9 @@ static bool monitorData(HMONITOR hMonitor, QWindowsScreenData *data) data->geometry = QRect(QPoint(info.rcMonitor.left, info.rcMonitor.top), QPoint(info.rcMonitor.right - 1, info.rcMonitor.bottom - 1)); data->availableGeometry = QRect(QPoint(info.rcWork.left, info.rcWork.top), QPoint(info.rcWork.right - 1, info.rcWork.bottom - 1)); data->deviceName = QString::fromWCharArray(info.szDevice); - DISPLAYCONFIG_PATH_INFO pathInfo = {}; - const bool hasPathInfo = getPathInfo(info, &pathInfo); - if (hasPathInfo) { - DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName = {}; - deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; - deviceName.header.size = sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME); - deviceName.header.adapterId = pathInfo.targetInfo.adapterId; - deviceName.header.id = pathInfo.targetInfo.id; - if (DisplayConfigGetDeviceInfo(&deviceName.header) == ERROR_SUCCESS) - data->name = QString::fromWCharArray(deviceName.monitorFriendlyDeviceName); + const auto pathGroup = getPathInfo(info); + if (!pathGroup.empty()) { + setMonitorDataFromSetupApi(*data, pathGroup); } if (data->name.isEmpty()) data->name = data->deviceName; @@ -153,7 +300,9 @@ static bool monitorData(HMONITOR hMonitor, QWindowsScreenData *data) // ### We might want to consider storing adapterId/id from DISPLAYCONFIG_PATH_TARGET_INFO, // if we are going to use DISPLAYCONFIG lookups more. - if (hasPathInfo) { + if (!pathGroup.empty()) { + // The first element in the clone group is the main monitor. + const auto &pathInfo = pathGroup[0]; switch (pathInfo.targetInfo.rotation) { case DISPLAYCONFIG_ROTATION_IDENTITY: data->orientation = Qt::LandscapeOrientation; @@ -182,11 +331,8 @@ static bool monitorData(HMONITOR hMonitor, QWindowsScreenData *data) // EnumDisplayMonitors (as opposed to EnumDisplayDevices) enumerates only // virtual desktop screens. data->flags |= QWindowsScreenData::VirtualDesktop; - if (info.dwFlags & MONITORINFOF_PRIMARY) { + if (info.dwFlags & MONITORINFOF_PRIMARY) data->flags |= QWindowsScreenData::PrimaryScreen; - if ((data->flags & QWindowsScreenData::LockScreen) == 0) - QWindowsFontDatabase::setDefaultVerticalDPI(data->dpi.second); - } return true; } @@ -232,15 +378,14 @@ static QDebug operator<<(QDebug dbg, const QWindowsScreenData &d) QDebugStateSaver saver(dbg); dbg.nospace(); dbg.noquote(); - dbg << "Screen \"" << d.name << "\" " - << d.geometry.width() << 'x' << d.geometry.height() << '+' << d.geometry.x() << '+' << d.geometry.y() - << " avail: " - << d.availableGeometry.width() << 'x' << d.availableGeometry.height() << '+' << d.availableGeometry.x() << '+' << d.availableGeometry.y() - << " physical: " << d.physicalSizeMM.width() << 'x' << d.physicalSizeMM.height() - << " DPI: " << d.dpi.first << 'x' << d.dpi.second << " Depth: " << d.depth - << " Format: " << d.format - << " hMonitor: " << d.hMonitor - << " device name: " << d.deviceName; + dbg << "Screen \"" << d.name << "\" " << d.geometry.width() << 'x' << d.geometry.height() << '+' + << d.geometry.x() << '+' << d.geometry.y() << " avail: " << d.availableGeometry.width() + << 'x' << d.availableGeometry.height() << '+' << d.availableGeometry.x() << '+' + << d.availableGeometry.y() << " physical: " << d.physicalSizeMM.width() << 'x' + << d.physicalSizeMM.height() << " DPI: " << d.dpi.first << 'x' << d.dpi.second + << " Depth: " << d.depth << " Format: " << d.format << " hMonitor: " << d.hMonitor + << " device name: " << d.deviceName << " manufacturer: " << d.manufacturer + << " model: " << d.model << " serial number: " << d.serialNumber; if (d.flags & QWindowsScreenData::PrimaryScreen) dbg << " primary"; if (d.flags & QWindowsScreenData::VirtualDesktop) @@ -273,8 +418,6 @@ QString QWindowsScreen::name() const : m_data.name; } -Q_GUI_EXPORT QPixmap qt_pixmapFromWinHBITMAP(HBITMAP bitmap, int hbitmapFormat = 0); - QPixmap QWindowsScreen::grabWindow(WId window, int xIn, int yIn, int width, int height) const { QSize windowSize; @@ -393,10 +536,14 @@ void QWindowsScreen::handleChanges(const QWindowsScreenData &newData) const bool dpiChanged = !qFuzzyCompare(m_data.dpi.first, newData.dpi.first) || !qFuzzyCompare(m_data.dpi.second, newData.dpi.second); const bool orientationChanged = m_data.orientation != newData.orientation; + const bool primaryChanged = (newData.flags & QWindowsScreenData::PrimaryScreen) + && !(m_data.flags & QWindowsScreenData::PrimaryScreen); m_data.dpi = newData.dpi; m_data.orientation = newData.orientation; m_data.geometry = newData.geometry; m_data.availableGeometry = newData.availableGeometry; + m_data.flags = (m_data.flags & ~QWindowsScreenData::PrimaryScreen) + | (newData.flags & QWindowsScreenData::PrimaryScreen); if (dpiChanged) { QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(screen(), @@ -409,6 +556,8 @@ void QWindowsScreen::handleChanges(const QWindowsScreenData &newData) QWindowSystemInterface::handleScreenGeometryChange(screen(), newData.geometry, newData.availableGeometry); } + if (primaryChanged) + QWindowSystemInterface::handlePrimaryScreenChanged(this); } HMONITOR QWindowsScreen::handle() const @@ -520,7 +669,8 @@ extern "C" LRESULT QT_WIN_CALLBACK qDisplayChangeObserverWndProc(HWND hwnd, UINT if (QWindowsTheme *t = QWindowsTheme::instance()) t->displayChanged(); QWindowsWindow::displayChanged(); - QWindowsContext::instance()->screenManager().handleScreenChanges(); + if (auto *context = QWindowsContext::instance()) + context->screenManager().handleScreenChanges(); } return DefWindowProc(hwnd, message, wParam, lParam); @@ -548,11 +698,15 @@ void QWindowsScreenManager::initialize() handleScreenChanges(); } -QWindowsScreenManager::~QWindowsScreenManager() +void QWindowsScreenManager::destroyWindow() { + qCDebug(lcQpaScreen) << "Destroying display change observer" << m_displayChangeObserver; DestroyWindow(m_displayChangeObserver); + m_displayChangeObserver = nullptr; } +QWindowsScreenManager::~QWindowsScreenManager() = default; + bool QWindowsScreenManager::isSingleScreen() { return QWindowsContext::instance()->screenManager().screens().size() < 2; diff --git a/src/plugins/platforms/windows/qwindowsscreen.h b/src/plugins/platforms/windows/qwindowsscreen.h index 0635f172ad..ea6a29efe3 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.h +++ b/src/plugins/platforms/windows/qwindowsscreen.h @@ -7,9 +7,9 @@ #include "qtwindowsglobal.h" #include <QtCore/qlist.h> -#include <QtCore/qpair.h> #include <QtCore/qscopedpointer.h> #include <qpa/qplatformscreen.h> +#include <QtGui/qscreen_platform.h> QT_BEGIN_NAMESPACE @@ -30,14 +30,18 @@ struct QWindowsScreenData QImage::Format format = QImage::Format_ARGB32_Premultiplied; unsigned flags = VirtualDesktop; QString name; + QString manufacturer; + QString model; + QString serialNumber; Qt::ScreenOrientation orientation = Qt::LandscapeOrientation; qreal refreshRateHz = 60; HMONITOR hMonitor = nullptr; - QString deviceName = {}; + QString deviceName; + QString devicePath; std::optional<int> deviceIndex = std::nullopt; }; -class QWindowsScreen : public QPlatformScreen +class QWindowsScreen : public QPlatformScreen, public QNativeInterface::QWindowsScreen { public: #ifndef QT_NO_CURSOR @@ -56,6 +60,9 @@ public: qreal devicePixelRatio() const override { return 1.0; } qreal refreshRate() const override { return m_data.refreshRateHz; } QString name() const override; + QString manufacturer() const override { return m_data.manufacturer; } + QString model() const override { return m_data.model; } + QString serialNumber() const override { return m_data.serialNumber; } Qt::ScreenOrientation orientation() const override { return m_data.orientation; } QList<QPlatformScreen *> virtualSiblings() const override; QWindow *topLevelAt(const QPoint &point) const override; @@ -69,7 +76,7 @@ public: inline void handleChanges(const QWindowsScreenData &newData); - HMONITOR handle() const; + HMONITOR handle() const override; #ifndef QT_NO_CURSOR QPlatformCursor *cursor() const override { return m_cursor.data(); } @@ -98,6 +105,7 @@ public: QWindowsScreenManager(); void initialize(); + void destroyWindow(); ~QWindowsScreenManager(); void clearScreens(); diff --git a/src/plugins/platforms/windows/qwindowsservices.cpp b/src/plugins/platforms/windows/qwindowsservices.cpp index fbad934a05..89f93fd161 100644 --- a/src/plugins/platforms/windows/qwindowsservices.cpp +++ b/src/plugins/platforms/windows/qwindowsservices.cpp @@ -1,7 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#define QT_NO_URL_CAST_FROM_STRING #include "qwindowsservices.h" #include <QtCore/qt_windows.h> @@ -12,8 +11,10 @@ #include <QtCore/qthread.h> #include <QtCore/private/qwinregistry_p.h> +#include <QtCore/private/qfunctions_win_p.h> #include <shlobj.h> +#include <shlwapi.h> #include <intshcut.h> QT_BEGIN_NAMESPACE @@ -25,23 +26,73 @@ enum { debug = 0 }; class QWindowsShellExecuteThread : public QThread { public: - explicit QWindowsShellExecuteThread(const wchar_t *path) : m_path(path) { } + explicit QWindowsShellExecuteThread(const wchar_t *operation, const wchar_t *file, + const wchar_t *parameters) + : m_operation(operation) + , m_file(file) + , m_parameters(parameters) { } void run() override { - if (SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) { - m_result = ShellExecute(nullptr, nullptr, m_path, nullptr, nullptr, SW_SHOWNORMAL); - CoUninitialize(); - } + QComHelper comHelper; + if (comHelper.isValid()) + m_result = ShellExecute(nullptr, m_operation, m_file, m_parameters, nullptr, + SW_SHOWNORMAL); } HINSTANCE result() const { return m_result; } private: HINSTANCE m_result = nullptr; - const wchar_t *m_path; + const wchar_t *m_operation; + const wchar_t *m_file; + const wchar_t *m_parameters; }; +static QString msgShellExecuteFailed(const QUrl &url, quintptr code) +{ + QString result; + QTextStream(&result) <<"ShellExecute '" << url.toString() << "' failed (error " << code << ")."; + return result; +} + +// Retrieve the web browser and open the URL. This should be used for URLs with +// fragments which don't work when using ShellExecute() directly (QTBUG-14460, +// QTBUG-55300). +static bool openWebBrowser(const QUrl &url) +{ + WCHAR browserExecutable[MAX_PATH] = {}; + const wchar_t operation[] = L"open"; + DWORD browserExecutableSize = MAX_PATH; + if (FAILED(AssocQueryString(0, ASSOCSTR_EXECUTABLE, L"http", operation, + browserExecutable, &browserExecutableSize))) { + return false; + } + QString browser = QString::fromWCharArray(browserExecutable, browserExecutableSize - 1); + // Workaround for "old" MS Edge entries. Instead of LaunchWinApp.exe we can just use msedge.exe + if (browser.contains("LaunchWinApp.exe"_L1, Qt::CaseInsensitive)) + browser = "msedge.exe"_L1; + const QString urlS = url.toString(QUrl::FullyEncoded); + + // Run ShellExecute() in a thread since it may spin the event loop. + // Prevent it from interfering with processing of posted events (QTBUG-85676). + QWindowsShellExecuteThread thread(operation, + reinterpret_cast<const wchar_t *>(browser.utf16()), + reinterpret_cast<const wchar_t *>(urlS.utf16())); + thread.start(); + thread.wait(); + + const auto result = reinterpret_cast<quintptr>(thread.result()); + if (debug) + qDebug() << __FUNCTION__ << urlS << QString::fromWCharArray(browserExecutable) << result; + // ShellExecute returns a value greater than 32 if successful + if (result <= 32) { + qWarning("%s", qPrintable(msgShellExecuteFailed(url, result))); + return false; + } + return true; +} + static inline bool shellExecute(const QUrl &url) { const QString nativeFilePath = url.isLocalFile() && !url.hasFragment() && !url.hasQuery() @@ -51,7 +102,9 @@ static inline bool shellExecute(const QUrl &url) // Run ShellExecute() in a thread since it may spin the event loop. // Prevent it from interfering with processing of posted events (QTBUG-85676). - QWindowsShellExecuteThread thread(reinterpret_cast<const wchar_t *>(nativeFilePath.utf16())); + QWindowsShellExecuteThread thread(nullptr, + reinterpret_cast<const wchar_t *>(nativeFilePath.utf16()), + nullptr); thread.start(); thread.wait(); @@ -59,7 +112,7 @@ static inline bool shellExecute(const QUrl &url) // ShellExecute returns a value greater than 32 if successful if (result <= 32) { - qWarning("ShellExecute '%ls' failed (error %zu).", qUtf16Printable(url.toString()), result); + qWarning("%s", qPrintable(msgShellExecuteFailed(url, result))); return false; } return true; @@ -101,10 +154,15 @@ static inline bool launchMail(const QUrl &url) qWarning("Cannot launch '%ls': There is no mail program installed.", qUtf16Printable(url.toString())); return false; } + // Fix mail launch if no param is expected in this command. + if (command.indexOf("%1"_L1) < 0) { + qWarning() << "The mail command lacks the '%1' parameter."; + return false; + } //Make sure the path for the process is in quotes const QChar doubleQuote = u'"'; if (!command.startsWith(doubleQuote)) { - const int exeIndex = command.indexOf(QStringLiteral(".exe "), 0, Qt::CaseInsensitive); + const int exeIndex = command.indexOf(".exe "_L1, 0, Qt::CaseInsensitive); if (exeIndex != -1) { command.insert(exeIndex + 4, doubleQuote); command.prepend(doubleQuote); @@ -136,7 +194,8 @@ bool QWindowsServices::openUrl(const QUrl &url) const QString scheme = url.scheme(); if (scheme == u"mailto" && launchMail(url)) return true; - return shellExecute(url); + return url.isLocalFile() && url.hasFragment() + ? openWebBrowser(url) : shellExecute(url); } bool QWindowsServices::openDocument(const QUrl &url) diff --git a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp index cf150b5772..6f0680ac23 100644 --- a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp +++ b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp @@ -1,13 +1,7 @@ // Copyright (C) 2017 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#ifndef WINVER -# define WINVER 0x0A00 // required for NOTIFYICONDATA_V2_SIZE, ChangeWindowMessageFilterEx() (MinGW 5.3) -#endif - -#ifndef NTDDI_VERSION -# define NTDDI_VERSION 0x0A00000B // required for Shell_NotifyIconGetRect (MinGW 5.3) -#endif +#include <QtCore/qt_windows.h> #include "qwindowssystemtrayicon.h" #include "qwindowscontext.h" @@ -23,7 +17,6 @@ #include <QtCore/qsettings.h> #include <qpa/qwindowsysteminterface.h> -#include <qt_windows.h> #include <commctrl.h> #include <shellapi.h> #include <shlobj.h> @@ -31,6 +24,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + static const UINT q_uNOTIFYICONID = 0; static uint MYWM_TASKBARCREATED = 0; @@ -123,7 +118,7 @@ static inline HWND createTrayIconMessageWindow() return nullptr; // Register window class in the platform plugin. const QString className = - ctx->registerWindowClass(QWindowsContext::classNamePrefix() + QStringLiteral("TrayIconMessageWindowClass"), + ctx->registerWindowClass(QWindowsContext::classNamePrefix() + "TrayIconMessageWindowClass"_L1, qWindowsTrayIconWndProc); const wchar_t windowName[] = L"QTrayIconMessageWindow"; return CreateWindowEx(0, reinterpret_cast<const wchar_t *>(className.utf16()), @@ -169,8 +164,7 @@ void QWindowsSystemTrayIcon::cleanup() void QWindowsSystemTrayIcon::updateIcon(const QIcon &icon) { qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << icon << ')' << this; - if (icon.cacheKey() == m_icon.cacheKey()) - return; + m_icon = icon; const HICON hIconToDestroy = createIcon(icon); if (ensureInstalled()) sendTrayMessage(NIM_MODIFY); @@ -190,6 +184,9 @@ void QWindowsSystemTrayIcon::updateToolTip(const QString &tooltip) QRect QWindowsSystemTrayIcon::geometry() const { + if (!isIconVisible()) + return QRect(); + NOTIFYICONIDENTIFIER nid; memset(&nid, 0, sizeof(nid)); nid.cbSize = sizeof(nid); @@ -223,25 +220,19 @@ void QWindowsSystemTrayIcon::showMessage(const QString &title, const QString &me qStringToLimitedWCharArray(title, tnd.szInfoTitle, 64); tnd.uID = q_uNOTIFYICONID; - tnd.dwInfoFlags = NIIF_USER; - - QSize size(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); - const QSize largeIcon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); - const QSize more = icon.actualSize(largeIcon); - if (more.height() > (largeIcon.height() * 3/4) || more.width() > (largeIcon.width() * 3/4)) { - tnd.dwInfoFlags |= NIIF_LARGE_ICON; - size = largeIcon; - } + + const auto size = icon.actualSize(QSize(256, 256)); QPixmap pm = icon.pixmap(size); + if (m_hMessageIcon) { + DestroyIcon(m_hMessageIcon); + m_hMessageIcon = nullptr; + } if (pm.isNull()) { tnd.dwInfoFlags = NIIF_INFO; } else { - if (pm.size() != size) { - qWarning("QSystemTrayIcon::showMessage: Wrong icon size (%dx%d), please add standard one: %dx%d", - pm.size().width(), pm.size().height(), size.width(), size.height()); - pm = pm.scaled(size, Qt::IgnoreAspectRatio); - } - tnd.hBalloonIcon = qt_pixmapToWinHICON(pm); + tnd.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON; + m_hMessageIcon = qt_pixmapToWinHICON(pm); + tnd.hBalloonIcon = m_hMessageIcon; } tnd.hWnd = m_hwnd; tnd.uTimeout = msecsIn <= 0 ? UINT(10000) : UINT(msecsIn); // 10s default @@ -299,7 +290,10 @@ void QWindowsSystemTrayIcon::ensureCleanup() } if (m_hIcon != nullptr) DestroyIcon(m_hIcon); + if (m_hMessageIcon != nullptr) + DestroyIcon(m_hMessageIcon); m_hIcon = nullptr; + m_hMessageIcon = nullptr; m_menu = nullptr; // externally owned m_toolTip.clear(); } @@ -316,6 +310,29 @@ bool QWindowsSystemTrayIcon::setIconVisible(bool visible) return Shell_NotifyIcon(NIM_MODIFY, &tnd) == TRUE; } +bool QWindowsSystemTrayIcon::isIconVisible() const +{ + NOTIFYICONIDENTIFIER nid; + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(nid); + nid.hWnd = m_hwnd; + nid.uID = q_uNOTIFYICONID; + RECT rect; + const HRESULT hr = Shell_NotifyIconGetRect(&nid, &rect); + // Windows 10 returns S_FALSE if the icon is hidden + if (FAILED(hr) || hr == S_FALSE) + return false; + + HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO info; + info.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(monitor, &info); + // Windows 11 seems to return a geometry outside of the current monitor's geometry in case of + // the icon being hidden. As it's impossible to change the alignment of the task bar on Windows + // 11 this check should be fine. + return rect.bottom <= info.rcMonitor.bottom; +} + bool QWindowsSystemTrayIcon::sendTrayMessage(DWORD msg) { NOTIFYICONDATA tnd; @@ -409,8 +426,15 @@ bool QWindowsSystemTrayIcon::winEvent(const MSG &message, long *result) QWindowsPopupMenu::notifyTriggered(LOWORD(message.wParam)); break; default: - if (message.message == MYWM_TASKBARCREATED) // self-registered message id (tray crashed) + if (message.message == MYWM_TASKBARCREATED) { + // self-registered message id to handle that + // - screen resolution/DPR changed + const QIcon oldIcon = m_icon; + m_icon = QIcon(); // updateIcon is a no-op if the icon doesn't change + updateIcon(oldIcon); + // - or tray crashed sendTrayMessage(NIM_ADD); + } break; } return false; diff --git a/src/plugins/platforms/windows/qwindowssystemtrayicon.h b/src/plugins/platforms/windows/qwindowssystemtrayicon.h index 2545ae9101..3ad5feb125 100644 --- a/src/plugins/platforms/windows/qwindowssystemtrayicon.h +++ b/src/plugins/platforms/windows/qwindowssystemtrayicon.h @@ -49,12 +49,14 @@ private: void ensureCleanup(); bool sendTrayMessage(DWORD msg); bool setIconVisible(bool visible); + bool isIconVisible() const; HICON createIcon(const QIcon &icon); QIcon m_icon; QString m_toolTip; HWND m_hwnd = nullptr; HICON m_hIcon = nullptr; + HICON m_hMessageIcon = nullptr; mutable QPointer<QWindowsPopupMenu> m_menu; bool m_ignoreNextMouseRelease = false; bool m_visible = false; diff --git a/src/plugins/platforms/windows/qwindowstabletsupport.cpp b/src/plugins/platforms/windows/qwindowstabletsupport.cpp index b7cc2bf77b..ceebb483d2 100644 --- a/src/plugins/platforms/windows/qwindowstabletsupport.cpp +++ b/src/plugins/platforms/windows/qwindowstabletsupport.cpp @@ -595,7 +595,8 @@ bool QWindowsTabletSupport::translateTabletPacketEvent() << "mode=" << m_mode; } - const Qt::KeyboardModifiers keyboardModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const auto *keyMapper = QWindowsContext::instance()->keyMapper(); + const Qt::KeyboardModifiers keyboardModifiers = keyMapper->queryKeyboardModifiers(); for (int i = 0; i < packetCount ; ++i) { const PACKET &packet = localPacketBuf[i]; diff --git a/src/plugins/platforms/windows/qwindowstheme.cpp b/src/plugins/platforms/windows/qwindowstheme.cpp index eff177b16f..b6017c7692 100644 --- a/src/plugins/platforms/windows/qwindowstheme.cpp +++ b/src/plugins/platforms/windows/qwindowstheme.cpp @@ -1,37 +1,35 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -// SHSTOCKICONINFO is only available since Vista -#ifndef _WIN32_WINNT -# define _WIN32_WINNT 0x0A00 -#endif +#include <QtCore/qt_windows.h> #include "qwindowstheme.h" #include "qwindowsmenu.h" #include "qwindowsdialoghelpers.h" #include "qwindowscontext.h" +#include "qwindowsiconengine.h" #include "qwindowsintegration.h" #if QT_CONFIG(systemtrayicon) # include "qwindowssystemtrayicon.h" #endif #include "qwindowsscreen.h" -#include "qt_windows.h" +#include "qwindowswindow.h" #include <commctrl.h> #include <objbase.h> -#ifndef Q_CC_MINGW -# include <commoncontrols.h> -#endif +#include <commoncontrols.h> #include <shellapi.h> +#include <QtCore/qapplicationstatic.h> #include <QtCore/qvariant.h> #include <QtCore/qcoreapplication.h> #include <QtCore/qdebug.h> -#include <QtCore/qtextstream.h> #include <QtCore/qsysinfo.h> #include <QtCore/qcache.h> #include <QtCore/qthread.h> +#include <QtCore/qqueue.h> #include <QtCore/qmutex.h> #include <QtCore/qwaitcondition.h> +#include <QtCore/qoperatingsystemversion.h> #include <QtGui/qcolor.h> #include <QtGui/qpalette.h> #include <QtGui/qguiapplication.h> @@ -43,33 +41,20 @@ #include <private/qhighdpiscaling_p.h> #include <private/qsystemlibrary_p.h> #include <private/qwinregistry_p.h> +#include <QtCore/private/qfunctions_win_p.h> #include <algorithm> -#if defined(__IImageList_INTERFACE_DEFINED__) && defined(__IID_DEFINED__) -# define USE_IIMAGELIST -#endif +#if QT_CONFIG(cpp_winrt) +# include <QtCore/private/qt_winrtbase_p.h> + +# include <winrt/Windows.UI.ViewManagement.h> +#endif // QT_CONFIG(cpp_winrt) QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -static inline QColor COLORREFToQColor(COLORREF cr) -{ - return QColor(GetRValue(cr), GetGValue(cr), GetBValue(cr)); -} - -static inline QTextStream& operator<<(QTextStream &str, const QColor &c) -{ - str.setIntegerBase(16); - str.setFieldWidth(2); - str.setPadChar(u'0'); - str << " rgb: #" << c.red() << c.green() << c.blue(); - str.setIntegerBase(10); - str.setFieldWidth(0); - return str; -} - static inline bool booleanSystemParametersInfo(UINT what, bool defaultValue) { BOOL result; @@ -93,128 +78,159 @@ static inline QColor mixColors(const QColor &c1, const QColor &c2) (c1.blue() + c2.blue()) / 2}; } +enum AccentColorLevel { + AccentColorDarkest, + AccentColorNormal, + AccentColorLightest +}; + +#if QT_CONFIG(cpp_winrt) +static constexpr QColor getSysColor(winrt::Windows::UI::Color &&color) +{ + return QColor(color.R, color.G, color.B, color.A); +} +#endif + +[[maybe_unused]] [[nodiscard]] static inline QColor qt_accentColor(AccentColorLevel level) +{ +#if QT_CONFIG(cpp_winrt) + using namespace winrt::Windows::UI::ViewManagement; + const auto settings = UISettings(); + const QColor accent = getSysColor(settings.GetColorValue(UIColorType::Accent)); + const QColor accentLight = getSysColor(settings.GetColorValue(UIColorType::AccentLight1)); + const QColor accentDarkest = getSysColor(settings.GetColorValue(UIColorType::AccentDark3)); +#else + const QWinRegistryKey registry(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)"); + if (!registry.isValid()) + return {}; + const QVariant value = registry.value(L"AccentColor"); + if (!value.isValid()) + return {}; + // The retrieved value is in the #AABBGGRR format, we need to + // convert it to the #AARRGGBB format which Qt expects. + const QColor abgr = QColor::fromRgba(qvariant_cast<DWORD>(value)); + if (!abgr.isValid()) + return {}; + const QColor accent = QColor::fromRgb(abgr.blue(), abgr.green(), abgr.red(), abgr.alpha()); + const QColor accentLight = accent.lighter(120); + const QColor accentDarkest = accent.darker(120 * 120 * 120); +#endif + if (level == AccentColorDarkest) + return accentDarkest; + else if (level == AccentColorLightest) + return accentLight; + return accent; +} + static inline QColor getSysColor(int index) { - return COLORREFToQColor(GetSysColor(index)); + COLORREF cr = GetSysColor(index); + return QColor(GetRValue(cr), GetGValue(cr), GetBValue(cr)); } // QTBUG-48823/Windows 10: SHGetFileInfo() (as called by item views on file system // models has been observed to trigger a WM_PAINT on the mainwindow. Suppress the // behavior by running it in a thread. - -struct QShGetFileInfoParams -{ - QShGetFileInfoParams(const QString &fn, DWORD a, SHFILEINFO *i, UINT f, bool *r) - : fileName(fn), attributes(a), flags(f), info(i), result(r) - { } - - const QString &fileName; - const DWORD attributes; - const UINT flags; - SHFILEINFO *const info; - bool *const result; -}; - class QShGetFileInfoThread : public QThread { public: - explicit QShGetFileInfoThread() - : QThread(), m_params(nullptr) + struct Task + { + Task(const QString &fn, DWORD a, UINT f) + : fileName(fn), attributes(a), flags(f) + {} + Q_DISABLE_COPY(Task) + ~Task() + { + DestroyIcon(hIcon); + hIcon = 0; + } + // Request + const QString fileName; + const DWORD attributes; + const UINT flags; + // Result + HICON hIcon = 0; + int iIcon = -1; + bool finished = false; + bool resultValid() const { return hIcon != 0 && iIcon >= 0 && finished; } + }; + + QShGetFileInfoThread() + : QThread() { - connect(this, &QThread::finished, this, &QObject::deleteLater); + start(); } - void run() override + ~QShGetFileInfoThread() { - m_init = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + cancel(); + wait(); + } - QMutexLocker readyLocker(&m_readyMutex); - while (!m_cancelled.loadRelaxed()) { - if (!m_params && !m_cancelled.loadRelaxed() - && !m_readyCondition.wait(&m_readyMutex, QDeadlineTimer(1000ll))) - continue; + QSharedPointer<Task> getNextTask() + { + QMutexLocker l(&m_waitForTaskMutex); + while (!isInterruptionRequested()) { + if (!m_taskQueue.isEmpty()) + return m_taskQueue.dequeue(); + m_waitForTaskCondition.wait(&m_waitForTaskMutex); + } + return nullptr; + } - if (m_params) { - const QString fileName = m_params->fileName; + void run() override + { + QComHelper comHelper(COINIT_MULTITHREADED); + + while (!isInterruptionRequested()) { + auto task = getNextTask(); + if (task) { SHFILEINFO info; - const bool result = SHGetFileInfo(reinterpret_cast<const wchar_t *>(fileName.utf16()), - m_params->attributes, &info, sizeof(SHFILEINFO), - m_params->flags); - m_doneMutex.lock(); - if (!m_cancelled.loadRelaxed()) { - *m_params->result = result; - memcpy(m_params->info, &info, sizeof(SHFILEINFO)); + const bool result = SHGetFileInfo(reinterpret_cast<const wchar_t *>(task->fileName.utf16()), + task->attributes, &info, sizeof(SHFILEINFO), + task->flags); + if (result) { + task->hIcon = info.hIcon; + task->iIcon = info.iIcon; } - m_params = nullptr; - + task->finished = true; m_doneCondition.wakeAll(); - m_doneMutex.unlock(); } } - - if (m_init != S_FALSE) - CoUninitialize(); } - bool runWithParams(QShGetFileInfoParams *params, qint64 timeOutMSecs) + void runWithParams(const QSharedPointer<Task> &task, + std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)) { - QMutexLocker doneLocker(&m_doneMutex); - - m_readyMutex.lock(); - m_params = params; - m_readyCondition.wakeAll(); - m_readyMutex.unlock(); + { + QMutexLocker l(&m_waitForTaskMutex); + m_taskQueue.enqueue(task); + m_waitForTaskCondition.wakeAll(); + } - return m_doneCondition.wait(&m_doneMutex, QDeadlineTimer(timeOutMSecs)); + QMutexLocker doneLocker(&m_doneMutex); + while (!task->finished && !isInterruptionRequested()) { + if (!m_doneCondition.wait(&m_doneMutex, QDeadlineTimer(timeout))) + return; + } } void cancel() { - QMutexLocker doneLocker(&m_doneMutex); - m_cancelled.storeRelaxed(1); - m_readyCondition.wakeAll(); + requestInterruption(); + m_doneCondition.wakeAll(); + m_waitForTaskCondition.wakeAll(); } private: - HRESULT m_init; - QShGetFileInfoParams *m_params; - QAtomicInt m_cancelled; - QWaitCondition m_readyCondition; + QQueue<QSharedPointer<Task>> m_taskQueue; QWaitCondition m_doneCondition; - QMutex m_readyMutex; + QWaitCondition m_waitForTaskCondition; QMutex m_doneMutex; + QMutex m_waitForTaskMutex; }; - -static bool shGetFileInfoBackground(const QString &fileName, DWORD attributes, - SHFILEINFO *info, UINT flags, - qint64 timeOutMSecs = 5000) -{ - static QShGetFileInfoThread *getFileInfoThread = nullptr; - if (!getFileInfoThread) { - getFileInfoThread = new QShGetFileInfoThread; - getFileInfoThread->start(); - } - - bool result = false; - QShGetFileInfoParams params(fileName, attributes, info, flags, &result); - if (!getFileInfoThread->runWithParams(¶ms, timeOutMSecs)) { - // Cancel and reset getFileInfoThread. It'll - // be reinitialized the next time we get called. - getFileInfoThread->cancel(); - getFileInfoThread = nullptr; - qWarning().noquote() << "SHGetFileInfo() timed out for " << fileName; - return false; - } - return result; -} - -// Dark Mode constants -enum DarkModeColors : QRgb { - darkModeBtnHighlightRgb = 0xc0c0c0, - darkModeBtnShadowRgb = 0x808080, - darkModeHighlightRgb = 0x0055ff, // deviating from 0x800080 - darkModeMenuHighlightRgb = darkModeHighlightRgb -}; +Q_APPLICATION_STATIC(QShGetFileInfoThread, s_shGetFileInfoThread) // from QStyle::standardPalette static inline QPalette standardPalette() @@ -239,16 +255,28 @@ static QColor placeHolderColor(QColor textColor) return textColor; } -static void populateLightSystemBasePalette(QPalette &result) +/* + This is used when the theme is light mode, and when the theme is dark but the + application doesn't support dark mode. In the latter case, we need to check. +*/ +void QWindowsTheme::populateLightSystemBasePalette(QPalette &result) { + const QColor background = getSysColor(COLOR_BTNFACE); + const QColor textColor = getSysColor(COLOR_WINDOWTEXT); + + const QColor accent = qt_accentColor(AccentColorNormal); + const QColor accentDarkest = qt_accentColor(AccentColorDarkest); + + const QColor linkColor = accent; + const QColor btnFace = background; + const QColor btnHighlight = getSysColor(COLOR_BTNHIGHLIGHT); + + result.setColor(QPalette::Highlight, getSysColor(COLOR_HIGHLIGHT)); result.setColor(QPalette::WindowText, getSysColor(COLOR_WINDOWTEXT)); - const QColor btnFace = getSysColor(COLOR_BTNFACE); result.setColor(QPalette::Button, btnFace); - const QColor btnHighlight = getSysColor(COLOR_BTNHIGHLIGHT); result.setColor(QPalette::Light, btnHighlight); result.setColor(QPalette::Dark, getSysColor(COLOR_BTNSHADOW)); result.setColor(QPalette::Mid, result.button().color().darker(150)); - const QColor textColor = getSysColor(COLOR_WINDOWTEXT); result.setColor(QPalette::Text, textColor); result.setColor(QPalette::PlaceholderText, placeHolderColor(textColor)); result.setColor(QPalette::BrightText, btnHighlight); @@ -257,42 +285,11 @@ static void populateLightSystemBasePalette(QPalette &result) result.setColor(QPalette::ButtonText, getSysColor(COLOR_BTNTEXT)); result.setColor(QPalette::Midlight, getSysColor(COLOR_3DLIGHT)); result.setColor(QPalette::Shadow, getSysColor(COLOR_3DDKSHADOW)); - result.setColor(QPalette::Highlight, getSysColor(COLOR_HIGHLIGHT)); result.setColor(QPalette::HighlightedText, getSysColor(COLOR_HIGHLIGHTTEXT)); -} - -static void populateDarkSystemBasePalette(QPalette &result) -{ - const QColor darkModeWindowText = Qt::white; - result.setColor(QPalette::WindowText, darkModeWindowText); - const QColor darkModebtnFace = Qt::black; - result.setColor(QPalette::Button, darkModebtnFace); - const QColor btnHighlight = QColor(darkModeBtnHighlightRgb); - result.setColor(QPalette::Light, btnHighlight); - result.setColor(QPalette::Dark, QColor(darkModeBtnShadowRgb)); - result.setColor(QPalette::Mid, result.button().color().darker(150)); - result.setColor(QPalette::Text, darkModeWindowText); - result.setColor(QPalette::PlaceholderText, placeHolderColor(darkModeWindowText)); - result.setColor(QPalette::BrightText, btnHighlight); - result.setColor(QPalette::Base, darkModebtnFace); - result.setColor(QPalette::Window, darkModebtnFace); - result.setColor(QPalette::ButtonText, darkModeWindowText); - result.setColor(QPalette::Midlight, darkModeWindowText); - result.setColor(QPalette::Shadow, darkModeWindowText); - result.setColor(QPalette::Highlight, QColor(darkModeHighlightRgb)); - result.setColor(QPalette::HighlightedText, darkModeWindowText); -} + result.setColor(QPalette::Accent, accent); -static QPalette systemPalette(bool light) -{ - QPalette result = standardPalette(); - if (light) - populateLightSystemBasePalette(result); - else - populateDarkSystemBasePalette(result); - - result.setColor(QPalette::Link, Qt::blue); - result.setColor(QPalette::LinkVisited, Qt::magenta); + result.setColor(QPalette::Link, linkColor); + result.setColor(QPalette::LinkVisited, accentDarkest); result.setColor(QPalette::Inactive, QPalette::Button, result.button().color()); result.setColor(QPalette::Inactive, QPalette::Window, result.window().color()); result.setColor(QPalette::Inactive, QPalette::Light, result.light().color()); @@ -300,35 +297,87 @@ static QPalette systemPalette(bool light) if (result.midlight() == result.button()) result.setColor(QPalette::Midlight, result.button().color().lighter(110)); - if (result.window() != result.base()) { - result.setColor(QPalette::Inactive, QPalette::Highlight, result.color(QPalette::Inactive, QPalette::Window)); - result.setColor(QPalette::Inactive, QPalette::HighlightedText, result.color(QPalette::Inactive, QPalette::Text)); - } - - const QColor disabled = - mixColors(result.windowText().color(), result.button().color()); +} - result.setColorGroup(QPalette::Disabled, result.windowText(), result.button(), - result.light(), result.dark(), result.mid(), - result.text(), result.brightText(), result.base(), - result.window()); - result.setColor(QPalette::Disabled, QPalette::WindowText, disabled); - result.setColor(QPalette::Disabled, QPalette::Text, disabled); - result.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); - result.setColor(QPalette::Disabled, QPalette::Highlight, - light ? getSysColor(COLOR_HIGHLIGHT) : QColor(darkModeHighlightRgb)); - result.setColor(QPalette::Disabled, QPalette::HighlightedText, - light ? getSysColor(COLOR_HIGHLIGHTTEXT) : QColor(Qt::white)); - result.setColor(QPalette::Disabled, QPalette::Base, - result.window().color()); - return result; +void QWindowsTheme::populateDarkSystemBasePalette(QPalette &result) +{ + QColor foreground, background, + accent, accentDark, accentDarker, accentDarkest, + accentLight, accentLighter, accentLightest; +#if QT_CONFIG(cpp_winrt) + using namespace winrt::Windows::UI::ViewManagement; + const auto settings = UISettings(); + + // We have to craft a palette from these colors. The settings.UIElementColor(UIElementType) API + // returns the old system colors, not the dark mode colors. If the background is black (which it + // usually), then override it with a dark gray instead so that we can go up and down the lightness. + if (QWindowsTheme::queryColorScheme() == Qt::ColorScheme::Dark) { + // the system is actually running in dark mode, so UISettings will give us dark colors + foreground = getSysColor(settings.GetColorValue(UIColorType::Foreground)); + background = [&settings]() -> QColor { + auto systemBackground = getSysColor(settings.GetColorValue(UIColorType::Background)); + if (systemBackground == Qt::black) + systemBackground = QColor(0x1E, 0x1E, 0x1E); + return systemBackground; + }(); + + accent = getSysColor(settings.GetColorValue(UIColorType::Accent)); + accentDark = getSysColor(settings.GetColorValue(UIColorType::AccentDark1)); + accentDarker = getSysColor(settings.GetColorValue(UIColorType::AccentDark2)); + accentDarkest = getSysColor(settings.GetColorValue(UIColorType::AccentDark3)); + accentLight = getSysColor(settings.GetColorValue(UIColorType::AccentLight1)); + accentLighter = getSysColor(settings.GetColorValue(UIColorType::AccentLight2)); + accentLightest = getSysColor(settings.GetColorValue(UIColorType::AccentLight3)); + } else +#endif + { + // If the system is running in light mode, then we need to make up our own dark palette + foreground = Qt::white; + background = QColor(0x1E, 0x1E, 0x1E); + accent = qt_accentColor(AccentColorNormal); + accentDark = accent.darker(120); + accentDarker = accentDark.darker(120); + accentDarkest = accentDarker.darker(120); + accentLight = accent.lighter(120); + accentLighter = accentLight.lighter(120); + accentLightest = accentLighter.lighter(120); + } + const QColor linkColor = accent; + const QColor buttonColor = background.lighter(200); + + result.setColor(QPalette::All, QPalette::WindowText, foreground); + result.setColor(QPalette::All, QPalette::Text, foreground); + result.setColor(QPalette::All, QPalette::BrightText, accentLightest); + + result.setColor(QPalette::All, QPalette::Button, buttonColor); + result.setColor(QPalette::All, QPalette::ButtonText, foreground); + result.setColor(QPalette::All, QPalette::Light, buttonColor.lighter(200)); + result.setColor(QPalette::All, QPalette::Midlight, buttonColor.lighter(150)); + result.setColor(QPalette::All, QPalette::Dark, buttonColor.darker(200)); + result.setColor(QPalette::All, QPalette::Mid, buttonColor.darker(150)); + result.setColor(QPalette::All, QPalette::Shadow, Qt::black); + + result.setColor(QPalette::All, QPalette::Base, background.lighter(150)); + result.setColor(QPalette::All, QPalette::Window, background); + + result.setColor(QPalette::All, QPalette::Highlight, accent); + result.setColor(QPalette::All, QPalette::HighlightedText, accent.lightness() > 128 ? Qt::black : Qt::white); + result.setColor(QPalette::All, QPalette::Link, linkColor); + result.setColor(QPalette::All, QPalette::LinkVisited, accentDarkest); + result.setColor(QPalette::All, QPalette::AlternateBase, accentDarkest); + result.setColor(QPalette::All, QPalette::ToolTipBase, buttonColor); + result.setColor(QPalette::All, QPalette::ToolTipText, foreground.darker(120)); + result.setColor(QPalette::All, QPalette::PlaceholderText, placeHolderColor(foreground)); + result.setColor(QPalette::All, QPalette::Accent, accent); } static inline QPalette toolTipPalette(const QPalette &systemPalette, bool light) { QPalette result(systemPalette); - const QColor tipBgColor = light ? getSysColor(COLOR_INFOBK) : QColor(Qt::black); - const QColor tipTextColor = light ? getSysColor(COLOR_INFOTEXT) : QColor(Qt::white); + const QColor tipBgColor = light ? getSysColor(COLOR_INFOBK) + : systemPalette.button().color(); + const QColor tipTextColor = light ? getSysColor(COLOR_INFOTEXT) + : systemPalette.buttonText().color().darker(120); result.setColor(QPalette::All, QPalette::Button, tipBgColor); result.setColor(QPalette::All, QPalette::Window, tipBgColor); @@ -342,8 +391,7 @@ static inline QPalette toolTipPalette(const QPalette &systemPalette, bool light) result.setColor(QPalette::All, QPalette::ButtonText, tipTextColor); result.setColor(QPalette::All, QPalette::ToolTipBase, tipBgColor); result.setColor(QPalette::All, QPalette::ToolTipText, tipTextColor); - const QColor disabled = - mixColors(result.windowText().color(), result.button().color()); + const QColor disabled = mixColors(result.windowText().color(), result.button().color()); result.setColor(QPalette::Disabled, QPalette::WindowText, disabled); result.setColor(QPalette::Disabled, QPalette::Text, disabled); result.setColor(QPalette::Disabled, QPalette::ToolTipText, disabled); @@ -355,11 +403,13 @@ static inline QPalette toolTipPalette(const QPalette &systemPalette, bool light) static inline QPalette menuPalette(const QPalette &systemPalette, bool light) { + if (!light) + return systemPalette; + QPalette result(systemPalette); - const QColor menuColor = light ? getSysColor(COLOR_MENU) : QColor(Qt::black); - const QColor menuTextColor = light ? getSysColor(COLOR_MENUTEXT) : QColor(Qt::white); - const QColor disabled = light - ? getSysColor(COLOR_GRAYTEXT) : QColor(darkModeBtnHighlightRgb); + const QColor menuColor = getSysColor(COLOR_MENU); + const QColor menuTextColor = getSysColor(COLOR_MENUTEXT); + const QColor disabled = getSysColor(COLOR_GRAYTEXT); // we might need a special color group for the result. result.setColor(QPalette::Active, QPalette::Button, menuColor); result.setColor(QPalette::Active, QPalette::Text, menuTextColor); @@ -368,9 +418,7 @@ static inline QPalette menuPalette(const QPalette &systemPalette, bool light) result.setColor(QPalette::Disabled, QPalette::WindowText, disabled); result.setColor(QPalette::Disabled, QPalette::Text, disabled); const bool isFlat = booleanSystemParametersInfo(SPI_GETFLATMENU, false); - const QColor highlightColor = light - ? (getSysColor(isFlat ? COLOR_MENUHILIGHT : COLOR_HIGHLIGHT)) - : QColor(darkModeMenuHighlightRgb); + const QColor highlightColor = getSysColor(isFlat ? COLOR_MENUHILIGHT : COLOR_HIGHLIGHT); result.setColor(QPalette::Disabled, QPalette::Highlight, highlightColor); result.setColor(QPalette::Disabled, QPalette::HighlightedText, disabled); result.setColor(QPalette::Disabled, QPalette::Button, @@ -395,13 +443,14 @@ static inline QPalette menuPalette(const QPalette &systemPalette, bool light) static inline QPalette *menuBarPalette(const QPalette &menuPalette, bool light) { QPalette *result = nullptr; - if (booleanSystemParametersInfo(SPI_GETFLATMENU, false)) { - result = new QPalette(menuPalette); - const QColor menubar(light ? getSysColor(COLOR_MENUBAR) : QColor(Qt::black)); - result->setColor(QPalette::Active, QPalette::Button, menubar); - result->setColor(QPalette::Disabled, QPalette::Button, menubar); - result->setColor(QPalette::Inactive, QPalette::Button, menubar); - } + if (!light || !booleanSystemParametersInfo(SPI_GETFLATMENU, false)) + return result; + + result = new QPalette(menuPalette); + const QColor menubar(getSysColor(COLOR_MENUBAR)); + result->setColor(QPalette::Active, QPalette::Button, menubar); + result->setColor(QPalette::Disabled, QPalette::Button, menubar); + result->setColor(QPalette::Inactive, QPalette::Button, menubar); return result; } @@ -411,6 +460,7 @@ QWindowsTheme *QWindowsTheme::m_instance = nullptr; QWindowsTheme::QWindowsTheme() { m_instance = this; + s_colorScheme = QWindowsTheme::queryColorScheme(); std::fill(m_fonts, m_fonts + NFonts, nullptr); std::fill(m_palettes, m_palettes + NPalettes, nullptr); refresh(); @@ -432,7 +482,10 @@ static inline QStringList iconThemeSearchPaths() static inline QStringList styleNames() { - return { QStringLiteral("WindowsVista"), QStringLiteral("Windows") }; + QStringList styles = { QStringLiteral("WindowsVista"), QStringLiteral("Windows") }; + if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11) + styles.prepend(QStringLiteral("Windows11")); + return styles; } static inline int uiEffects() @@ -487,15 +540,53 @@ QVariant QWindowsTheme::themeHint(ThemeHint hint) const } case MouseDoubleClickDistance: return GetSystemMetrics(SM_CXDOUBLECLK); + case MenuBarFocusOnAltPressRelease: + return true; default: break; } return QPlatformTheme::themeHint(hint); } -QPlatformTheme::Appearance QWindowsTheme::appearance() const +Qt::ColorScheme QWindowsTheme::colorScheme() const +{ + return QWindowsTheme::effectiveColorScheme(); +} + +Qt::ColorScheme QWindowsTheme::effectiveColorScheme() +{ + if (queryHighContrast()) + return Qt::ColorScheme::Unknown; + if (s_colorSchemeOverride != Qt::ColorScheme::Unknown) + return s_colorSchemeOverride; + if (s_colorScheme != Qt::ColorScheme::Unknown) + return s_colorScheme; + return queryColorScheme(); +} + +void QWindowsTheme::requestColorScheme(Qt::ColorScheme scheme) { - return QWindowsContext::isDarkMode() ? Appearance::Dark : Appearance::Light; + s_colorSchemeOverride = scheme; + handleSettingsChanged(); +} + +void QWindowsTheme::handleSettingsChanged() +{ + const auto oldColorScheme = s_colorScheme; + s_colorScheme = Qt::ColorScheme::Unknown; // make effectiveColorScheme() query registry + const auto newColorScheme = effectiveColorScheme(); + const bool colorSchemeChanged = newColorScheme != oldColorScheme; + s_colorScheme = newColorScheme; + auto integration = QWindowsIntegration::instance(); + integration->updateApplicationBadge(); + if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle)) { + QWindowsTheme::instance()->refresh(); + QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); + } + if (colorSchemeChanged) { + for (QWindowsWindow *w : std::as_const(QWindowsContext::instance()->windows())) + w->setDarkBorder(s_colorScheme == Qt::ColorScheme::Dark); + } } void QWindowsTheme::clearPalettes() @@ -506,29 +597,64 @@ void QWindowsTheme::clearPalettes() void QWindowsTheme::refreshPalettes() { - if (!QGuiApplication::desktopSettingsAware()) return; const bool light = - !QWindowsContext::isDarkMode() + effectiveColorScheme() != Qt::ColorScheme::Dark || !QWindowsIntegration::instance()->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle); - m_palettes[SystemPalette] = new QPalette(systemPalette(light)); + clearPalettes(); + m_palettes[SystemPalette] = new QPalette(QWindowsTheme::systemPalette(s_colorScheme)); m_palettes[ToolTipPalette] = new QPalette(toolTipPalette(*m_palettes[SystemPalette], light)); m_palettes[MenuPalette] = new QPalette(menuPalette(*m_palettes[SystemPalette], light)); m_palettes[MenuBarPalette] = menuBarPalette(*m_palettes[MenuPalette], light); if (!light) { - m_palettes[ButtonPalette] = new QPalette(*m_palettes[SystemPalette]); - m_palettes[ButtonPalette]->setColor(QPalette::Button, QColor(0x666666u)); - const QColor checkBoxBlue(0x0078d7u); - const QColor white(Qt::white); m_palettes[CheckBoxPalette] = new QPalette(*m_palettes[SystemPalette]); - m_palettes[CheckBoxPalette]->setColor(QPalette::Window, checkBoxBlue); - m_palettes[CheckBoxPalette]->setColor(QPalette::Base, checkBoxBlue); - m_palettes[CheckBoxPalette]->setColor(QPalette::Button, checkBoxBlue); - m_palettes[CheckBoxPalette]->setColor(QPalette::ButtonText, white); + m_palettes[CheckBoxPalette]->setColor(QPalette::Active, QPalette::Base, qt_accentColor(AccentColorNormal)); + m_palettes[CheckBoxPalette]->setColor(QPalette::Active, QPalette::Button, qt_accentColor(AccentColorLightest)); + m_palettes[CheckBoxPalette]->setColor(QPalette::Inactive, QPalette::Base, qt_accentColor(AccentColorDarkest)); m_palettes[RadioButtonPalette] = new QPalette(*m_palettes[CheckBoxPalette]); + } +} +QPalette QWindowsTheme::systemPalette(Qt::ColorScheme colorScheme) +{ + QPalette result = standardPalette(); + + switch (colorScheme) { + case Qt::ColorScheme::Unknown: + // when a high-contrast theme is active or when we fail to read, assume light + Q_FALLTHROUGH(); + case Qt::ColorScheme::Light: + populateLightSystemBasePalette(result); + break; + case Qt::ColorScheme::Dark: + populateDarkSystemBasePalette(result); + break; } + + if (result.window() != result.base()) { + result.setColor(QPalette::Inactive, QPalette::Highlight, + result.color(QPalette::Inactive, QPalette::Window)); + result.setColor(QPalette::Inactive, QPalette::HighlightedText, + result.color(QPalette::Inactive, QPalette::Text)); + result.setColor(QPalette::Inactive, QPalette::Accent, + result.color(QPalette::Inactive, QPalette::Window)); + } + + const QColor disabled = mixColors(result.windowText().color(), result.button().color()); + + result.setColorGroup(QPalette::Disabled, result.windowText(), result.button(), + result.light(), result.dark(), result.mid(), + result.text(), result.brightText(), result.base(), + result.window()); + result.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + result.setColor(QPalette::Disabled, QPalette::Text, disabled); + result.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + result.setColor(QPalette::Disabled, QPalette::Highlight, result.color(QPalette::Highlight)); + result.setColor(QPalette::Disabled, QPalette::HighlightedText, result.color(QPalette::HighlightedText)); + result.setColor(QPalette::Disabled, QPalette::Accent, disabled); + result.setColor(QPalette::Disabled, QPalette::Base, result.window().color()); + return result; } void QWindowsTheme::clearFonts() @@ -570,20 +696,22 @@ void QWindowsTheme::refreshFonts() clearFonts(); if (!QGuiApplication::desktopSettingsAware()) return; + + const int dpi = 96; NONCLIENTMETRICS ncm; - auto &screenManager = QWindowsContext::instance()->screenManager(); - QWindowsContext::nonClientMetricsForScreen(&ncm, screenManager.screens().value(0)); - qCDebug(lcQpaWindows) << __FUNCTION__ << ncm; - const QFont menuFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfMenuFont); - const QFont messageBoxFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfMessageFont); - const QFont statusFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfStatusFont); - const QFont titleFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfCaptionFont); + QWindowsContext::nonClientMetrics(&ncm, dpi); + qCDebug(lcQpaWindow) << __FUNCTION__ << ncm; + + const QFont menuFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfMenuFont, dpi); + const QFont messageBoxFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfMessageFont, dpi); + const QFont statusFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfStatusFont, dpi); + const QFont titleFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfCaptionFont, dpi); QFont fixedFont(QStringLiteral("Courier New"), messageBoxFont.pointSize()); fixedFont.setStyleHint(QFont::TypeWriter); LOGFONT lfIconTitleFont; - SystemParametersInfo(SPI_GETICONTITLELOGFONT, sizeof(lfIconTitleFont), &lfIconTitleFont, 0); - const QFont iconTitleFont = QWindowsFontDatabase::LOGFONT_to_QFont(lfIconTitleFont); + SystemParametersInfoForDpi(SPI_GETICONTITLELOGFONT, sizeof(lfIconTitleFont), &lfIconTitleFont, 0, dpi); + const QFont iconTitleFont = QWindowsFontDatabase::LOGFONT_to_QFont(lfIconTitleFont, dpi); m_fonts[SystemFont] = new QFont(QWindowsFontDatabase::systemDefaultFont()); m_fonts[MenuFont] = new QFont(menuFont); @@ -640,13 +768,9 @@ void QWindowsTheme::refreshIconPixmapSizes() fileIconSizes[LargeFileIcon] + fileIconSizes[LargeFileIcon] / 2; fileIconSizes[JumboFileIcon] = 8 * fileIconSizes[LargeFileIcon]; // empirical, has not been observed to work -#ifdef USE_IIMAGELIST int *availEnd = fileIconSizes + JumboFileIcon + 1; -#else - int *availEnd = fileIconSizes + LargeFileIcon + 1; -#endif // USE_IIMAGELIST m_fileIconSizes = QAbstractFileIconEngine::toSizeList(fileIconSizes, availEnd); - qCDebug(lcQpaWindows) << __FUNCTION__ << m_fileIconSizes; + qCDebug(lcQpaWindow) << __FUNCTION__ << m_fileIconSizes; } // Defined in qpixmap_win.cpp @@ -757,15 +881,18 @@ QPixmap QWindowsTheme::standardPixmap(StandardPixmap sp, const QSizeF &pixmapSiz } if (stockId != SIID_INVALID) { - QPixmap pixmap; SHSTOCKICONINFO iconInfo; memset(&iconInfo, 0, sizeof(iconInfo)); iconInfo.cbSize = sizeof(iconInfo); - stockFlags |= (pixmapSize.width() > 16 ? SHGFI_LARGEICON : SHGFI_SMALLICON); - if (SHGetStockIconInfo(stockId, SHGFI_ICON | stockFlags, &iconInfo) == S_OK) { - pixmap = qt_pixmapFromWinHICON(iconInfo.hIcon); - DestroyIcon(iconInfo.hIcon); - return pixmap; + stockFlags |= SHGSI_ICONLOCATION; + if (SHGetStockIconInfo(stockId, stockFlags, &iconInfo) == S_OK) { + const auto iconSize = pixmapSize.width(); + HICON icon; + if (SHDefExtractIcon(iconInfo.szPath, iconInfo.iIcon, 0, &icon, nullptr, iconSize) == S_OK) { + QPixmap pixmap = qt_pixmapFromWinHICON(icon); + DestroyIcon(icon); + return pixmap; + } } } @@ -835,10 +962,9 @@ public: // Shell image list helper functions. -static QPixmap pixmapFromShellImageList(int iImageList, const SHFILEINFO &info) +static QPixmap pixmapFromShellImageList(int iImageList, int iIcon) { QPixmap result; -#ifdef USE_IIMAGELIST // For MinGW: static const IID iID_IImageList = {0x46eb5926, 0x582e, 0x4017, {0x9f, 0xdf, 0xe8, 0x99, 0x8d, 0xaa, 0x9, 0x50}}; @@ -847,16 +973,12 @@ static QPixmap pixmapFromShellImageList(int iImageList, const SHFILEINFO &info) if (hr != S_OK) return result; HICON hIcon; - hr = imageList->GetIcon(info.iIcon, ILD_TRANSPARENT, &hIcon); + hr = imageList->GetIcon(iIcon, ILD_TRANSPARENT, &hIcon); if (hr == S_OK) { result = qt_pixmapFromWinHICON(hIcon); DestroyIcon(hIcon); } imageList->Release(); -#else - Q_UNUSED(iImageList); - Q_UNUSED(info); -#endif // USE_IIMAGELIST return result; } @@ -896,13 +1018,10 @@ QString QWindowsFileIconEngine::cacheKey() const QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon::State) { - /* We don't use the variable, but by storing it statically, we - * ensure CoInitialize is only called once. */ - static HRESULT comInit = CoInitialize(nullptr); - Q_UNUSED(comInit); + QComHelper comHelper; static QCache<QString, FakePointer<int> > dirIconEntryCache(1000); - static QMutex mx; + Q_CONSTINIT static QMutex mx; static int defaultFolderIIcon = -1; const bool useDefaultFolderIcon = options() & QPlatformTheme::DontUseCustomDirectoryIcons; @@ -911,13 +1030,9 @@ QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon const int width = int(size.width()); const int iconSize = width > fileIconSizes[SmallFileIcon] ? SHGFI_LARGEICON : SHGFI_SMALLICON; const int requestedImageListSize = -#ifdef USE_IIMAGELIST width > fileIconSizes[ExtraLargeFileIcon] ? sHIL_JUMBO : (width > fileIconSizes[LargeFileIcon] ? sHIL_EXTRALARGE : 0); -#else - 0; -#endif // !USE_IIMAGELIST bool cacheableDirIcon = fileInfo().isDir() && !fileInfo().isRoot(); if (cacheableDirIcon) { QMutexLocker locker(&mx); @@ -933,7 +1048,6 @@ QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon } } - SHFILEINFO info; unsigned int flags = SHGFI_ICON | iconSize | SHGFI_SYSICONINDEX | SHGFI_ADDOVERLAYS | SHGFI_OVERLAYINDEX; DWORD attributes = 0; QString path = filePath; @@ -945,43 +1059,43 @@ QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon flags |= SHGFI_USEFILEATTRIBUTES; attributes |= FILE_ATTRIBUTE_NORMAL; } - const bool val = shGetFileInfoBackground(path, attributes, &info, flags); - + auto task = QSharedPointer<QShGetFileInfoThread::Task>( + new QShGetFileInfoThread::Task(path, attributes, flags)); + s_shGetFileInfoThread()->runWithParams(task); // Even if GetFileInfo returns a valid result, hIcon can be empty in some cases - if (val && info.hIcon) { + if (task->resultValid()) { QString key; if (cacheableDirIcon) { if (useDefaultFolderIcon && defaultFolderIIcon < 0) - defaultFolderIIcon = info.iIcon; + defaultFolderIIcon = task->iIcon; //using the unique icon index provided by windows save us from duplicate keys - key = dirIconPixmapCacheKey(info.iIcon, iconSize, requestedImageListSize); + key = dirIconPixmapCacheKey(task->iIcon, iconSize, requestedImageListSize); QPixmapCache::find(key, &pixmap); if (!pixmap.isNull()) { QMutexLocker locker(&mx); - dirIconEntryCache.insert(filePath, FakePointer<int>::create(info.iIcon)); + dirIconEntryCache.insert(filePath, FakePointer<int>::create(task->iIcon)); } } if (pixmap.isNull()) { if (requestedImageListSize) { - pixmap = pixmapFromShellImageList(requestedImageListSize, info); + pixmap = pixmapFromShellImageList(requestedImageListSize, task->iIcon); if (pixmap.isNull() && requestedImageListSize == sHIL_JUMBO) - pixmap = pixmapFromShellImageList(sHIL_EXTRALARGE, info); + pixmap = pixmapFromShellImageList(sHIL_EXTRALARGE, task->iIcon); } if (pixmap.isNull()) - pixmap = qt_pixmapFromWinHICON(info.hIcon); + pixmap = qt_pixmapFromWinHICON(task->hIcon); if (!pixmap.isNull()) { if (cacheableDirIcon) { QMutexLocker locker(&mx); QPixmapCache::insert(key, pixmap); - dirIconEntryCache.insert(filePath, FakePointer<int>::create(info.iIcon)); + dirIconEntryCache.insert(filePath, FakePointer<int>::create(task->iIcon)); } } else { qWarning("QWindowsTheme::fileIconPixmap() no icon found"); } } - DestroyIcon(info.hIcon); } return pixmap; @@ -992,6 +1106,11 @@ QIcon QWindowsTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOpt return QIcon(new QWindowsFileIconEngine(fileInfo, iconOptions)); } +QIconEngine *QWindowsTheme::createIconEngine(const QString &iconName) const +{ + return new QWindowsIconEngine(iconName); +} + static inline bool doUseNativeMenus() { const unsigned options = QWindowsIntegration::instance()->options(); @@ -1016,19 +1135,23 @@ bool QWindowsTheme::useNativeMenus() return result; } -bool QWindowsTheme::queryDarkMode() +Qt::ColorScheme QWindowsTheme::queryColorScheme() { - if (queryHighContrast()) { - return false; - } + if (queryHighContrast()) + return Qt::ColorScheme::Unknown; + const auto setting = QWinRegistryKey(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)") .dwordValue(L"AppsUseLightTheme"); - return setting.second && setting.first == 0; + return setting.second && setting.first == 0 ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; } bool QWindowsTheme::queryHighContrast() { - return booleanSystemParametersInfo(SPI_GETHIGHCONTRAST, false); + HIGHCONTRAST hcf = {}; + hcf.cbSize = static_cast<UINT>(sizeof(HIGHCONTRAST)); + if (SystemParametersInfo(SPI_GETHIGHCONTRAST, hcf.cbSize, &hcf, FALSE)) + return hcf.dwFlags & HCF_HIGHCONTRASTON; + return false; } QPlatformMenuItem *QWindowsTheme::createPlatformMenuItem() const diff --git a/src/plugins/platforms/windows/qwindowstheme.h b/src/plugins/platforms/windows/qwindowstheme.h index 85a7607d7f..a89fb1e5bd 100644 --- a/src/plugins/platforms/windows/qwindowstheme.h +++ b/src/plugins/platforms/windows/qwindowstheme.h @@ -31,7 +31,10 @@ public: #endif QVariant themeHint(ThemeHint) const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; + + static void handleSettingsChanged(); const QPalette *palette(Palette type = SystemPalette) const override { return m_palettes[type]; } @@ -41,6 +44,7 @@ public: QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions iconOptions = {}) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; void windowsThemeChanged(QWindow *window); void displayChanged() { refreshIconPixmapSizes(); } @@ -53,21 +57,31 @@ public: void showPlatformMenuBar() override; static bool useNativeMenus(); - static bool queryDarkMode(); - static bool queryHighContrast(); void refreshFonts(); void refresh(); static const char *name; + static QPalette systemPalette(Qt::ColorScheme); + private: void clearPalettes(); void refreshPalettes(); void clearFonts(); void refreshIconPixmapSizes(); + static void populateLightSystemBasePalette(QPalette &result); + static void populateDarkSystemBasePalette(QPalette &result); + + static Qt::ColorScheme queryColorScheme(); + static Qt::ColorScheme effectiveColorScheme(); + static bool queryHighContrast(); + static QWindowsTheme *m_instance; + static inline Qt::ColorScheme s_colorScheme = Qt::ColorScheme::Unknown; + static inline Qt::ColorScheme s_colorSchemeOverride = Qt::ColorScheme::Unknown; + QPalette *m_palettes[NPalettes]; QFont *m_fonts[NFonts]; QList<QSize> m_fileIconSizes; diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp index 76060f45ee..acfff64751 100644 --- a/src/plugins/platforms/windows/qwindowswindow.cpp +++ b/src/plugins/platforms/windows/qwindowswindow.cpp @@ -1,12 +1,11 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#ifndef WINVER -# define WINVER 0x0A00 // Enable touch functions for MinGW -#endif +#include <QtCore/qt_windows.h> #include "qwindowswindow.h" #include "qwindowscontext.h" +#include "qwindowstheme.h" #if QT_CONFIG(draganddrop) # include "qwindowsdrag.h" #endif @@ -29,6 +28,7 @@ #include <QtGui/qwindow.h> #include <QtGui/qregion.h> #include <QtGui/qopenglcontext.h> +#include <QtGui/private/qwindowsthemecache_p.h> #include <private/qwindow_p.h> // QWINDOWSIZE_MAX #include <private/qguiapplication_p.h> #include <private/qhighdpiscaling_p.h> @@ -431,11 +431,7 @@ static inline bool windowIsAccelerated(const QWindow *w) { switch (w->surfaceType()) { case QSurface::OpenGLSurface: - return true; - case QSurface::RasterGLSurface: - return qt_window_private(const_cast<QWindow *>(w))->compositing; case QSurface::VulkanSurface: - return true; case QSurface::Direct3DSurface: return true; default: @@ -470,20 +466,27 @@ static bool shouldShowMaximizeButton(const QWindow *w, Qt::WindowFlags flags) w->maximumSize() == QSize(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX); } +bool QWindowsWindow::hasNoNativeFrame(HWND hwnd, Qt::WindowFlags flags) +{ + const LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); + return (style & WS_CHILD) || (flags & Qt::FramelessWindowHint); +} + // Set the WS_EX_LAYERED flag on a HWND if required. This is required for // translucent backgrounds, not fully opaque windows and for // Qt::WindowTransparentForInput (in combination with WS_EX_TRANSPARENT). bool QWindowsWindow::setWindowLayered(HWND hwnd, Qt::WindowFlags flags, bool hasAlpha, qreal opacity) { - const LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + const LONG_PTR exStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE); + // Native children are frameless by nature, so check for that as well. const bool needsLayered = (flags & Qt::WindowTransparentForInput) - || (hasAlpha && (flags & Qt::FramelessWindowHint)) || opacity < 1.0; + || (hasAlpha && hasNoNativeFrame(hwnd, flags)) || opacity < 1.0; const bool isLayered = (exStyle & WS_EX_LAYERED); if (needsLayered != isLayered) { if (needsLayered) { - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_LAYERED); + SetWindowLongPtr(hwnd, GWL_EXSTYLE, exStyle | WS_EX_LAYERED); } else { - SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED); + SetWindowLongPtr(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED); } } return needsLayered; @@ -493,7 +496,7 @@ static void setWindowOpacity(HWND hwnd, Qt::WindowFlags flags, bool hasAlpha, bo { if (QWindowsWindow::setWindowLayered(hwnd, flags, hasAlpha, level)) { const BYTE alpha = BYTE(qRound(255.0 * level)); - if (hasAlpha && !accelerated && (flags & Qt::FramelessWindowHint)) { + if (hasAlpha && !accelerated && QWindowsWindow::hasNoNativeFrame(hwnd, flags)) { // Non-GL windows with alpha: Use blend function to update. BLENDFUNCTION blend = {AC_SRC_OVER, 0, alpha, AC_SRC_ALPHA}; UpdateLayeredWindow(hwnd, nullptr, nullptr, nullptr, nullptr, nullptr, 0, &blend, ULW_ALPHA); @@ -640,13 +643,18 @@ static inline void fixTopLevelWindowFlags(Qt::WindowFlags &flags) flags |= Qt::FramelessWindowHint; } -static QScreen *screenForName(const QWindow *w, const QString &name) +static QScreen *screenForDeviceName(const QWindow *w, const QString &name) { + const auto getDeviceName = [](const QScreen *screen) -> QString { + if (const auto s = static_cast<const QWindowsScreen *>(screen->handle())) + return s->data().deviceName; + return {}; + }; QScreen *winScreen = w ? w->screen() : QGuiApplication::primaryScreen(); - if (winScreen && winScreen->name() != name) { + if (winScreen && getDeviceName(winScreen) != name) { const auto screens = winScreen->virtualSiblings(); for (QScreen *screen : screens) { - if (screen->name() == name) + if (getDeviceName(screen) == name) return screen; } } @@ -790,15 +798,8 @@ void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flag style = WS_CHILD; } - // if (!testAttribute(Qt::WA_PaintUnclipped)) - // ### Commented out for now as it causes some problems, but - // this should be correct anyway, so dig some more into this -#ifdef Q_FLATTEN_EXPOSE - if (windowIsOpenGL(w)) // a bit incorrect since the is-opengl status may change from false to true at any time later on - style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN; // see SetPixelFormat -#else style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN ; -#endif + if (topLevel) { if ((type == Qt::Window || dialog || tool)) { if (!(flags & Qt::FramelessWindowHint)) { @@ -838,12 +839,28 @@ void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flag // NOTE: WS_EX_TRANSPARENT flag can make mouse inputs fall through a layered window if (flagsIn & Qt::WindowTransparentForInput) exStyle |= WS_EX_LAYERED | WS_EX_TRANSPARENT; + + // Currently only compatible with D3D surfaces, use it with care. + if (qEnvironmentVariableIntValue("QT_QPA_DISABLE_REDIRECTION_SURFACE")) + exStyle |= WS_EX_NOREDIRECTIONBITMAP; } } static inline bool shouldApplyDarkFrame(const QWindow *w) { - return w->isTopLevel() && !w->flags().testFlag(Qt::FramelessWindowHint); + if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + return false; + + // the user of the application has explicitly opted out of dark frames + if (!QWindowsIntegration::instance()->darkModeHandling().testFlag(QWindowsApplication::DarkModeWindowFrames)) + return false; + + // if the application supports a dark border, and the palette is dark (window background color + // is darker than the text), then turn dark-border support on, otherwise use a light border. + auto *dWindow = QWindowPrivate::get(const_cast<QWindow*>(w)); + const QPalette windowPal = dWindow->windowPalette(); + return windowPal.color(QPalette::WindowText).lightness() + > windowPal.color(QPalette::Window).lightness(); } QWindowsWindowData @@ -874,11 +891,12 @@ QWindowsWindowData style, exStyle)); QWindowsContext::instance()->setWindowCreationContext(context); - const bool hasFrame = (style & (WS_DLGFRAME | WS_THICKFRAME)); + const bool hasFrame = (style & (WS_DLGFRAME | WS_THICKFRAME)) + && !(result.flags & Qt::FramelessWindowHint); QMargins invMargins = topLevel && hasFrame && QWindowsGeometryHint::positionIncludesFrame(w) ? invisibleMargins(QPoint(context->frameX, context->frameY)) : QMargins(); - qCDebug(lcQpaWindows).nospace() + qCDebug(lcQpaWindow).nospace() << "CreateWindowEx: " << w << " class=" << windowClassName << " title=" << title << '\n' << *this << "\nrequested: " << rect << ": " << context->frameWidth << 'x' << context->frameHeight @@ -904,7 +922,7 @@ QWindowsWindowData pos.x(), pos.y(), context->frameWidth, context->frameHeight, parentHandle, nullptr, appinst, nullptr); - qCDebug(lcQpaWindows).nospace() + qCDebug(lcQpaWindow).nospace() << "CreateWindowEx: returns " << w << ' ' << result.hwnd << " obtained geometry: " << context->obtainedPos << context->obtainedSize << ' ' << context->margins; @@ -913,11 +931,8 @@ QWindowsWindowData return result; } - if (QWindowsContext::isDarkMode() - && QWindowsIntegration::instance()->darkModeHandling().testFlag(QWindowsApplication::DarkModeWindowFrames) - && shouldApplyDarkFrame(w)) { + if (QWindowsTheme::instance()->colorScheme() == Qt::ColorScheme::Dark && shouldApplyDarkFrame(w)) QWindowsWindow::setDarkBorderToWindow(result.hwnd, true); - } if (mirrorParentWidth != 0) { context->obtainedPos.setX(mirrorParentWidth - context->obtainedSize.width() @@ -927,6 +942,7 @@ QWindowsWindowData QRect obtainedGeometry(context->obtainedPos, context->obtainedSize); result.geometry = obtainedGeometry; + result.restoreGeometry = frameGeometry(result.hwnd, topLevel); result.fullFrameMargins = context->margins; result.embedded = embedded; result.hasFrame = hasFrame; @@ -947,7 +963,7 @@ void WindowCreationData::applyWindowFlags(HWND hwnd) const const LONG_PTR newExStyle = exStyle; if (newExStyle != oldExStyle) SetWindowLongPtr(hwnd, GWL_EXSTYLE, newExStyle); - qCDebug(lcQpaWindows).nospace() << __FUNCTION__ << hwnd << *this + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << hwnd << *this << "\n Style from " << debugWinStyle(DWORD(oldStyle)) << "\n to " << debugWinStyle(DWORD(newStyle)) << "\n ExStyle from " << debugWinExStyle(DWORD(oldExStyle)) << " to " @@ -1001,6 +1017,21 @@ static QSize toNativeSizeConstrained(QSize dip, const QScreen *s) return dip; } +// Helper for checking if frame adjustment needs to be skipped +// NOTE: Unmaximized frameless windows will skip margins calculation +static bool shouldOmitFrameAdjustment(const Qt::WindowFlags flags, DWORD style) +{ + return flags.testFlag(Qt::FramelessWindowHint) && !(style & WS_MAXIMIZE); +} + +// Helper for checking if frame adjustment needs to be skipped +// NOTE: Unmaximized frameless windows will skip margins calculation +static bool shouldOmitFrameAdjustment(const Qt::WindowFlags flags, HWND hwnd) +{ + DWORD style = hwnd != nullptr ? DWORD(GetWindowLongPtr(hwnd, GWL_STYLE)) : 0; + return flags.testFlag(Qt::FramelessWindowHint) && !(style & WS_MAXIMIZE); +} + /*! \class QWindowsGeometryHint \brief Stores geometry constraints and provides utility functions. @@ -1013,7 +1044,7 @@ static QSize toNativeSizeConstrained(QSize dip, const QScreen *s) QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, DWORD style, DWORD exStyle) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; RECT rect = {0,0,0,0}; style &= ~DWORD(WS_OVERLAPPED); // Not permitted, see docs. @@ -1021,7 +1052,7 @@ QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, DWORD styl qErrnoWarning("%s: AdjustWindowRectEx failed", __FUNCTION__); const QMargins result(qAbs(rect.left), qAbs(rect.top), qAbs(rect.right), qAbs(rect.bottom)); - qCDebug(lcQpaWindows).nospace() << __FUNCTION__ << " style=" + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << " style=" << Qt::showbase << Qt::hex << style << " exStyle=" << exStyle << Qt::dec << Qt::noshowbase << ' ' << rect << ' ' << result; return result; @@ -1029,15 +1060,13 @@ QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, DWORD styl QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, HWND hwnd) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) - return {}; return frameOnPrimaryScreen(w, DWORD(GetWindowLongPtr(hwnd, GWL_STYLE)), DWORD(GetWindowLongPtr(hwnd, GWL_EXSTYLE))); } QMargins QWindowsGeometryHint::frame(const QWindow *w, DWORD style, DWORD exStyle, qreal dpi) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; RECT rect = {0,0,0,0}; style &= ~DWORD(WS_OVERLAPPED); // Not permitted, see docs. @@ -1046,7 +1075,7 @@ QMargins QWindowsGeometryHint::frame(const QWindow *w, DWORD style, DWORD exStyl } const QMargins result(qAbs(rect.left), qAbs(rect.top), qAbs(rect.right), qAbs(rect.bottom)); - qCDebug(lcQpaWindows).nospace() << __FUNCTION__ << " style=" + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << " style=" << Qt::showbase << Qt::hex << style << " exStyle=" << exStyle << Qt::dec << Qt::noshowbase << " dpi=" << dpi << ' ' << rect << ' ' << result; @@ -1055,7 +1084,7 @@ QMargins QWindowsGeometryHint::frame(const QWindow *w, DWORD style, DWORD exStyl QMargins QWindowsGeometryHint::frame(const QWindow *w, HWND hwnd, DWORD style, DWORD exStyle) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; if (QWindowsScreenManager::isSingleScreen()) return frameOnPrimaryScreen(w, style, exStyle); @@ -1069,17 +1098,15 @@ QMargins QWindowsGeometryHint::frame(const QWindow *w, HWND hwnd, DWORD style, D QMargins QWindowsGeometryHint::frame(const QWindow *w, HWND hwnd) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) - return {}; - return frame(w, hwnd, DWORD(GetWindowLongPtrW(hwnd, GWL_STYLE)), - DWORD(GetWindowLongPtrW(hwnd, GWL_EXSTYLE))); + return frame(w, hwnd, DWORD(GetWindowLongPtr(hwnd, GWL_STYLE)), + DWORD(GetWindowLongPtr(hwnd, GWL_EXSTYLE))); } // For newly created windows. QMargins QWindowsGeometryHint::frame(const QWindow *w, const QRect &geometry, DWORD style, DWORD exStyle) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; if (QWindowsScreenManager::isSingleScreen() || !QWindowsContext::shouldHaveNonClientDpiScaling(w)) { @@ -1108,7 +1135,7 @@ bool QWindowsGeometryHint::handleCalculateSize(const QMargins &customMargins, co ncp->rgrc[0].right -= customMargins.right(); ncp->rgrc[0].bottom -= customMargins.bottom(); result = nullptr; - qCDebug(lcQpaWindows).nospace() << __FUNCTION__ << oldClientArea << '+' << customMargins << "-->" + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << oldClientArea << '+' << customMargins << "-->" << ncp->rgrc[0] << ' ' << ncp->rgrc[1] << ' ' << ncp->rgrc[2] << ' ' << ncp->lppos->cx << ',' << ncp->lppos->cy; return true; @@ -1144,7 +1171,7 @@ void QWindowsGeometryHint::applyToMinMaxInfo(const QWindow *w, QSize minimumSize; QSize maximumSize; frameSizeConstraints(w, screen, margins, &minimumSize, &maximumSize); - qCDebug(lcQpaWindows).nospace() << '>' << __FUNCTION__ << '<' << " min=" + qCDebug(lcQpaWindow).nospace() << '>' << __FUNCTION__ << '<' << " min=" << minimumSize.width() << ',' << minimumSize.height() << " max=" << maximumSize.width() << ',' << maximumSize.height() << " margins=" << margins @@ -1159,7 +1186,7 @@ void QWindowsGeometryHint::applyToMinMaxInfo(const QWindow *w, mmi->ptMaxTrackSize.x = maximumSize.width(); if (maximumSize.height() < QWINDOWSIZE_MAX) mmi->ptMaxTrackSize.y = maximumSize.height(); - qCDebug(lcQpaWindows).nospace() << '<' << __FUNCTION__ << " out " << *mmi; + qCDebug(lcQpaWindow).nospace() << '<' << __FUNCTION__ << " out " << *mmi; } void QWindowsGeometryHint::applyToMinMaxInfo(const QWindow *w, @@ -1194,7 +1221,7 @@ bool QWindowsGeometryHint::positionIncludesFrame(const QWindow *w) bool QWindowsBaseWindow::isRtlLayout(HWND hwnd) { - return (GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) != 0; + return (GetWindowLongPtr(hwnd, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) != 0; } QWindowsBaseWindow *QWindowsBaseWindow::baseWindowOf(const QWindow *w) @@ -1255,7 +1282,7 @@ void QWindowsBaseWindow::hide_sys() // Normal hide, do not activate other window void QWindowsBaseWindow::raise_sys() { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window(); + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window(); const Qt::WindowType type = window()->type(); if (type == Qt::Popup || type == Qt::SubWindow // Special case for QTBUG-63121: MDI subwindows with WindowStaysOnTopHint @@ -1266,14 +1293,14 @@ void QWindowsBaseWindow::raise_sys() void QWindowsBaseWindow::lower_sys() { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window(); + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window(); if (!(window()->flags() & Qt::WindowStaysOnTopHint)) SetWindowPos(handle(), HWND_BOTTOM, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE); } void QWindowsBaseWindow::setWindowTitle_sys(const QString &title) { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window() << title; + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window() << title; SetWindowText(handle(), reinterpret_cast<const wchar_t *>(title.utf16())); } @@ -1332,6 +1359,8 @@ QWindowsForeignWindow::QWindowsForeignWindow(QWindow *window, HWND hwnd) , m_hwnd(hwnd) , m_topLevelStyle(0) { + if (QPlatformWindow::parent()) + setParent(QPlatformWindow::parent()); } void QWindowsForeignWindow::setParent(const QPlatformWindow *newParentWindow) @@ -1340,7 +1369,7 @@ void QWindowsForeignWindow::setParent(const QPlatformWindow *newParentWindow) const HWND newParent = newParentWindow ? reinterpret_cast<HWND>(newParentWindow->winId()) : HWND(nullptr); const bool isTopLevel = !newParent; const DWORD oldStyle = style(); - qCDebug(lcQpaWindows) << __FUNCTION__ << window() << "newParent=" + qCDebug(lcQpaWindow) << __FUNCTION__ << window() << "newParent=" << newParentWindow << newParent << "oldStyle=" << debugWinStyle(oldStyle); SetParent(m_hwnd, newParent); if (wasTopLevel != isTopLevel) { // Top level window flags need to be set/cleared manually. @@ -1358,7 +1387,7 @@ void QWindowsForeignWindow::setParent(const QPlatformWindow *newParentWindow) void QWindowsForeignWindow::setVisible(bool visible) { - qCDebug(lcQpaWindows) << __FUNCTION__ << window() << visible; + qCDebug(lcQpaWindow) << __FUNCTION__ << window() << visible; if (visible) ShowWindow(handle(), SW_SHOWNOACTIVATE); else @@ -1395,13 +1424,16 @@ QWindowCreationContext::QWindowCreationContext(const QWindow *w, const QScreen * requestedGeometry(geometry), obtainedPos(geometryIn.topLeft()), obtainedSize(geometryIn.size()), - margins(QWindowsGeometryHint::frame(w, geometry, style, exStyle)), - customMargins(cm) + margins(QWindowsGeometryHint::frame(w, geometry, style, exStyle)) { // Geometry of toplevels does not consider window frames. // TODO: No concept of WA_wasMoved yet that would indicate a // CW_USEDEFAULT unless set. For now, assume that 0,0 means 'default' // for toplevels. + + if (!(w->flags() & Qt::FramelessWindowHint)) + customMargins = cm; + if (geometry.isValid() || !qt_window_private(const_cast<QWindow *>(w))->resizeAutomatic) { frameX = geometry.x(); @@ -1420,7 +1452,7 @@ QWindowCreationContext::QWindowCreationContext(const QWindow *w, const QScreen * } } - qCDebug(lcQpaWindows).nospace() + qCDebug(lcQpaWindow).nospace() << __FUNCTION__ << ' ' << w << ' ' << geometry << " pos incl. frame=" << QWindowsGeometryHint::positionIncludesFrame(w) << " frame=" << frameWidth << 'x' << frameHeight << '+' @@ -1504,6 +1536,7 @@ QWindowsWindow::QWindowsWindow(QWindow *aWindow, const QWindowsWindowData &data) QWindowsWindow::~QWindowsWindow() { setFlag(WithinDestroy); + QWindowsThemeCache::clearThemeCache(m_data.hwnd); if (testFlag(TouchRegistered)) UnregisterTouchWindow(m_data.hwnd); destroyWindow(); @@ -1556,7 +1589,7 @@ void QWindowsWindow::fireFullExpose(bool force) void QWindowsWindow::destroyWindow() { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window() << m_data.hwnd; + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window() << m_data.hwnd; if (m_data.hwnd) { // Stop event dispatching before Window is destroyed. setFlag(WithinDestroy); // Clear any transient child relationships as Windows will otherwise destroy them (QTBUG-35499, QTBUG-36666) @@ -1663,7 +1696,7 @@ QScreen *QWindowsWindow::forcedScreenForGLWindow(const QWindow *w) forceToScreen = GpuDescription::detect().gpuSuitableScreen; m_screenForGLInitialized = true; } - return forceToScreen.isEmpty() ? nullptr : screenForName(w, forceToScreen); + return forceToScreen.isEmpty() ? nullptr : screenForDeviceName(w, forceToScreen); } // Returns topmost QWindowsWindow ancestor even if there are embedded windows in the chain. @@ -1705,7 +1738,7 @@ QWindowsWindowData void QWindowsWindow::setVisible(bool visible) { const QWindow *win = window(); - qCDebug(lcQpaWindows) << __FUNCTION__ << this << win << m_data.hwnd << visible; + qCDebug(lcQpaWindow) << __FUNCTION__ << this << win << m_data.hwnd << visible; if (m_data.hwnd) { if (visible) { show_sys(); @@ -1885,7 +1918,7 @@ void QWindowsWindow::show_sys() const void QWindowsWindow::setParent(const QPlatformWindow *newParent) { - qCDebug(lcQpaWindows) << __FUNCTION__ << window() << newParent; + qCDebug(lcQpaWindow) << __FUNCTION__ << window() << newParent; if (m_data.hwnd) setParent_sys(newParent); @@ -1945,6 +1978,12 @@ void QWindowsWindow::handleCompositionSettingsChanged() } } +qreal QWindowsWindow::dpiRelativeScale(const UINT dpi) const +{ + return QHighDpiScaling::roundScaleFactor(qreal(dpi) / QWindowsScreen::baseDpi) / + QHighDpiScaling::roundScaleFactor(qreal(savedDpi()) / QWindowsScreen::baseDpi); +} + void QWindowsWindow::handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT *result) { // We want to keep QWindow's device independent size constant across the @@ -1952,11 +1991,19 @@ void QWindowsWindow::handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT * // by the change of DPI (e.g. 120 -> 144 = 1.2), also taking any scale // factor rounding into account. The win32 window size includes the margins; // add the margins for the new DPI to the window size. - const int dpi = int(wParam); - const qreal scale = QHighDpiScaling::roundScaleFactor(qreal(dpi) / QWindowsScreen::baseDpi) / - QHighDpiScaling::roundScaleFactor(qreal(savedDpi()) / QWindowsScreen::baseDpi); - const QMargins margins = QWindowsGeometryHint::frame(window(), style(), exStyle(), dpi); - const QSize windowSize = (geometry().size() * scale).grownBy(margins); + const UINT dpi = UINT(wParam); + const qreal scale = dpiRelativeScale(dpi); + const QMargins margins = fullFrameMargins(); + if (!(m_data.flags & Qt::FramelessWindowHint)) { + // We need to update the custom margins to match the current DPI, because + // we don't want our users manually hook into this message just to set a + // new margin, but here we can't call setCustomMargins() directly, that + // function will change the window geometry which conflicts with what we + // are currently doing. + m_data.customMargins *= scale; + } + + const QSize windowSize = (geometry().size() * scale).grownBy((margins * scale) + customMargins()); SIZE *size = reinterpret_cast<SIZE *>(lParam); size->cx = windowSize.width(); size->cy = windowSize.height(); @@ -1966,11 +2013,17 @@ void QWindowsWindow::handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT * void QWindowsWindow::handleDpiChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) { const UINT dpi = HIWORD(wParam); + const qreal scale = dpiRelativeScale(dpi); setSavedDpi(dpi); + QWindowsThemeCache::clearThemeCache(hwnd); + // Send screen change first, so that the new screen is set during any following resize checkForScreenChanged(QWindowsWindow::FromDpiChange); + if (!IsZoomed(hwnd)) + m_data.restoreGeometry.setSize(m_data.restoreGeometry.size() * scale); + // We get WM_DPICHANGED in one of two situations: // // 1. The DPI change is a "spontaneous" DPI change as a result of e.g. @@ -1993,13 +2046,22 @@ void QWindowsWindow::handleDpiChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) SetWindowPos(hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, prcNewWindow->right - prcNewWindow->left, prcNewWindow->bottom - prcNewWindow->top, SWP_NOZORDER | SWP_NOACTIVATE); + // If the window does not have a frame, WM_MOVE and WM_SIZE won't be + // called which prevents the content from being scaled appropriately + // after a DPI change. + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) + handleGeometryChange(); } + + // Re-apply mask now that we have a new DPI, which have resulted in + // a new scale factor. + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); } void QWindowsWindow::handleDpiChangedAfterParent(HWND hwnd) { const UINT dpi = GetDpiForWindow(hwnd); - const qreal scale = qreal(dpi) / qreal(savedDpi()); + const qreal scale = dpiRelativeScale(dpi); setSavedDpi(dpi); checkForScreenChanged(QWindowsWindow::FromDpiChange); @@ -2106,7 +2168,7 @@ void QWindowsWindow::setGeometry(const QRect &rectIn) if (m_data.geometry != rect && (isVisible() || QLibraryInfo::isDebugBuild())) { const auto warning = msgUnableToSetGeometry(this, rectIn, m_data.geometry, - m_data.fullFrameMargins, m_data.customMargins); + fullFrameMargins(), customMargins()); qWarning("%s: %s", __FUNCTION__, qPrintable(warning)); } } else { @@ -2121,8 +2183,41 @@ void QWindowsWindow::handleMoved() handleGeometryChange(); } -void QWindowsWindow::handleResized(int wParam) +void QWindowsWindow::handleResized(int wParam, LPARAM lParam) { + /* Prevents borderless windows from covering the taskbar when maximized. */ + if ((m_data.flags.testFlag(Qt::FramelessWindowHint) + || (m_data.flags.testFlag(Qt::CustomizeWindowHint) && !m_data.flags.testFlag(Qt::WindowTitleHint))) + && IsZoomed(m_data.hwnd)) { + const int resizedWidth = LOWORD(lParam); + const int resizedHeight = HIWORD(lParam); + + const HMONITOR monitor = MonitorFromWindow(m_data.hwnd, MONITOR_DEFAULTTOPRIMARY); + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfoW(monitor, &monitorInfo); + + int correctLeft = monitorInfo.rcMonitor.left; + int correctTop = monitorInfo.rcMonitor.top; + int correctWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left; + int correctHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top; + + if (!m_data.flags.testFlag(Qt::FramelessWindowHint)) { + const int borderWidth = invisibleMargins(m_data.hwnd).left(); + correctLeft -= borderWidth; + correctTop -= borderWidth; + correctWidth += borderWidth * 2; + correctHeight += borderWidth * 2; + } + + if (resizedWidth != correctWidth || resizedHeight != correctHeight) { + qCDebug(lcQpaWindow) << __FUNCTION__ << "correcting: " << resizedWidth << "x" + << resizedHeight << " -> " << correctWidth << "x" << correctHeight; + SetWindowPos(m_data.hwnd, nullptr, correctLeft, correctTop, correctWidth, correctHeight, + SWP_NOZORDER | SWP_NOACTIVATE); + } + } + switch (wParam) { case SIZE_MAXHIDE: // Some other window affected. case SIZE_MAXSHOW: @@ -2170,11 +2265,11 @@ void QWindowsWindow::checkForScreenChanged(ScreenChangeMode mode) return; // For screens with different DPI: postpone until WM_DPICHANGE // Check on currentScreen as it can be 0 when resuming a session (QTBUG-80436). - if (mode == FromGeometryChange && currentScreen != nullptr - && !equalDpi(currentScreen->logicalDpi(), newScreen->logicalDpi())) { + const bool changingDpi = !equalDpi(QDpi(savedDpi(), savedDpi()), newScreen->logicalDpi()); + if (mode == FromGeometryChange && currentScreen != nullptr && changingDpi) return; - } - qCDebug(lcQpaWindows).noquote().nospace() << __FUNCTION__ + + qCDebug(lcQpaWindow).noquote().nospace() << __FUNCTION__ << ' ' << window() << " \"" << (currentScreen ? currentScreen->name() : QString()) << "\"->\"" << newScreen->name() << '"'; updateFullFrameMargins(); @@ -2185,6 +2280,7 @@ void QWindowsWindow::handleGeometryChange() { const QRect previousGeometry = m_data.geometry; m_data.geometry = geometry_sys(); + updateFullFrameMargins(); QWindowSystemInterface::handleGeometryChange(window(), m_data.geometry); // QTBUG-32121: OpenGL/normal windows (with exception of ANGLE // which we no longer support in Qt 6) do not receive expose @@ -2202,6 +2298,9 @@ void QWindowsWindow::handleGeometryChange() if (testFlag(SynchronousGeometryChangeEvent)) QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); + if (!testFlag(ResizeMoveActive)) + updateRestoreGeometry(); + if (!wasSync) clearFlag(SynchronousGeometryChangeEvent); qCDebug(lcQpaEvents) << __FUNCTION__ << this << window() << m_data.geometry; @@ -2212,7 +2311,7 @@ void QWindowsBaseWindow::setGeometry_sys(const QRect &rect) const const QMargins margins = fullFrameMargins(); const QRect frameGeometry = rect + margins; - qCDebug(lcQpaWindows) << '>' << __FUNCTION__ << window() + qCDebug(lcQpaWindow) << '>' << __FUNCTION__ << window() << "\n from " << geometry_sys() << " frame: " << margins << " to " <<rect << " new frame: " << frameGeometry; @@ -2243,7 +2342,7 @@ void QWindowsBaseWindow::setGeometry_sys(const QRect &rect) const result = MoveWindow(hwnd, x, frameGeometry.y(), frameGeometry.width(), frameGeometry.height(), true); } - qCDebug(lcQpaWindows) << '<' << __FUNCTION__ << window() + qCDebug(lcQpaWindow) << '<' << __FUNCTION__ << window() << "\n resulting " << result << geometry_sys(); } @@ -2297,7 +2396,7 @@ bool QWindowsWindow::handleWmPaint(HWND hwnd, UINT message, return true; } // QTBUG-75455: Suppress WM_PAINT sent to invisible windows when setting WS_EX_LAYERED - if (!window()->isVisible() && (GetWindowLong(hwnd, GWL_EXSTYLE) & WS_EX_LAYERED) != 0) + if (!window()->isVisible() && (GetWindowLongPtr(hwnd, GWL_EXSTYLE) & WS_EX_LAYERED) != 0) return false; // Ignore invalid update bounding rectangles if (!GetUpdateRect(m_data.hwnd, 0, FALSE)) @@ -2329,7 +2428,7 @@ void QWindowsWindow::setWindowTitle(const QString &title) void QWindowsWindow::setWindowFlags(Qt::WindowFlags flags) { - qCDebug(lcQpaWindows) << '>' << __FUNCTION__ << this << window() << "\n from: " + qCDebug(lcQpaWindow) << '>' << __FUNCTION__ << this << window() << "\n from: " << m_data.flags << "\n to: " << flags; const QRect oldGeometry = geometry(); if (m_data.flags != flags) { @@ -2347,7 +2446,7 @@ void QWindowsWindow::setWindowFlags(Qt::WindowFlags flags) if (oldGeometry != newGeometry) handleGeometryChange(); - qCDebug(lcQpaWindows) << '<' << __FUNCTION__ << "\n returns: " + qCDebug(lcQpaWindow) << '<' << __FUNCTION__ << "\n returns: " << m_data.flags << " geometry " << oldGeometry << "->" << newGeometry; } @@ -2362,13 +2461,14 @@ QWindowsWindowData QWindowsWindow::setWindowFlags_sys(Qt::WindowFlags wt, QWindowsWindowData result = m_data; result.flags = creationData.flags; result.embedded = creationData.embedded; - result.hasFrame = (creationData.style & (WS_DLGFRAME | WS_THICKFRAME)); + result.hasFrame = (creationData.style & (WS_DLGFRAME | WS_THICKFRAME)) + && !(creationData.flags & Qt::FramelessWindowHint); return result; } void QWindowsWindow::handleWindowStateChange(Qt::WindowStates state) { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window() + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window() << "\n from " << m_windowState << " to " << state; m_windowState = state; QWindowSystemInterface::handleWindowStateChanged(window(), state); @@ -2376,6 +2476,14 @@ void QWindowsWindow::handleWindowStateChange(Qt::WindowStates state) handleHidden(); QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); // Tell QQuickWindow to stop rendering now. } else { + if (state & Qt::WindowMaximized) { + WINDOWPLACEMENT windowPlacement{}; + windowPlacement.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(m_data.hwnd, &windowPlacement); + const RECT geometry = RECTfromQRect(m_data.restoreGeometry); + windowPlacement.rcNormalPosition = geometry; + SetWindowPlacement(m_data.hwnd, &windowPlacement); + } // QTBUG-17548: We send expose events when receiving WM_Paint, but for // layered windows and transient children, we won't receive any WM_Paint. QWindow *w = window(); @@ -2399,6 +2507,11 @@ void QWindowsWindow::handleWindowStateChange(Qt::WindowStates state) } } +void QWindowsWindow::updateRestoreGeometry() +{ + m_data.restoreGeometry = normalFrameGeometry(m_data.hwnd); +} + void QWindowsWindow::setWindowState(Qt::WindowStates state) { if (m_data.hwnd) { @@ -2435,7 +2548,7 @@ void QWindowsWindow::setWindowState_sys(Qt::WindowStates newState) const Qt::WindowStates oldState = m_windowState; if (oldState == newState) return; - qCDebug(lcQpaWindows) << '>' << __FUNCTION__ << this << window() + qCDebug(lcQpaWindow) << '>' << __FUNCTION__ << this << window() << " from " << oldState << " to " << newState; const bool visible = isVisible(); @@ -2443,11 +2556,7 @@ void QWindowsWindow::setWindowState_sys(Qt::WindowStates newState) if (stateChange & Qt::WindowFullScreen) { if (newState & Qt::WindowFullScreen) { -#ifndef Q_FLATTEN_EXPOSE UINT newStyle = WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_POPUP; -#else - UINT newStyle = WS_POPUP; -#endif // Save geometry and style to be restored when fullscreen // is turned off again, since on Windows, it is not a real // Window state but emulated by changing geometry and style. @@ -2470,26 +2579,26 @@ void QWindowsWindow::setWindowState_sys(Qt::WindowStates newState) if (testFlag(HasBorderInFullScreen)) newStyle |= WS_BORDER; setStyle(newStyle); - // Use geometry of QWindow::screen() within creation or the virtual screen the - // window is in (QTBUG-31166, QTBUG-30724). - const QScreen *screen = window()->screen(); - if (!screen) - screen = QGuiApplication::primaryScreen(); - const QRect r = screen ? QHighDpi::toNativePixels(screen->geometry(), window()) : m_savedFrameGeometry; - + const HMONITOR monitor = MonitorFromWindow(m_data.hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfoW(monitor, &monitorInfo); + const QRect screenGeometry(monitorInfo.rcMonitor.left, monitorInfo.rcMonitor.top, + monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left, + monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top); if (newState & Qt::WindowMinimized) { - setMinimizedGeometry(m_data.hwnd, r); + setMinimizedGeometry(m_data.hwnd, screenGeometry); if (stateChange & Qt::WindowMaximized) setRestoreMaximizedFlag(m_data.hwnd, newState & Qt::WindowMaximized); } else { const UINT swpf = SWP_FRAMECHANGED | SWP_NOACTIVATE; const bool wasSync = testFlag(SynchronousGeometryChangeEvent); setFlag(SynchronousGeometryChangeEvent); - SetWindowPos(m_data.hwnd, HWND_TOP, r.left(), r.top(), r.width(), r.height(), swpf); + SetWindowPos(m_data.hwnd, HWND_TOP, screenGeometry.left(), screenGeometry.top(), screenGeometry.width(), screenGeometry.height(), swpf); if (!wasSync) clearFlag(SynchronousGeometryChangeEvent); clearFlag(MaximizeToFullScreen); - QWindowSystemInterface::handleGeometryChange(window(), r); + QWindowSystemInterface::handleGeometryChange(window(), screenGeometry); QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); } } else { @@ -2561,12 +2670,12 @@ void QWindowsWindow::setWindowState_sys(Qt::WindowStates newState) setRestoreMaximizedFlag(m_data.hwnd, newState & Qt::WindowMaximized); } } - qCDebug(lcQpaWindows) << '<' << __FUNCTION__ << this << window() << newState; + qCDebug(lcQpaWindow) << '<' << __FUNCTION__ << this << window() << newState; } void QWindowsWindow::setStyle(unsigned s) const { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window() << debugWinStyle(s); + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window() << debugWinStyle(s); setFlag(WithinSetStyle); SetWindowLongPtr(m_data.hwnd, GWL_STYLE, s); clearFlag(WithinSetStyle); @@ -2574,13 +2683,16 @@ void QWindowsWindow::setStyle(unsigned s) const void QWindowsWindow::setExStyle(unsigned s) const { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window() << debugWinExStyle(s); + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window() << debugWinExStyle(s); SetWindowLongPtr(m_data.hwnd, GWL_EXSTYLE, s); } bool QWindowsWindow::windowEvent(QEvent *event) { switch (event->type()) { + case QEvent::ApplicationPaletteChange: + setDarkBorder(QWindowsTheme::instance()->colorScheme() == Qt::ColorScheme::Dark); + break; case QEvent::WindowBlocked: // Blocked by another modal window. setEnabled(false); setFlag(BlockedByModal); @@ -2600,16 +2712,23 @@ bool QWindowsWindow::windowEvent(QEvent *event) void QWindowsWindow::propagateSizeHints() { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window(); + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window(); } bool QWindowsWindow::handleGeometryChangingMessage(MSG *message, const QWindow *qWindow, const QMargins &margins) { auto *windowPos = reinterpret_cast<WINDOWPOS *>(message->lParam); + const QRect suggestedFrameGeometry(windowPos->x, windowPos->y, + windowPos->cx, windowPos->cy); + const QRect suggestedGeometry = suggestedFrameGeometry - margins; // Tell Windows to discard the entire contents of the client area, as re-using // parts of the client area would lead to jitter during resize. - windowPos->flags |= SWP_NOCOPYBITS; + // Check the suggestedGeometry against the current one to only discard during + // resize, and not a plain move. We also look for SWP_NOSIZE since that, too, + // implies an identical size, and comparing QRects wouldn't work with null cx/cy + if (!(windowPos->flags & SWP_NOSIZE) && suggestedGeometry.size() != qWindow->geometry().size()) + windowPos->flags |= SWP_NOCOPYBITS; if ((windowPos->flags & SWP_NOZORDER) == 0) { if (QWindowsWindow *platformWindow = QWindowsWindow::windowsWindowOf(qWindow)) { @@ -2625,9 +2744,6 @@ bool QWindowsWindow::handleGeometryChangingMessage(MSG *message, const QWindow * return false; if (windowPos->flags & SWP_NOSIZE) return false; - const QRect suggestedFrameGeometry(windowPos->x, windowPos->y, - windowPos->cx, windowPos->cy); - const QRect suggestedGeometry = suggestedFrameGeometry - margins; const QRectF correctedGeometryF = QPlatformWindow::closestAcceptableGeometry(qWindow, suggestedGeometry); if (!correctedGeometryF.isValid()) return false; @@ -2649,8 +2765,10 @@ bool QWindowsWindow::handleGeometryChanging(MSG *message) const void QWindowsWindow::setFullFrameMargins(const QMargins &newMargins) { + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) + return; if (m_data.fullFrameMargins != newMargins) { - qCDebug(lcQpaWindows) << __FUNCTION__ << window() << m_data.fullFrameMargins << "->" << newMargins; + qCDebug(lcQpaWindow) << __FUNCTION__ << window() << m_data.fullFrameMargins << "->" << newMargins; m_data.fullFrameMargins = newMargins; } } @@ -2666,12 +2784,46 @@ void QWindowsWindow::updateFullFrameMargins() void QWindowsWindow::calculateFullFrameMargins() { + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) + return; + + // QTBUG-113736: systemMargins depends on AdjustWindowRectExForDpi. This doesn't take into + // account possible external modifications to the titlebar, as with ExtendsContentIntoTitleBar() + // from the Windows App SDK. We can fix this by comparing the WindowRect (which includes the + // frame) to the ClientRect. If a 'typical' frame is detected, i.e. only the titlebar has been + // modified, we can safely adjust the frame by deducting the bottom margin to the total Y + // difference between the two rects, to get the actual size of the titlebar and prevent + // unwanted client area slicing. + + RECT windowRect{}; + RECT clientRect{}; + GetWindowRect(handle(), &windowRect); + GetClientRect(handle(), &clientRect); + + // QTBUG-117704 It is also possible that the user has manually removed the frame (for example + // by handling WM_NCCALCSIZE). If that is the case, i.e., the client area and the window area + // have identical sizes, we don't want to override the user-defined margins. + + if (qrectFromRECT(windowRect).size() == qrectFromRECT(clientRect).size()) + return; + // Normally obtained from WM_NCCALCSIZE. This calculation only works // when no native menu is present. const auto systemMargins = testFlag(DisableNonClientScaling) ? QWindowsGeometryHint::frameOnPrimaryScreen(window(), m_data.hwnd) : frameMargins_sys(); - setFullFrameMargins(systemMargins + m_data.customMargins); + const QMargins actualMargins = systemMargins + customMargins(); + + const int yDiff = (windowRect.bottom - windowRect.top) - (clientRect.bottom - clientRect.top); + const bool typicalFrame = (actualMargins.left() == actualMargins.right()) + && (actualMargins.right() == actualMargins.bottom()); + + const QMargins adjustedMargins = typicalFrame ? + QMargins(actualMargins.left(), (yDiff - actualMargins.bottom()), + actualMargins.right(), actualMargins.bottom()) + : actualMargins; + + setFullFrameMargins(adjustedMargins); } QMargins QWindowsWindow::frameMargins() const @@ -2684,12 +2836,14 @@ QMargins QWindowsWindow::frameMargins() const QMargins QWindowsWindow::fullFrameMargins() const { + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) + return {}; return m_data.fullFrameMargins; } void QWindowsWindow::setOpacity(qreal level) { - qCDebug(lcQpaWindows) << __FUNCTION__ << level; + qCDebug(lcQpaWindow) << __FUNCTION__ << level; if (!qFuzzyCompare(m_opacity, level)) { m_opacity = level; if (m_data.hwnd) @@ -2749,41 +2903,76 @@ void QWindowsWindow::setMask(const QRegion ®ion) void QWindowsWindow::requestActivateWindow() { - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window(); - // 'Active' state handling is based in focus since it needs to work for - // child windows as well. - if (m_data.hwnd) { - const DWORD currentThread = GetCurrentThreadId(); - bool attached = false; - DWORD foregroundThread = 0; - - // QTBUG-14062, QTBUG-37435: Windows normally only flashes the taskbar entry - // when activating windows of inactive applications. Attach to the input of the - // currently active window while setting the foreground window to always activate - // the window when desired. - const auto activationBehavior = QWindowsIntegration::instance()->windowActivationBehavior(); - if (QGuiApplication::applicationState() != Qt::ApplicationActive - && activationBehavior == QWindowsApplication::AlwaysActivateWindow) { - if (const HWND foregroundWindow = GetForegroundWindow()) { - foregroundThread = GetWindowThreadProcessId(foregroundWindow, nullptr); - if (foregroundThread && foregroundThread != currentThread) - attached = AttachThreadInput(foregroundThread, currentThread, TRUE) == TRUE; - if (attached) { - if (!window()->flags().testFlag(Qt::WindowStaysOnBottomHint) - && !window()->flags().testFlag(Qt::WindowStaysOnTopHint) - && window()->type() != Qt::ToolTip) { - const UINT swpFlags = SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER; - SetWindowPos(m_data.hwnd, HWND_TOPMOST, 0, 0, 0, 0, swpFlags); - SetWindowPos(m_data.hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, swpFlags); - } - } - } - } + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window(); + + if (!m_data.hwnd) + return; + + const auto activationBehavior = QWindowsIntegration::instance()->windowActivationBehavior(); + if (QGuiApplication::applicationState() == Qt::ApplicationActive + || activationBehavior != QWindowsApplication::AlwaysActivateWindow) { SetForegroundWindow(m_data.hwnd); SetFocus(m_data.hwnd); - if (attached) - AttachThreadInput(foregroundThread, currentThread, FALSE); + return; } + + // Force activate this window. The following code will bring the window to the + // foreground and activate it. If the window is hidden, it will show up. If + // the window is minimized, it will restore to the previous position. + + // But first we need some sanity checks. + if (m_data.flags & Qt::WindowStaysOnBottomHint) { + qCWarning(lcQpaWindow) << + "Windows with Qt::WindowStaysOnBottomHint can't be brought to the foreground."; + return; + } + if (m_data.flags & Qt::WindowStaysOnTopHint) { + qCWarning(lcQpaWindow) << + "Windows with Qt::WindowStaysOnTopHint will always be on the foreground."; + return; + } + if (window()->type() == Qt::ToolTip) { + qCWarning(lcQpaWindow) << "ToolTip windows should not be activated."; + return; + } + + // We need to show the window first, otherwise we won't be able to bring it to front. + if (!IsWindowVisible(m_data.hwnd)) + ShowWindow(m_data.hwnd, SW_SHOW); + + if (IsIconic(m_data.hwnd)) { + ShowWindow(m_data.hwnd, SW_RESTORE); + // When the window is restored, it will always become the foreground window. + // So return early here, we don't need the following code to bring it to front. + return; + } + + // OK, our window is not minimized, so now we will try to bring it to front manually. + const HWND oldForegroundWindow = GetForegroundWindow(); + if (!oldForegroundWindow) // It may be NULL, according to MS docs. + return; + + // First try to send a message to the current foreground window to check whether + // it is currently hanging or not. + if (SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, + SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG, 1000, nullptr) == 0) { + qCWarning(lcQpaWindow) << "The foreground window hangs, can't activate current window."; + return; + } + + const DWORD windowThreadProcessId = GetWindowThreadProcessId(oldForegroundWindow, nullptr); + const DWORD currentThreadId = GetCurrentThreadId(); + + AttachThreadInput(windowThreadProcessId, currentThreadId, TRUE); + const auto cleanup = qScopeGuard([windowThreadProcessId, currentThreadId](){ + AttachThreadInput(windowThreadProcessId, currentThreadId, FALSE); + }); + + BringWindowToTop(m_data.hwnd); + + // Activate the window too. This will force us to the virtual desktop this + // window is on, if it's on another virtual desktop. + SetActiveWindow(m_data.hwnd); } bool QWindowsWindow::setKeyboardGrabEnabled(bool grab) @@ -2792,7 +2981,7 @@ bool QWindowsWindow::setKeyboardGrabEnabled(bool grab) qWarning("%s: No handle", __FUNCTION__); return false; } - qCDebug(lcQpaWindows) << __FUNCTION__ << this << window() << grab; + qCDebug(lcQpaWindow) << __FUNCTION__ << this << window() << grab; QWindowsContext *context = QWindowsContext::instance(); if (grab) { @@ -2806,7 +2995,7 @@ bool QWindowsWindow::setKeyboardGrabEnabled(bool grab) bool QWindowsWindow::setMouseGrabEnabled(bool grab) { - qCDebug(lcQpaWindows) << __FUNCTION__ << window() << grab; + qCDebug(lcQpaWindow) << __FUNCTION__ << window() << grab; if (!m_data.hwnd) { qWarning("%s: No handle", __FUNCTION__); return false; @@ -2881,37 +3070,7 @@ void QWindowsWindow::setFrameStrutEventsEnabled(bool enabled) void QWindowsWindow::getSizeHints(MINMAXINFO *mmi) const { QWindowsGeometryHint::applyToMinMaxInfo(window(), fullFrameMargins(), mmi); - - // This block fixes QTBUG-8361, QTBUG-4362: Frameless/title-less windows shouldn't cover the - // taskbar when maximized - if (m_data.flags.testFlag(Qt::FramelessWindowHint) - || (m_data.flags.testFlag(Qt::CustomizeWindowHint) && !m_data.flags.testFlag(Qt::WindowTitleHint))) { - if (QPlatformScreen *currentScreen = screen()) { - const QRect geometry = currentScreen->geometry(); - const QRect availableGeometry = currentScreen->availableGeometry(); - mmi->ptMaxSize.y = availableGeometry.height(); - - // Width, because you can have the taskbar on the sides too. - mmi->ptMaxSize.x = availableGeometry.width(); - - // If you have the taskbar on top, or on the left you don't want it at (0,0): - QPoint availablePositionDiff = availableGeometry.topLeft() - geometry.topLeft(); - mmi->ptMaxPosition.x = availablePositionDiff.x(); - mmi->ptMaxPosition.y = availablePositionDiff.y(); - if (!m_data.flags.testFlag(Qt::FramelessWindowHint)) { - const int borderWidth = invisibleMargins(m_data.hwnd).left(); - mmi->ptMaxSize.x += borderWidth * 2; - mmi->ptMaxSize.y += borderWidth * 2; - mmi->ptMaxTrackSize = mmi->ptMaxSize; - mmi->ptMaxPosition.x -= borderWidth; - mmi->ptMaxPosition.y -= borderWidth; - } - } else { - qWarning("screen() returned a null screen"); - } - } - - qCDebug(lcQpaWindows) << __FUNCTION__ << window() << *mmi; + qCDebug(lcQpaWindow) << __FUNCTION__ << window() << *mmi; } bool QWindowsWindow::handleNonClientHitTest(const QPoint &globalPos, LRESULT *result) const @@ -3023,7 +3182,7 @@ void QWindowsWindow::setCursor(const CursorHandlePtr &c) } if (changed) { const bool apply = applyNewCursor(window()); - qCDebug(lcQpaWindows) << window() << __FUNCTION__ + qCDebug(lcQpaWindow) << window() << __FUNCTION__ << c->handle() << " doApply=" << apply; m_cursor = c; if (apply) @@ -3128,17 +3287,6 @@ enum : WORD { DwmwaUseImmersiveDarkModeBefore20h1 = 19 }; -static bool queryDarkBorder(HWND hwnd) -{ - BOOL result = FALSE; - const bool ok = - SUCCEEDED(DwmGetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, &result, sizeof(result))) - || SUCCEEDED(DwmGetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeBefore20h1, &result, sizeof(result))); - if (!ok) - qWarning("%s: Unable to retrieve dark window border setting.", __FUNCTION__); - return result == TRUE; -} - bool QWindowsWindow::setDarkBorderToWindow(HWND hwnd, bool d) { const BOOL darkBorder = d ? TRUE : FALSE; @@ -3146,14 +3294,16 @@ bool QWindowsWindow::setDarkBorderToWindow(HWND hwnd, bool d) SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, &darkBorder, sizeof(darkBorder))) || SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeBefore20h1, &darkBorder, sizeof(darkBorder))); if (!ok) - qWarning("%s: Unable to set dark window border.", __FUNCTION__); + qCWarning(lcQpaWindow, "%s: Unable to set %s window border.", __FUNCTION__, d ? "dark" : "light"); return ok; } void QWindowsWindow::setDarkBorder(bool d) { - if (shouldApplyDarkFrame(window()) && queryDarkBorder(m_data.hwnd) != d) - setDarkBorderToWindow(m_data.hwnd, d); + // respect explicit opt-out and incompatible palettes or styles + d = d && shouldApplyDarkFrame(window()); + + setDarkBorderToWindow(m_data.hwnd, d); } QWindowsMenuBar *QWindowsWindow::menuBar() const @@ -3166,6 +3316,13 @@ void QWindowsWindow::setMenuBar(QWindowsMenuBar *mb) m_menuBar = mb; } +QMargins QWindowsWindow::customMargins() const +{ + if (m_data.flags & Qt::FramelessWindowHint) + return {}; + return m_data.customMargins; +} + /*! \brief Sets custom margins to be added to the default margins determined by the windows style in the handling of the WM_NCCALCSIZE message. @@ -3178,6 +3335,10 @@ void QWindowsWindow::setMenuBar(QWindowsMenuBar *mb) void QWindowsWindow::setCustomMargins(const QMargins &newCustomMargins) { + if (m_data.flags & Qt::FramelessWindowHint) { + qCWarning(lcQpaWindow) << "You should not set custom margins for a frameless window."; + return; + } if (newCustomMargins != m_data.customMargins) { const QMargins oldCustomMargins = m_data.customMargins; m_data.customMargins = newCustomMargins; @@ -3186,7 +3347,7 @@ void QWindowsWindow::setCustomMargins(const QMargins &newCustomMargins) const QPoint topLeft = currentFrameGeometry.topLeft(); QRect newFrame = currentFrameGeometry.marginsRemoved(oldCustomMargins) + m_data.customMargins; newFrame.moveTo(topLeft); - qCDebug(lcQpaWindows) << __FUNCTION__ << oldCustomMargins << "->" << newCustomMargins + qCDebug(lcQpaWindow) << __FUNCTION__ << oldCustomMargins << "->" << newCustomMargins << currentFrameGeometry << "->" << newFrame; SetWindowPos(m_data.hwnd, nullptr, newFrame.x(), newFrame.y(), newFrame.width(), newFrame.height(), SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE); } @@ -3269,24 +3430,6 @@ void QWindowsWindow::registerTouchWindow() qErrnoWarning("RegisterTouchWindow() failed for window '%s'.", qPrintable(window()->objectName())); } -void QWindowsWindow::aboutToMakeCurrent() -{ -#ifndef QT_NO_OPENGL - // For RasterGLSurface windows, that become OpenGL windows dynamically, it might be - // time to set up some GL specifics. This is particularly important for layered - // windows (WS_EX_LAYERED due to alpha > 0). - const bool isCompositing = qt_window_private(window())->compositing; - if (isCompositing != testFlag(Compositing)) { - if (isCompositing) - setFlag(Compositing); - else - clearFlag(Compositing); - - updateGLWindowSettings(window(), m_data.hwnd, m_data.flags, m_opacity); - } -#endif -} - void QWindowsWindow::setHasBorderInFullScreenStatic(QWindow *window, bool border) { if (QPlatformWindow *handle = window->handle()) diff --git a/src/plugins/platforms/windows/qwindowswindow.h b/src/plugins/platforms/windows/qwindowswindow.h index 29dfbdb856..024711e7f3 100644 --- a/src/plugins/platforms/windows/qwindowswindow.h +++ b/src/plugins/platforms/windows/qwindowswindow.h @@ -78,6 +78,7 @@ struct QWindowsWindowData { Qt::WindowFlags flags; QRect geometry; + QRect restoreGeometry; QMargins fullFrameMargins; // Do not use directly for windows, see FrameDirty. QMargins customMargins; // User-defined, additional frame for NCCALCSIZE HWND hwnd = nullptr; @@ -218,6 +219,8 @@ public: void setGeometry(const QRect &rect) override; QRect geometry() const override { return m_data.geometry; } QRect normalGeometry() const override; + QRect restoreGeometry() const { return m_data.restoreGeometry; } + void updateRestoreGeometry(); void setVisible(bool visible) override; bool isVisible() const; @@ -272,7 +275,7 @@ public: QWindowsMenuBar *menuBar() const; void setMenuBar(QWindowsMenuBar *mb); - QMargins customMargins() const override { return m_data.customMargins; } + QMargins customMargins() const override; void setCustomMargins(const QMargins &m) override; void setStyle(unsigned s) const; @@ -281,7 +284,7 @@ public: bool handleWmPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result); void handleMoved(); - void handleResized(int wParam); + void handleResized(int wParam, LPARAM lParam); void handleHidden(); void handleCompositionSettingsChanged(); void handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT *result); @@ -296,6 +299,7 @@ public: static inline void *userDataOf(HWND hwnd); static inline void setUserDataOf(HWND hwnd, void *ud); + static bool hasNoNativeFrame(HWND hwnd, Qt::WindowFlags flags); static bool setWindowLayered(HWND hwnd, Qt::WindowFlags flags, bool hasAlpha, qreal opacity); bool isLayered() const; @@ -320,7 +324,6 @@ public: void *surface(void *nativeConfig, int *err); void invalidateSurface() override; - void aboutToMakeCurrent(); void setAlertState(bool enabled) override; bool isAlertState() const override { return testFlag(AlertState); } @@ -342,6 +345,7 @@ public: void setSavedDpi(int dpi) { m_savedDpi = dpi; } int savedDpi() const { return m_savedDpi; } + qreal dpiRelativeScale(const UINT dpi) const; private: inline void show_sys() const; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp index 001cb8505b..5892493281 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp @@ -5,6 +5,7 @@ #if QT_CONFIG(accessibility) #include "qwindowsuiaaccessibility.h" +#include "qwindowsuiautomation.h" #include "qwindowsuiamainprovider.h" #include "qwindowsuiautils.h" @@ -14,13 +15,13 @@ #include <QtGui/private/qguiapplication_p.h> #include <QtCore/qt_windows.h> #include <qpa/qplatformintegration.h> -#include <QtGui/private/qwindowsuiawrapper_p.h> #include <QtCore/private/qwinregistry_p.h> QT_BEGIN_NAMESPACE using namespace QWindowsUiAutomation; +using namespace Qt::Literals::StringLiterals; bool QWindowsUiaAccessibility::m_accessibleActive = false; @@ -46,7 +47,7 @@ bool QWindowsUiaAccessibility::handleWmGetObject(HWND hwnd, WPARAM wParam, LPARA if (QWindow *window = QWindowsContext::instance()->findWindow(hwnd)) { if (QAccessibleInterface *accessible = window->accessibleRoot()) { QWindowsUiaMainProvider *provider = QWindowsUiaMainProvider::providerForAccessible(accessible); - *lResult = QWindowsUiaWrapper::instance()->returnRawElementProvider(hwnd, wParam, lParam, provider); + *lResult = UiaReturnRawElementProvider(hwnd, wParam, lParam, provider); return true; } } @@ -78,8 +79,8 @@ static QString alertSound(const QObject *object) static QString soundFileName(const QString &soundName) { - const QString key = QStringLiteral("AppEvents\\Schemes\\Apps\\.Default\\") - + soundName + QStringLiteral("\\.Current"); + const QString key = "AppEvents\\Schemes\\Apps\\.Default\\"_L1 + + soundName + "\\.Current"_L1; return QWinRegistryKey(HKEY_CURRENT_USER, key).stringValue(L""); } @@ -97,11 +98,7 @@ void QWindowsUiaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event if (!event) return; - // Ignore events sent before the first UI Automation - // request or while QAccessible is being activated. - if (!m_accessibleActive) - return; - + // Always handle system sound events switch (event->type()) { case QAccessible::PopupMenuStart: playSystemSound(QStringLiteral("MenuPopup")); @@ -116,19 +113,23 @@ void QWindowsUiaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event break; } - QAccessibleInterface *accessible = event->accessibleInterface(); - if (!isActive() || !accessible || !accessible->isValid()) + // Ignore events sent before the first UI Automation + // request or while QAccessible is being activated. + if (!m_accessibleActive) return; - // Ensures QWindowsUiaWrapper is properly initialized. - if (!QWindowsUiaWrapper::instance()->ready()) + QAccessibleInterface *accessible = event->accessibleInterface(); + if (!isActive() || !accessible || !accessible->isValid()) return; // No need to do anything when nobody is listening. - if (!QWindowsUiaWrapper::instance()->clientsAreListening()) + if (!UiaClientsAreListening()) return; switch (event->type()) { + case QAccessible::Announcement: + QWindowsUiaMainProvider::raiseNotification(static_cast<QAccessibleAnnouncementEvent *>(event)); + break; case QAccessible::Focus: QWindowsUiaMainProvider::notifyFocusChange(event); break; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h index 1813bb4d89..2e8ee585da 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h @@ -7,6 +7,7 @@ #include <QtGui/qtguiglobal.h> #if QT_CONFIG(accessibility) +#include <QtCore/qt_windows.h> #include "qwindowscontext.h" #include <qpa/qplatformaccessibility.h> diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h index c899b4096e..032679ab10 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h @@ -10,8 +10,8 @@ #include <QtGui/qaccessible.h> #include <QtCore/qpointer.h> -#include <qwindowscombase.h> -#include <QtGui/private/qwindowsuiawrapper_p.h> +#include "qwindowsuiautomation.h" +#include <QtCore/private/qcomobject_p.h> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h index 49b37ba295..b384eb521c 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Expand/Collapse control pattern provider. Used for menu items with submenus. class QWindowsUiaExpandCollapseProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IExpandCollapseProvider> + public QComObject<IExpandCollapseProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaExpandCollapseProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h index b501616966..289a867869 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Grid Item control pattern provider. Used by items within a table/tree. class QWindowsUiaGridItemProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IGridItemProvider> + public QComObject<IGridItemProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaGridItemProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h index b905133f6b..d33bbd0429 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h @@ -12,8 +12,7 @@ QT_BEGIN_NAMESPACE // Implements the Grid control pattern provider. Used by tables/trees. -class QWindowsUiaGridProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IGridProvider> +class QWindowsUiaGridProvider : public QWindowsUiaBaseProvider, public QComObject<IGridProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaGridProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h index eca7e73039..ec006c673e 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h @@ -12,8 +12,7 @@ QT_BEGIN_NAMESPACE // Implements the Invoke control pattern provider. -class QWindowsUiaInvokeProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IInvokeProvider> +class QWindowsUiaInvokeProvider : public QWindowsUiaBaseProvider, public QComObject<IInvokeProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaInvokeProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp index 4d4d706b83..be88ab4ae8 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp @@ -18,7 +18,6 @@ #include "qwindowsuiagriditemprovider.h" #include "qwindowsuiawindowprovider.h" #include "qwindowsuiaexpandcollapseprovider.h" -#include "qwindowscombase.h" #include "qwindowscontext.h" #include "qwindowsuiautils.h" #include "qwindowsuiaprovidercache.h" @@ -27,6 +26,7 @@ #include <QtGui/qaccessible.h> #include <QtGui/qguiapplication.h> #include <QtGui/qwindow.h> +#include <qpa/qplatforminputcontextfactory_p.h> #if !defined(Q_CC_BOR) && !defined (Q_CC_GNU) #include <comdef.h> @@ -38,10 +38,13 @@ QT_BEGIN_NAMESPACE using namespace QWindowsUiAutomation; +QMutex QWindowsUiaMainProvider::m_mutex; // Returns a cached instance of the provider for a specific accessible interface. QWindowsUiaMainProvider *QWindowsUiaMainProvider::providerForAccessible(QAccessibleInterface *accessible) { + QMutexLocker locker(&m_mutex); + if (!accessible) return nullptr; @@ -58,9 +61,8 @@ QWindowsUiaMainProvider *QWindowsUiaMainProvider::providerForAccessible(QAccessi return provider; } -QWindowsUiaMainProvider::QWindowsUiaMainProvider(QAccessibleInterface *a, int initialRefCount) - : QWindowsUiaBaseProvider(QAccessible::uniqueId(a)), - m_ref(initialRefCount) +QWindowsUiaMainProvider::QWindowsUiaMainProvider(QAccessibleInterface *a) + : QWindowsUiaBaseProvider(QAccessible::uniqueId(a)) { } @@ -71,12 +73,13 @@ QWindowsUiaMainProvider::~QWindowsUiaMainProvider() void QWindowsUiaMainProvider::notifyFocusChange(QAccessibleEvent *event) { if (QAccessibleInterface *accessible = event->accessibleInterface()) { - // If this is a table/tree/list, raise event for the focused cell/item instead. - if (accessible->tableInterface()) + // If this is a complex element, raise event for the focused child instead. + if (accessible->childCount()) { if (QAccessibleInterface *child = accessible->focusChild()) accessible = child; + } if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_AutomationFocusChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_AutomationFocusChangedEventId); } } @@ -93,7 +96,7 @@ void QWindowsUiaMainProvider::notifyStateChange(QAccessibleStateChangeEvent *eve if (accessible->state().checked) toggleState = accessible->state().checkStateMixed ? ToggleState_Indeterminate : ToggleState_On; setVariantI4(toggleState, &newVal); - QWindowsUiaWrapper::instance()->raiseAutomationPropertyChangedEvent(provider, UIA_ToggleToggleStatePropertyId, oldVal, newVal); + UiaRaiseAutomationPropertyChangedEvent(provider, UIA_ToggleToggleStatePropertyId, oldVal, newVal); } } } @@ -102,9 +105,13 @@ void QWindowsUiaMainProvider::notifyStateChange(QAccessibleStateChangeEvent *eve // Notifies window opened/closed. if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { if (accessible->state().active) { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Window_WindowOpenedEventId); + UiaRaiseAutomationEvent(provider, UIA_Window_WindowOpenedEventId); + if (QAccessibleInterface *focused = accessible->focusChild()) { + if (QWindowsUiaMainProvider *focusedProvider = providerForAccessible(focused)) + UiaRaiseAutomationEvent(focusedProvider, UIA_AutomationFocusChangedEventId); + } } else { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Window_WindowClosedEventId); + UiaRaiseAutomationEvent(provider, UIA_Window_WindowClosedEventId); } } } @@ -133,27 +140,11 @@ void QWindowsUiaMainProvider::notifyValueChange(QAccessibleValueChangeEvent *eve } if (event->value().typeId() == QMetaType::QString) { if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { - - // Tries to notify the change using UiaRaiseNotificationEvent(), which is only available on - // Windows 10 version 1709 or newer. Otherwise uses UiaRaiseAutomationPropertyChangedEvent(). - - BSTR displayString = bStrFromQString(event->value().toString()); - BSTR activityId = bStrFromQString(QString()); - - HRESULT hr = QWindowsUiaWrapper::instance()->raiseNotificationEvent(provider, NotificationKind_Other, - NotificationProcessing_ImportantMostRecent, - displayString, activityId); - - ::SysFreeString(displayString); - ::SysFreeString(activityId); - - if (hr == static_cast<HRESULT>(UIA_E_NOTSUPPORTED)) { - VARIANT oldVal, newVal; - clearVariant(&oldVal); - setVariantString(event->value().toString(), &newVal); - QWindowsUiaWrapper::instance()->raiseAutomationPropertyChangedEvent(provider, UIA_ValueValuePropertyId, oldVal, newVal); - ::SysFreeString(newVal.bstrVal); - } + // Notifies changes in string values. + VARIANT oldVal, newVal; + clearVariant(&oldVal); + setVariantString(event->value().toString(), &newVal); + UiaRaiseAutomationPropertyChangedEvent(provider, UIA_ValueValuePropertyId, oldVal, newVal); } } else if (QAccessibleValueInterface *valueInterface = accessible->valueInterface()) { if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { @@ -161,7 +152,7 @@ void QWindowsUiaMainProvider::notifyValueChange(QAccessibleValueChangeEvent *eve VARIANT oldVal, newVal; clearVariant(&oldVal); setVariantDouble(valueInterface->currentValue().toDouble(), &newVal); - QWindowsUiaWrapper::instance()->raiseAutomationPropertyChangedEvent(provider, UIA_RangeValueValuePropertyId, oldVal, newVal); + UiaRaiseAutomationPropertyChangedEvent(provider, UIA_RangeValueValuePropertyId, oldVal, newVal); } } } @@ -177,7 +168,7 @@ void QWindowsUiaMainProvider::notifyNameChange(QAccessibleEvent *event) VARIANT oldVal, newVal; clearVariant(&oldVal); setVariantString(accessible->text(QAccessible::Name), &newVal); - QWindowsUiaWrapper::instance()->raiseAutomationPropertyChangedEvent(provider, UIA_NamePropertyId, oldVal, newVal); + UiaRaiseAutomationPropertyChangedEvent(provider, UIA_NamePropertyId, oldVal, newVal); ::SysFreeString(newVal.bstrVal); } } @@ -188,7 +179,7 @@ void QWindowsUiaMainProvider::notifySelectionChange(QAccessibleEvent *event) { if (QAccessibleInterface *accessible = event->accessibleInterface()) { if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_SelectionItem_ElementSelectedEventId); + UiaRaiseAutomationEvent(provider, UIA_SelectionItem_ElementSelectedEventId); } } } @@ -200,46 +191,59 @@ void QWindowsUiaMainProvider::notifyTextChange(QAccessibleEvent *event) if (accessible->textInterface()) { if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { if (event->type() == QAccessible::TextSelectionChanged) { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); } else if (event->type() == QAccessible::TextCaretMoved) { if (!accessible->state().readOnly) { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); } } else { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Text_TextChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_Text_TextChangedEventId); } } } } } -HRESULT STDMETHODCALLTYPE QWindowsUiaMainProvider::QueryInterface(REFIID iid, LPVOID *iface) +void QWindowsUiaMainProvider::raiseNotification(QAccessibleAnnouncementEvent *event) { - if (!iface) - return E_INVALIDARG; - *iface = nullptr; - - QAccessibleInterface *accessible = accessibleInterface(); - - const bool result = qWindowsComQueryUnknownInterfaceMulti<IRawElementProviderSimple>(this, iid, iface) - || qWindowsComQueryInterface<IRawElementProviderSimple>(this, iid, iface) - || qWindowsComQueryInterface<IRawElementProviderFragment>(this, iid, iface) - || (accessible && hwndForAccessible(accessible) && qWindowsComQueryInterface<IRawElementProviderFragmentRoot>(this, iid, iface)); - return result ? S_OK : E_NOINTERFACE; + if (QAccessibleInterface *accessible = event->accessibleInterface()) { + if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { + BSTR message = bStrFromQString(event->message()); + QAccessible::AnnouncementPriority prio = event->priority(); + NotificationProcessing processing = (prio == QAccessible::AnnouncementPriority::Assertive) + ? NotificationProcessing_ImportantAll + : NotificationProcessing_All; + BSTR activityId = bStrFromQString(QString::fromLatin1("")); + UiaRaiseNotificationEvent(provider, NotificationKind_Other, processing, message, activityId); + + ::SysFreeString(message); + ::SysFreeString(activityId); + } + } } -ULONG QWindowsUiaMainProvider::AddRef() +HRESULT STDMETHODCALLTYPE QWindowsUiaMainProvider::QueryInterface(REFIID iid, LPVOID *iface) { - return ++m_ref; + HRESULT result = QComObject::QueryInterface(iid, iface); + + if (SUCCEEDED(result) && iid == __uuidof(IRawElementProviderFragmentRoot)) { + QAccessibleInterface *accessible = accessibleInterface(); + if (accessible && hwndForAccessible(accessible)) { + result = S_OK; + } else { + result = E_NOINTERFACE; + iface = nullptr; + } + } + + return result; } ULONG STDMETHODCALLTYPE QWindowsUiaMainProvider::Release() { - if (!--m_ref) { - delete this; - return 0; - } - return m_ref; + QMutexLocker locker(&m_mutex); + + return QComObject::Release(); } HRESULT QWindowsUiaMainProvider::get_ProviderOptions(ProviderOptions *pRetVal) @@ -294,15 +298,20 @@ HRESULT QWindowsUiaMainProvider::GetPatternProvider(PATTERNID idPattern, IUnknow *pRetVal = new QWindowsUiaToggleProvider(id()); break; case UIA_SelectionPatternId: - // Lists of items. - if (accessible->role() == QAccessible::List) { + case UIA_SelectionPattern2Id: + // Selections via QAccessibleSelectionInterface or lists of items. + if (accessible->selectionInterface() + || accessible->role() == QAccessible::List + || accessible->role() == QAccessible::PageTabList) { *pRetVal = new QWindowsUiaSelectionProvider(id()); } break; case UIA_SelectionItemPatternId: - // Items within a list and radio buttons. - if ((accessible->role() == QAccessible::RadioButton) - || (accessible->role() == QAccessible::ListItem)) { + // Parent supports selection interface or items within a list and radio buttons. + if ((accessible->parent() && accessible->parent()->selectionInterface()) + || (accessible->role() == QAccessible::RadioButton) + || (accessible->role() == QAccessible::ListItem) + || (accessible->role() == QAccessible::PageTab)) { *pRetVal = new QWindowsUiaSelectionItemProvider(id()); } break; @@ -345,7 +354,8 @@ HRESULT QWindowsUiaMainProvider::GetPatternProvider(PATTERNID idPattern, IUnknow if ((accessible->role() == QAccessible::MenuItem && accessible->childCount() > 0 && accessible->child(0)->role() == QAccessible::PopupMenu) - || accessible->role() == QAccessible::ComboBox) { + || accessible->role() == QAccessible::ComboBox + || (accessible->role() == QAccessible::TreeItem && accessible->state().expandable)) { *pRetVal = new QWindowsUiaExpandCollapseProvider(id()); } break; @@ -356,6 +366,115 @@ HRESULT QWindowsUiaMainProvider::GetPatternProvider(PATTERNID idPattern, IUnknow return S_OK; } +void QWindowsUiaMainProvider::fillVariantArrayForRelation(QAccessibleInterface* accessible, + QAccessible::Relation relation, VARIANT *pRetVal) +{ + Q_ASSERT(accessible); + + typedef QPair<QAccessibleInterface*, QAccessible::Relation> RelationPair; + const QList<RelationPair> relationInterfaces = accessible->relations(relation); + if (relationInterfaces.empty()) + return; + + SAFEARRAY *elements = SafeArrayCreateVector(VT_UNKNOWN, 0, relationInterfaces.size()); + for (LONG i = 0; i < relationInterfaces.size(); ++i) { + if (QWindowsUiaMainProvider *childProvider = QWindowsUiaMainProvider::providerForAccessible(relationInterfaces.at(i).first)) { + SafeArrayPutElement(elements, &i, static_cast<IRawElementProviderSimple*>(childProvider)); + childProvider->Release(); + } + } + + pRetVal->vt = VT_UNKNOWN | VT_ARRAY; + pRetVal->parray = elements; +} + +void QWindowsUiaMainProvider::setAriaProperties(QAccessibleInterface *accessible, VARIANT *pRetVal) +{ + Q_ASSERT(accessible); + + QAccessibleAttributesInterface *attributesIface = accessible->attributesInterface(); + if (!attributesIface) + return; + + QString ariaString; + const QList<QAccessible::Attribute> attrKeys = attributesIface->attributeKeys(); + for (qsizetype i = 0; i < attrKeys.size(); ++i) { + if (i != 0) + ariaString += QStringLiteral(";"); + const QAccessible::Attribute key = attrKeys.at(i); + const QVariant value = attributesIface->attributeValue(key); + // see "Core Accessibility API Mappings" spec: https://www.w3.org/TR/core-aam-1.2/ + switch (key) { + case QAccessible::Attribute::Custom: + { + // forward custom attributes as-is + Q_ASSERT((value.canConvert<QHash<QString, QString>>())); + const QHash<QString, QString> attrMap = value.value<QHash<QString, QString>>(); + for (auto [name, val] : attrMap.asKeyValueRange()) { + if (name != *attrMap.keyBegin()) + ariaString += QStringLiteral(";"); + ariaString += name + QStringLiteral("=") + val; + } + break; + } + case QAccessible::Attribute::Level: + Q_ASSERT(value.canConvert<int>()); + ariaString += QStringLiteral("level=") + QString::number(value.toInt()); + break; + default: + break; + } + } + + setVariantString(ariaString, pRetVal); +} + +void QWindowsUiaMainProvider::setStyle(QAccessibleInterface *accessible, VARIANT *pRetVal) +{ + Q_ASSERT(accessible); + + QAccessibleAttributesInterface *attributesIface = accessible->attributesInterface(); + if (!attributesIface) + return; + + // currently, only heading styles are implemented here + if (accessible->role() != QAccessible::Role::Heading) + return; + + const QVariant levelVariant = attributesIface->attributeValue(QAccessible::Attribute::Level); + if (!levelVariant.isValid()) + return; + + Q_ASSERT(levelVariant.canConvert<int>()); + // UIA only has styles for heading levels 1-9 + const int level = levelVariant.toInt(); + if (level < 1 || level > 9) + return; + + const int styleId = styleIdForHeadingLevel(level); + setVariantI4(styleId, pRetVal); +} + +int QWindowsUiaMainProvider::styleIdForHeadingLevel(int headingLevel) +{ + // only heading levels 1-9 have a corresponding UIA style ID + Q_ASSERT(headingLevel > 0 && headingLevel <= 9); + + static constexpr int styles[] = { + StyleId_Heading1, + StyleId_Heading2, + StyleId_Heading3, + StyleId_Heading4, + StyleId_Heading5, + StyleId_Heading6, + StyleId_Heading7, + StyleId_Heading8, + StyleId_Heading9, + }; + + return styles[headingLevel - 1]; +} + HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pRetVal) { qCDebug(lcQpaUiAutomation) << __FUNCTION__ << idProp; @@ -379,6 +498,9 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR // Accelerator key. setVariantString(accessible->text(QAccessible::Accelerator), pRetVal); break; + case UIA_AriaPropertiesPropertyId: + setAriaProperties(accessible, pRetVal); + break; case UIA_AutomationIdPropertyId: // Automation ID, which can be used by tools to select a specific control in the UI. setVariantString(automationIdForAccessible(accessible), pRetVal); @@ -390,6 +512,15 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR setVariantString(className, pRetVal); } break; + case UIA_DescribedByPropertyId: + fillVariantArrayForRelation(accessible, QAccessible::DescriptionFor, pRetVal); + break; + case UIA_FlowsFromPropertyId: + fillVariantArrayForRelation(accessible, QAccessible::FlowsTo, pRetVal); + break; + case UIA_FlowsToPropertyId: + fillVariantArrayForRelation(accessible, QAccessible::FlowsFrom, pRetVal); + break; case UIA_FrameworkIdPropertyId: setVariantString(QStringLiteral("Qt"), pRetVal); break; @@ -403,7 +534,7 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR // The native OSK should be disabled if the Qt OSK is in use, // or if disabled via application attribute. - static bool imModuleEmpty = qEnvironmentVariableIsEmpty("QT_IM_MODULE"); + static bool imModuleEmpty = QPlatformInputContextFactory::requested().isEmpty(); bool nativeVKDisabled = QCoreApplication::testAttribute(Qt::AA_DisableNativeVirtualKeyboard); // If we want to disable the native OSK auto-showing @@ -470,6 +601,9 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR setVariantString(name, pRetVal); break; } + case UIA_StyleIdAttributeId: + setStyle(accessible, pRetVal); + break; default: break; } @@ -506,7 +640,7 @@ HRESULT QWindowsUiaMainProvider::get_HostRawElementProvider(IRawElementProviderS // Returns a host provider only for controls associated with a native window handle. Others should return NULL. if (QAccessibleInterface *accessible = accessibleInterface()) { if (HWND hwnd = hwndForAccessible(accessible)) { - return QWindowsUiaWrapper::instance()->hostProviderFromHwnd(hwnd, pRetVal); + return UiaHostProviderFromHwnd(hwnd, pRetVal); } } return S_OK; @@ -696,26 +830,18 @@ HRESULT QWindowsUiaMainProvider::ElementProviderFromPoint(double x, double y, IR QPoint point; nativeUiaPointToPoint(uiaPoint, window, &point); - if (auto targetacc = accessible->childAt(point.x(), point.y())) { - auto acc = accessible->childAt(point.x(), point.y()); - // Reject the cases where childAt() returns a different instance in each call for the same - // element (e.g., QAccessibleTree), as it causes an endless loop with Youdao Dictionary installed. - if (targetacc == acc) { - // Controls can be embedded within grouping elements. By default returns the innermost control. - while (acc) { - targetacc = acc; - // For accessibility tools it may be better to return the text element instead of its subcomponents. - if (targetacc->textInterface()) break; - acc = targetacc->childAt(point.x(), point.y()); - if (acc != targetacc->childAt(point.x(), point.y())) { - qCDebug(lcQpaUiAutomation) << "Non-unique childAt() for" << targetacc; - break; - } - } - *pRetVal = providerForAccessible(targetacc); - } else { - qCDebug(lcQpaUiAutomation) << "Non-unique childAt() for" << accessible; + QAccessibleInterface *targetacc = accessible->childAt(point.x(), point.y()); + + if (targetacc) { + QAccessibleInterface *acc = targetacc; + // Controls can be embedded within grouping elements. By default returns the innermost control. + while (acc) { + targetacc = acc; + // For accessibility tools it may be better to return the text element instead of its subcomponents. + if (targetacc->textInterface()) break; + acc = acc->childAt(point.x(), point.y()); } + *pRetVal = providerForAccessible(targetacc); } return S_OK; } diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h index 5659a28e35..dafe877974 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h @@ -11,6 +11,7 @@ #include <QtCore/qpointer.h> #include <QtCore/qsharedpointer.h> +#include <QtCore/qmutex.h> #include <QtCore/qt_windows.h> #include <QtGui/qaccessible.h> @@ -19,15 +20,13 @@ QT_BEGIN_NAMESPACE // The main UI Automation class. class QWindowsUiaMainProvider : public QWindowsUiaBaseProvider, - public IRawElementProviderSimple, - public IRawElementProviderFragment, - public IRawElementProviderFragmentRoot + public QComObject<IRawElementProviderSimple, IRawElementProviderFragment, IRawElementProviderFragmentRoot> { Q_OBJECT Q_DISABLE_COPY_MOVE(QWindowsUiaMainProvider) public: static QWindowsUiaMainProvider *providerForAccessible(QAccessibleInterface *accessible); - explicit QWindowsUiaMainProvider(QAccessibleInterface *a, int initialRefCount = 1); + explicit QWindowsUiaMainProvider(QAccessibleInterface *a); virtual ~QWindowsUiaMainProvider(); static void notifyFocusChange(QAccessibleEvent *event); static void notifyStateChange(QAccessibleStateChangeEvent *event); @@ -35,10 +34,10 @@ public: static void notifyNameChange(QAccessibleEvent *event); static void notifySelectionChange(QAccessibleEvent *event); static void notifyTextChange(QAccessibleEvent *event); + static void raiseNotification(QAccessibleAnnouncementEvent *event); // IUnknown HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id, LPVOID *iface) override; - ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE Release() override; // IRawElementProviderSimple methods @@ -61,7 +60,12 @@ public: private: QString automationIdForAccessible(const QAccessibleInterface *accessible); - ULONG m_ref; + static void fillVariantArrayForRelation(QAccessibleInterface *accessible, QAccessible::Relation relation, VARIANT *pRetVal); + static void setAriaProperties(QAccessibleInterface *accessible, VARIANT *pRetVal); + static void setStyle(QAccessibleInterface *accessible, VARIANT *pRetVal); + /** Returns the UIA style ID for a heading level from 1 to 9. */ + static int styleIdForHeadingLevel(int headingLevel); + static QMutex m_mutex; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp index c53b130b92..ba2a88bb4e 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp @@ -39,7 +39,7 @@ void QWindowsUiaProviderCache::insert(QAccessible::Id id, QWindowsUiaBaseProvide m_providerTable[id] = provider; m_inverseTable[provider] = id; // Connects the destroyed signal to our slot, to remove deleted objects from the cache. - QObject::connect(provider, &QObject::destroyed, this, &QWindowsUiaProviderCache::objectDestroyed); + QObject::connect(provider, &QObject::destroyed, this, &QWindowsUiaProviderCache::objectDestroyed, Qt::DirectConnection); } } diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiarangevalueprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiarangevalueprovider.h index 3e06961a82..ffb5ae155b 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiarangevalueprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiarangevalueprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Range Value control pattern provider. class QWindowsUiaRangeValueProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IRangeValueProvider> + public QComObject<IRangeValueProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaRangeValueProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp index 14ec29c75b..95bc2f7570 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp @@ -36,12 +36,20 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::Select() if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; + if (QAccessibleInterface *parent = accessible->parent()) { + if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) { + selectionInterface->clear(); + bool ok = selectionInterface->select(accessible); + return ok ? S_OK : S_FALSE; + } + } + QAccessibleActionInterface *actionInterface = accessible->actionInterface(); if (!actionInterface) return UIA_E_ELEMENTNOTAVAILABLE; - if (accessible->role() == QAccessible::RadioButton) { - // For radio buttons we just invoke the selection action; others are automatically deselected. + if (accessible->role() == QAccessible::RadioButton || accessible->role() == QAccessible::PageTab) { + // For radio buttons/tabs we just invoke the selection action; others are automatically deselected. actionInterface->doAction(QAccessibleActionInterface::pressAction()); } else { // Toggle list item if not already selected. It must be done first to support all selection modes. @@ -73,12 +81,19 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::AddToSelection() if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; + if (QAccessibleInterface *parent = accessible->parent()) { + if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) { + bool ok = selectionInterface->select(accessible); + return ok ? S_OK : S_FALSE; + } + } + QAccessibleActionInterface *actionInterface = accessible->actionInterface(); if (!actionInterface) return UIA_E_ELEMENTNOTAVAILABLE; - if (accessible->role() == QAccessible::RadioButton) { - // For radio buttons we invoke the selection action. + if (accessible->role() == QAccessible::RadioButton || accessible->role() == QAccessible::PageTab) { + // For radio buttons and tabs we invoke the selection action. actionInterface->doAction(QAccessibleActionInterface::pressAction()); } else { // Toggle list item if not already selected. @@ -98,11 +113,18 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::RemoveFromSelection( if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; + if (QAccessibleInterface *parent = accessible->parent()) { + if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) { + bool ok = selectionInterface->unselect(accessible); + return ok ? S_OK : S_FALSE; + } + } + QAccessibleActionInterface *actionInterface = accessible->actionInterface(); if (!actionInterface) return UIA_E_ELEMENTNOTAVAILABLE; - if (accessible->role() != QAccessible::RadioButton) { + if (accessible->role() != QAccessible::RadioButton && accessible->role() != QAccessible::PageTab) { if (accessible->state().selected) { actionInterface->doAction(QAccessibleActionInterface::toggleAction()); } @@ -124,8 +146,18 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::get_IsSelected(BOOL if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; + if (QAccessibleInterface *parent = accessible->parent()) { + if (QAccessibleSelectionInterface *selectionInterface = parent->selectionInterface()) { + bool selected = selectionInterface->isSelected(accessible); + *pRetVal = selected ? TRUE : FALSE; + return S_OK; + } + } + if (accessible->role() == QAccessible::RadioButton) *pRetVal = accessible->state().checked; + else if (accessible->role() == QAccessible::PageTab) + *pRetVal = accessible->state().focused; else *pRetVal = accessible->state().selected; return S_OK; @@ -144,16 +176,21 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionItemProvider::get_SelectionContain if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; + QAccessibleInterface *parent = accessible->parent(); + if (parent && parent->selectionInterface()) { + *pRetVal = QWindowsUiaMainProvider::providerForAccessible(parent); + return S_OK; + } + QAccessibleActionInterface *actionInterface = accessible->actionInterface(); if (!actionInterface) return UIA_E_ELEMENTNOTAVAILABLE; // Radio buttons do not require a container. - if (accessible->role() == QAccessible::ListItem) { - if (QAccessibleInterface *parent = accessible->parent()) { - if (parent->role() == QAccessible::List) { - *pRetVal = QWindowsUiaMainProvider::providerForAccessible(parent); - } + if (parent) { + if ((accessible->role() == QAccessible::ListItem && parent->role() == QAccessible::List) + || (accessible->role() == QAccessible::PageTab && parent->role() == QAccessible::PageTabList)) { + *pRetVal = QWindowsUiaMainProvider::providerForAccessible(parent); } } return S_OK; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.h index b510ae951c..ee34fd9edd 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Selection Item control pattern provider. Used for List items and radio buttons. class QWindowsUiaSelectionItemProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<ISelectionItemProvider> + public QComObject<ISelectionItemProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaSelectionItemProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp index 1d110a637e..37148c655a 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp @@ -41,12 +41,24 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::GetSelection(SAFEARRAY * if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; - // First put selected items in a list, then build a safe array with the right size. + // First get/create list of selected items, then build a safe array with the right size. QList<QAccessibleInterface *> selectedList; - for (int i = 0; i < accessible->childCount(); ++i) { - if (QAccessibleInterface *child = accessible->child(i)) { - if (child->state().selected) { - selectedList.append(child); + if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) { + selectedList = selectionInterface->selectedItems(); + } else { + const int childCount = accessible->childCount(); + selectedList.reserve(childCount); + for (int i = 0; i < childCount; ++i) { + if (QAccessibleInterface *child = accessible->child(i)) { + if (accessible->role() == QAccessible::PageTabList) { + if (child->role() == QAccessible::PageTab && child->state().focused) { + selectedList.append(child); + } + } else { + if (child->state().selected) { + selectedList.append(child); + } + } } } } @@ -90,18 +102,155 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_IsSelectionRequired( if (!accessible) return UIA_E_ELEMENTNOTAVAILABLE; - // Initially returns false if none are selected. After the first selection, it may be required. - bool anySelected = false; - for (int i = 0; i < accessible->childCount(); ++i) { - if (QAccessibleInterface *child = accessible->child(i)) { - if (child->state().selected) { - anySelected = true; - break; + if (accessible->role() == QAccessible::PageTabList) { + *pRetVal = TRUE; + } else { + + // Initially returns false if none are selected. After the first selection, it may be required. + bool anySelected = false; + if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) { + anySelected = selectionInterface->selectedItem(0) != nullptr; + } else { + for (int i = 0; i < accessible->childCount(); ++i) { + if (QAccessibleInterface *child = accessible->child(i)) { + if (child->state().selected) { + anySelected = true; + break; + } + } + } + } + + *pRetVal = anySelected && !accessible->state().multiSelectable && !accessible->state().extSelectable; + } + return S_OK; +} + +HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_FirstSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **pRetVal) +{ + qCDebug(lcQpaUiAutomation) << __FUNCTION__; + + if (!pRetVal) + return E_INVALIDARG; + *pRetVal = nullptr; + + QAccessibleInterface *accessible = accessibleInterface(); + if (!accessible) + return UIA_E_ELEMENTNOTAVAILABLE; + + QAccessibleInterface *firstSelectedChild = nullptr; + if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) { + firstSelectedChild = selectionInterface->selectedItem(0); + if (!firstSelectedChild) + return UIA_E_ELEMENTNOTAVAILABLE; + } else { + int i = 0; + while (!firstSelectedChild && i < accessible->childCount()) { + if (QAccessibleInterface *child = accessible->child(i)) { + if (accessible->role() == QAccessible::PageTabList) { + if (child->role() == QAccessible::PageTab && child->state().focused) + firstSelectedChild = child; + } else if (child->state().selected) { + firstSelectedChild = child; + } + } + i++; + } + } + + if (!firstSelectedChild) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (QWindowsUiaMainProvider *childProvider = QWindowsUiaMainProvider::providerForAccessible(firstSelectedChild)) + { + *pRetVal = static_cast<IRawElementProviderSimple *>(childProvider); + return S_OK; + } + + return S_FALSE; +} + +HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_LastSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **pRetVal) +{ + qCDebug(lcQpaUiAutomation) << __FUNCTION__; + + if (!pRetVal) + return E_INVALIDARG; + *pRetVal = nullptr; + + QAccessibleInterface *accessible = accessibleInterface(); + if (!accessible) + return UIA_E_ELEMENTNOTAVAILABLE; + + QAccessibleInterface *lastSelectedChild = nullptr; + if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) { + const int selectedItemCount = selectionInterface->selectedItemCount(); + if (selectedItemCount <= 0) + return UIA_E_ELEMENTNOTAVAILABLE; + lastSelectedChild = selectionInterface->selectedItem(selectedItemCount - 1); + } else { + int i = accessible->childCount() - 1; + while (!lastSelectedChild && i >= 0) { + if (QAccessibleInterface *child = accessible->child(i)) { + if (accessible->role() == QAccessible::PageTabList) { + if (child->role() == QAccessible::PageTab && child->state().focused) + lastSelectedChild = child; + } else if (child->state().selected) { + lastSelectedChild = child; + } + } + i--; + } + } + + if (!lastSelectedChild) + return UIA_E_ELEMENTNOTAVAILABLE; + + if (QWindowsUiaMainProvider *childProvider = QWindowsUiaMainProvider::providerForAccessible(lastSelectedChild)) + { + *pRetVal = static_cast<IRawElementProviderSimple *>(childProvider); + return S_OK; + } + + return S_FALSE; +} + +HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_CurrentSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **pRetVal) +{ + qCDebug(lcQpaUiAutomation) << __FUNCTION__; + return get_FirstSelectedItem(pRetVal); +} + +HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_ItemCount(__RPC__out int *pRetVal) +{ + qCDebug(lcQpaUiAutomation) << __FUNCTION__; + + if (!pRetVal) + return E_INVALIDARG; + *pRetVal = -1; + + QAccessibleInterface *accessible = accessibleInterface(); + if (!accessible) + return UIA_E_ELEMENTNOTAVAILABLE; + + + if (QAccessibleSelectionInterface *selectionInterface = accessible->selectionInterface()) + *pRetVal = selectionInterface->selectedItemCount(); + else { + int selectedCount = 0; + for (int i = 0; i < accessible->childCount(); i++) { + if (QAccessibleInterface *child = accessible->child(i)) { + if (accessible->role() == QAccessible::PageTabList) { + if (child->role() == QAccessible::PageTab && child->state().focused) + selectedCount++; + } else if (child->state().selected) { + selectedCount++; + } } } + *pRetVal = selectedCount; } - *pRetVal = anySelected && !accessible->state().multiSelectable && !accessible->state().extSelectable; return S_OK; } diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.h index 9a187c9413..7a899e4261 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.h @@ -11,9 +11,22 @@ QT_BEGIN_NAMESPACE +namespace QtPrivate { + +template <> +struct QComObjectTraits<ISelectionProvider2> +{ + static constexpr bool isGuidOf(REFIID riid) noexcept + { + return QComObjectTraits<ISelectionProvider2, ISelectionProvider>::isGuidOf(riid); + } +}; + +} // namespace QtPrivate + // Implements the Selection control pattern provider. Used for Lists. class QWindowsUiaSelectionProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<ISelectionProvider> + public QComObject<ISelectionProvider2> { Q_DISABLE_COPY_MOVE(QWindowsUiaSelectionProvider) public: @@ -24,6 +37,12 @@ public: HRESULT STDMETHODCALLTYPE GetSelection(SAFEARRAY **pRetVal) override; HRESULT STDMETHODCALLTYPE get_CanSelectMultiple(BOOL *pRetVal) override; HRESULT STDMETHODCALLTYPE get_IsSelectionRequired(BOOL *pRetVal) override; + + // ISelectionProvider2 + HRESULT STDMETHODCALLTYPE get_FirstSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **pRetVal) override; + HRESULT STDMETHODCALLTYPE get_LastSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **pRetVal) override; + HRESULT STDMETHODCALLTYPE get_CurrentSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **pRetVal) override; + HRESULT STDMETHODCALLTYPE get_ItemCount(__RPC__out int *pRetVal) override; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatableitemprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiatableitemprovider.h index 8aed180671..3bb0e1027e 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatableitemprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatableitemprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Table Item control pattern provider. Used by items within a table/tree. class QWindowsUiaTableItemProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<ITableItemProvider> + public QComObject<ITableItemProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaTableItemProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatableprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiatableprovider.h index 6454bb9441..8beb11decf 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatableprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatableprovider.h @@ -12,8 +12,7 @@ QT_BEGIN_NAMESPACE // Implements the Table control pattern provider. Used by tables/trees. -class QWindowsUiaTableProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<ITableProvider> +class QWindowsUiaTableProvider : public QWindowsUiaBaseProvider, public QComObject<ITableProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaTableProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.cpp index e3b7c69b60..172836232e 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.cpp @@ -26,20 +26,6 @@ QWindowsUiaTextProvider::~QWindowsUiaTextProvider() { } -HRESULT STDMETHODCALLTYPE QWindowsUiaTextProvider::QueryInterface(REFIID iid, LPVOID *iface) -{ - qCDebug(lcQpaUiAutomation) << __FUNCTION__ << this; - - if (!iface) - return E_INVALIDARG; - *iface = nullptr; - - const bool result = qWindowsComQueryUnknownInterfaceMulti<ITextProvider>(this, iid, iface) - || qWindowsComQueryInterface<ITextProvider>(this, iid, iface) - || qWindowsComQueryInterface<ITextProvider2>(this, iid, iface); - return result ? S_OK : E_NOINTERFACE; -} - // Returns an array of providers for the selected text ranges. HRESULT STDMETHODCALLTYPE QWindowsUiaTextProvider::GetSelection(SAFEARRAY **pRetVal) { @@ -146,9 +132,10 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaTextProvider::RangeFromPoint(UiaPoint point nativeUiaPointToPoint(point, window, &pt); int offset = textInterface->offsetAtPoint(pt); - if ((offset >= 0) && (offset < textInterface->characterCount())) { - *pRetVal = new QWindowsUiaTextRangeProvider(id(), offset, offset); - } + if (offset < 0 || offset >= textInterface->characterCount()) + return UIA_E_ELEMENTNOTAVAILABLE; + + *pRetVal = new QWindowsUiaTextRangeProvider(id(), offset, offset); return S_OK; } diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.h index 5e6d430f1a..8f886510b4 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextprovider.h @@ -12,18 +12,27 @@ QT_BEGIN_NAMESPACE +namespace QtPrivate { + +template <> +struct QComObjectTraits<ITextProvider2> +{ + static constexpr bool isGuidOf(REFIID riid) noexcept + { + return QComObjectTraits<ITextProvider2, ITextProvider>::isGuidOf(riid); + } +}; + +} // namespace QtPrivate + // Implements the Text control pattern provider. Used for text controls. -class QWindowsUiaTextProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<ITextProvider2> +class QWindowsUiaTextProvider : public QWindowsUiaBaseProvider, public QComObject<ITextProvider2> { Q_DISABLE_COPY_MOVE(QWindowsUiaTextProvider) public: explicit QWindowsUiaTextProvider(QAccessible::Id id); ~QWindowsUiaTextProvider(); - // IUnknown overrides - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id, LPVOID *iface) override; - // ITextProvider HRESULT STDMETHODCALLTYPE GetSelection(SAFEARRAY **pRetVal) override; HRESULT STDMETHODCALLTYPE GetVisibleRanges(SAFEARRAY **pRetVal) override; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.cpp index 4d02036196..a62a33cfe2 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.cpp @@ -167,6 +167,15 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaTextRangeProvider::GetAttributeValue(TEXTAT else setVariantI4(CaretPosition_Unknown, pRetVal); break; + case UIA_StrikethroughStyleAttributeId: + { + const QString value = valueForIA2Attribute(textInterface, QStringLiteral("text-line-through-type")); + if (value.isEmpty()) + break; + const TextDecorationLineStyle uiaLineStyle = uiaLineStyleForIA2LineStyle(value); + setVariantI4(uiaLineStyle, pRetVal); + break; + } default: break; } @@ -307,14 +316,14 @@ HRESULT QWindowsUiaTextRangeProvider::Move(TextUnit unit, int count, int *pRetVa int len = textInterface->characterCount(); - if (len < 1) + if (len < 1 || count == 0) // MSDN: "Zero has no effect." return S_OK; if (unit == TextUnit_Character) { // Moves the start point, ensuring it lies within the bounds. - int start = qBound(0, m_startOffset + count, len - 1); + int start = qBound(0, m_startOffset + count, len); // If range was initially empty, leaves it as is; otherwise, normalizes it to one char. - m_endOffset = (m_endOffset > m_startOffset) ? start + 1 : start; + m_endOffset = (m_endOffset > m_startOffset) ? qMin(start + 1, len) : start; *pRetVal = start - m_startOffset; // Returns the actually moved distance. m_startOffset = start; } else { @@ -385,7 +394,7 @@ HRESULT QWindowsUiaTextRangeProvider::MoveEndpointByUnit(TextPatternRangeEndpoin if (unit == TextUnit_Character) { if (endpoint == TextPatternRangeEndpoint_Start) { - int boundedValue = qBound(0, m_startOffset + count, len - 1); + int boundedValue = qBound(0, m_startOffset + count, len); *pRetVal = boundedValue - m_startOffset; m_startOffset = boundedValue; m_endOffset = qBound(m_startOffset, m_endOffset, len); @@ -517,6 +526,42 @@ HRESULT QWindowsUiaTextRangeProvider::unselect() return S_OK; } +// helper method to retrieve the value of the given IAccessible2 text attribute, +// or an empty string if not set +QString QWindowsUiaTextRangeProvider::valueForIA2Attribute(QAccessibleTextInterface *textInterface, + const QString &key) +{ + Q_ASSERT(textInterface); + + int startOffset; + int endOffset; + const QString attributes = textInterface->attributes(m_startOffset, &startOffset, &endOffset); + // don't report if attributes don't apply for the whole range + if (startOffset > m_startOffset || endOffset < m_endOffset) + return {}; + + for (auto attr : QStringTokenizer{attributes, u';'}) + { + const QList<QStringView> items = attr.split(u':', Qt::SkipEmptyParts, Qt::CaseSensitive); + if (items.count() == 2 && items[0] == key) + return items[1].toString(); + } + + return {}; +} + +TextDecorationLineStyle QWindowsUiaTextRangeProvider::uiaLineStyleForIA2LineStyle(const QString &ia2LineStyle) +{ + if (ia2LineStyle == QStringLiteral("none")) + return TextDecorationLineStyle_None; + if (ia2LineStyle == QStringLiteral("single")) + return TextDecorationLineStyle_Single; + if (ia2LineStyle == QStringLiteral("double")) + return TextDecorationLineStyle_Double; + + return TextDecorationLineStyle_Other; +} + QT_END_NAMESPACE #endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.h index f7d28a34d2..a37429a603 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatextrangeprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Text Range control pattern provider. Used for text controls. class QWindowsUiaTextRangeProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<ITextRangeProvider> + public QComObject<ITextRangeProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaTextRangeProvider) public: @@ -42,6 +42,8 @@ public: private: HRESULT unselect(); + QString valueForIA2Attribute(QAccessibleTextInterface *textInterface, const QString &key); + TextDecorationLineStyle uiaLineStyleForIA2LineStyle(const QString &ia2LineStyle); int m_startOffset; int m_endOffset; }; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiatoggleprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiatoggleprovider.h index 742e26f6d6..17150dfbe0 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiatoggleprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiatoggleprovider.h @@ -12,8 +12,7 @@ QT_BEGIN_NAMESPACE // Implements the Toggle control pattern provider. Used for checkboxes. -class QWindowsUiaToggleProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IToggleProvider> +class QWindowsUiaToggleProvider : public QWindowsUiaBaseProvider, public QComObject<IToggleProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaToggleProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp index 9287e89084..78ab3e890e 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.cpp @@ -176,6 +176,9 @@ long roleToControlTypeId(QAccessible::Role role) {QAccessible::PageTabList, UIA_TabControlTypeId}, {QAccessible::Clock, UIA_CustomControlTypeId}, {QAccessible::Splitter, UIA_CustomControlTypeId}, + {QAccessible::Paragraph, UIA_TextControlTypeId}, + {QAccessible::WebDocument, UIA_DocumentControlTypeId}, + {QAccessible::Heading, UIA_TextControlTypeId}, }; return mapping.value(role, UIA_CustomControlTypeId); diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.h index 8fe8b1c6d7..bf90211cec 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautils.h @@ -12,7 +12,7 @@ #include <QtGui/qaccessible.h> #include <QtGui/qwindow.h> #include <QtCore/qrect.h> -#include <QtGui/private/qwindowsuiawrapper_p.h> +#include "qwindowsuiautomation.h" QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.cpp new file mode 100644 index 0000000000..6954a881d0 --- /dev/null +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/qtguiglobal.h> +#if QT_CONFIG(accessibility) + +#include "qwindowsuiautomation.h" + +#if defined(__MINGW32__) || defined(__MINGW64__) + +template<typename T, typename... TArg> +struct winapi_func +{ + using func_t = T(WINAPI*)(TArg...); + const func_t func; + const T error_value; +#ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + winapi_func(const char *lib_name, const char *func_name, func_t func_proto, + T error_value = T(__HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND))) : + func(reinterpret_cast<func_t>(GetProcAddress(LoadLibraryA(lib_name), func_name))), + error_value(error_value) + { + std::ignore = func_proto; + } +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + T invoke(TArg... arg) + { + if (!func) + return error_value; + return func(arg...); + } +}; + +#define FN(fn) #fn,fn + +BOOL WINAPI UiaClientsAreListening() +{ + static auto func = winapi_func("uiautomationcore", FN(UiaClientsAreListening), BOOL(false)); + return func.invoke(); +} + +LRESULT WINAPI UiaReturnRawElementProvider( + HWND hwnd, WPARAM wParam, LPARAM lParam, IRawElementProviderSimple *el) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaReturnRawElementProvider)); + return func.invoke(hwnd, wParam, lParam, el); +} + +HRESULT WINAPI UiaHostProviderFromHwnd(HWND hwnd, IRawElementProviderSimple **ppProvider) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaHostProviderFromHwnd)); + return func.invoke(hwnd, ppProvider); +} + +HRESULT WINAPI UiaRaiseAutomationPropertyChangedEvent( + IRawElementProviderSimple *pProvider, PROPERTYID id, VARIANT oldValue, VARIANT newValue) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaRaiseAutomationPropertyChangedEvent)); + return func.invoke(pProvider, id, oldValue, newValue); +} + +HRESULT WINAPI UiaRaiseAutomationEvent(IRawElementProviderSimple *pProvider, EVENTID id) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaRaiseAutomationEvent)); + return func.invoke(pProvider, id); +} + +HRESULT WINAPI UiaRaiseNotificationEvent( + IRawElementProviderSimple *pProvider, NotificationKind notificationKind, + NotificationProcessing notificationProcessing, BSTR displayString, BSTR activityId) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaRaiseNotificationEvent)); + return func.invoke(pProvider, notificationKind, notificationProcessing, displayString, activityId); +} + +#endif // defined(__MINGW32__) || defined(__MINGW64__) + +#endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h new file mode 100644 index 0000000000..4eb37bafa0 --- /dev/null +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h @@ -0,0 +1,81 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSUIAUTOMATION_H +#define QWINDOWSUIAUTOMATION_H + +#include <QtGui/qtguiglobal.h> +#if QT_CONFIG(accessibility) + +#include <uiautomation.h> + +#if defined(__MINGW32__) || defined(__MINGW64__) + +#define UIA_SelectionPattern2Id 10034 +#define UIA_IsReadOnlyAttributeId 40015 +#define UIA_StrikethroughStyleAttributeId 40026 +#define UIA_StyleIdAttributeId 40034 +#define UIA_CaretPositionAttributeId 40038 + +#define StyleId_Heading1 70001 +#define StyleId_Heading2 70002 +#define StyleId_Heading3 70003 +#define StyleId_Heading4 70004 +#define StyleId_Heading5 70005 +#define StyleId_Heading6 70006 +#define StyleId_Heading7 70007 +#define StyleId_Heading8 70008 +#define StyleId_Heading9 70009 + +enum CaretPosition { + CaretPosition_Unknown = 0, + CaretPosition_EndOfLine = 1, + CaretPosition_BeginningOfLine = 2 +}; + +enum TextDecorationLineStyle { + TextDecorationLineStyle_None = 0, + TextDecorationLineStyle_Single = 1, + TextDecorationLineStyle_WordsOnly = 2, + TextDecorationLineStyle_Double = 3, + TextDecorationLineStyle_Dot = 4, + TextDecorationLineStyle_Dash = 5, + TextDecorationLineStyle_DashDot = 6, + TextDecorationLineStyle_DashDotDot = 7, + TextDecorationLineStyle_Wavy = 8, + TextDecorationLineStyle_ThickSingle = 9, + TextDecorationLineStyle_DoubleWavy = 11, + TextDecorationLineStyle_ThickWavy = 12, + TextDecorationLineStyle_LongDash = 13, + TextDecorationLineStyle_ThickDash = 14, + TextDecorationLineStyle_ThickDashDot = 15, + TextDecorationLineStyle_ThickDashDotDot = 16, + TextDecorationLineStyle_ThickDot = 17, + TextDecorationLineStyle_ThickLongDash = 18, + TextDecorationLineStyle_Other = -1 +}; + +BOOL WINAPI UiaClientsAreListening(); + +#ifndef __ISelectionProvider2_INTERFACE_DEFINED__ +#define __ISelectionProvider2_INTERFACE_DEFINED__ +DEFINE_GUID(IID_ISelectionProvider2, 0x14f68475, 0xee1c, 0x44f6, 0xa8, 0x69, 0xd2, 0x39, 0x38, 0x1f, 0x0f, 0xe7); +MIDL_INTERFACE("14f68475-ee1c-44f6-a869-d239381f0fe7") +ISelectionProvider2 : public ISelectionProvider +{ +public: + virtual HRESULT STDMETHODCALLTYPE get_FirstSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **retVal) = 0; + virtual HRESULT STDMETHODCALLTYPE get_LastSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **retVal) = 0; + virtual HRESULT STDMETHODCALLTYPE get_CurrentSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **retVal) = 0; + virtual HRESULT STDMETHODCALLTYPE get_ItemCount(__RPC__out int *retVal) = 0; +}; +#ifdef __CRT_UUID_DECL +__CRT_UUID_DECL(ISelectionProvider2, 0x14f68475, 0xee1c, 0x44f6, 0xa8, 0x69, 0xd2, 0x39, 0x38, 0x1f, 0x0f, 0xe7) +#endif +#endif // __ISelectionProvider2_INTERFACE_DEFINED__ + +#endif // defined(__MINGW32__) || defined(__MINGW64__) + +#endif // QT_CONFIG(accessibility) + +#endif // QWINDOWSUIAUTOMATION_H diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h index 2f52019d33..8c0a6b8ee7 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h @@ -13,8 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Value control pattern provider. // Supported for all controls that can return text(QAccessible::Value). -class QWindowsUiaValueProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IValueProvider> +class QWindowsUiaValueProvider : public QWindowsUiaBaseProvider, public QComObject<IValueProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaValueProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h index e277b3f5c5..89a1655232 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h @@ -11,8 +11,7 @@ QT_BEGIN_NAMESPACE -class QWindowsUiaWindowProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IWindowProvider> +class QWindowsUiaWindowProvider : public QWindowsUiaBaseProvider, public QComObject<IWindowProvider> { Q_DISABLE_COPY(QWindowsUiaWindowProvider) public: diff --git a/src/plugins/platforms/xcb/CMakeLists.txt b/src/plugins/platforms/xcb/CMakeLists.txt index c73b2de73f..96758e7181 100644 --- a/src/plugins/platforms/xcb/CMakeLists.txt +++ b/src/plugins/platforms/xcb/CMakeLists.txt @@ -1,9 +1,15 @@ -# Generated from xcb.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## XcbQpaPrivate Module: ##################################################################### +if(GCC) + # Work around GCC ABI issues + add_compile_options(-Wno-psabi) +endif() + qt_internal_add_module(XcbQpaPrivate CONFIG_MODULE_NAME xcb_qpa_lib INTERNAL_MODULE @@ -44,6 +50,7 @@ qt_internal_add_module(XcbQpaPrivate PkgConfig::XKB_COMMON_X11 Qt::CorePrivate Qt::GuiPrivate + XCB::CURSOR XCB::ICCCM XCB::IMAGE XCB::KEYSYMS @@ -55,23 +62,16 @@ qt_internal_add_module(XcbQpaPrivate XCB::SYNC XCB::XCB XCB::XFIXES - # XCB::XINPUT # special case remove handled below XCB::XKB XKB::XKB + NO_UNITY_BUILD # X11 define clashes ) -# special case begin qt_disable_apple_app_extension_api_only(XcbQpaPrivate) -# special case end ## Scopes: ##################################################################### -qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_opengl - PUBLIC_LIBRARIES - Qt::OpenGLPrivate -) - qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_glib LIBRARIES GLIB2::GLIB2 @@ -83,21 +83,24 @@ qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_draganddrop ) qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_xcb_xlib + SOURCES + qt_xlib_wrapper.c qt_xlib_wrapper.h PUBLIC_LIBRARIES X11::XCB - # special case begin - # 'QMAKE_USE += xcb_xlib' in qmake implies also += xlib (aka X11) - # due to "use": "xcb xlib" in src/gui/configure.json. - # That's not yet handled by the conversion scripts unfortunately. X11::X11 - # special case end +) + +qt_internal_extend_target(XcbQpaPrivate CONDITION NOT QT_FEATURE_xcb_xlib + SOURCES + qxcbcursorfont.h ) qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_xcb_sm SOURCES qxcbsessionmanager.cpp qxcbsessionmanager.h PUBLIC_LIBRARIES - ${X11_SM_LIB} ${X11_ICE_LIB} + X11::SM + X11::ICE ) qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_vulkan @@ -140,7 +143,6 @@ qt_internal_extend_target(XcbQpaPrivate CONDITION QT_FEATURE_fontconfig AND QT_F WrapFreetype::WrapFreetype ) -# special case begin if(QT_FEATURE_system_xcb_xinput) qt_internal_extend_target(XcbQpaPrivate LIBRARIES XCB::XINPUT) else() @@ -154,7 +156,6 @@ else() "${PROJECT_SOURCE_DIR}/src/3rdparty/xcb/include" ) endif() -# special case end ##################################################################### ## QXcbIntegrationPlugin Plugin: @@ -163,7 +164,7 @@ endif() qt_internal_add_plugin(QXcbIntegrationPlugin OUTPUT_NAME qxcb PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES xcb # special case + DEFAULT_IF "xcb" IN_LIST QT_QPA_PLATFORMS SOURCES qxcbmain.cpp DEFINES @@ -174,16 +175,7 @@ qt_internal_add_plugin(QXcbIntegrationPlugin Qt::XcbQpaPrivate ) -#### Keys ignored in scope 18:.:.:xcb-plugin.pro:<TRUE>: -# OTHER_FILES = "xcb.json" "README" - -## Scopes: -##################################################################### - -#### Keys ignored in scope 20:.:.:xcb-plugin.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" add_subdirectory(gl_integrations) if(OFF) - add_subdirectory(xcb-static) # special case TODO: xcb-static sub folder + add_subdirectory(xcb-static) endif() - diff --git a/src/plugins/platforms/xcb/gl_integrations/CMakeLists.txt b/src/plugins/platforms/xcb/gl_integrations/CMakeLists.txt index 210a924550..957beb9ed4 100644 --- a/src/plugins/platforms/xcb/gl_integrations/CMakeLists.txt +++ b/src/plugins/platforms/xcb/gl_integrations/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from gl_integrations.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_xcb_egl_plugin) add_subdirectory(xcb_egl) diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/CMakeLists.txt b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/CMakeLists.txt index 4a62c8ad46..12938c159a 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/CMakeLists.txt +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/CMakeLists.txt @@ -1,10 +1,11 @@ -# Generated from xcb_egl.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QXcbEglIntegrationPlugin Plugin: ##################################################################### -qt_find_package(EGL) # special case +qt_find_package(EGL) qt_internal_add_plugin(QXcbEglIntegrationPlugin OUTPUT_NAME qxcb-egl-integration @@ -26,5 +27,6 @@ qt_internal_add_plugin(QXcbEglIntegrationPlugin Qt::Gui Qt::GuiPrivate Qt::XcbQpaPrivate - EGL::EGL # special case + EGL::EGL + NO_UNITY_BUILD # X11 define clashes ) diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglinclude.h b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglinclude.h index 17428f778f..d12501bd90 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglinclude.h +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglinclude.h @@ -7,7 +7,6 @@ #include <QtGui/QPalette> #include <QtCore/QTextStream> #include <QtGui/private/qmath_p.h> -#include <QtGui/private/qcssparser_p.h> #include <QtGui/private/qtextengine_p.h> #include <QtGui/private/qt_egl_p.h> diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.cpp b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.cpp index 08c34cc495..133b992cd9 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.cpp +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.cpp @@ -58,6 +58,7 @@ std::optional<VisualInfo> getVisualInfo(xcb_screen_t *screen, QXcbEglIntegration::QXcbEglIntegration() : m_connection(nullptr) , m_egl_display(EGL_NO_DISPLAY) + , m_using_platform_display(false) { qCDebug(lcQpaGl) << "Xcb EGL gl-integration created"; } @@ -74,23 +75,38 @@ bool QXcbEglIntegration::initialize(QXcbConnection *connection) const char *extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); +#if QT_CONFIG(xcb_xlib) if (extensions && strstr(extensions, "EGL_EXT_platform_x11")) { QEGLStreamConvenience streamFuncs; m_egl_display = streamFuncs.get_platform_display(EGL_PLATFORM_X11_KHR, - xlib_display(), + m_connection->xlib_display(), nullptr); + m_using_platform_display = true; } +#if QT_CONFIG(egl_x11) if (!m_egl_display) - m_egl_display = eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(xlib_display())); + m_egl_display = eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_connection->xlib_display())); +#endif +#else + if (extensions && (strstr(extensions, "EGL_EXT_platform_xcb") || strstr(extensions, "EGL_MESA_platform_xcb"))) { + QEGLStreamConvenience streamFuncs; + m_egl_display = streamFuncs.get_platform_display(EGL_PLATFORM_XCB_KHR, + reinterpret_cast<void *>(connection->xcb_connection()), + nullptr); + m_using_platform_display = true; + } +#endif EGLint major, minor; bool success = eglInitialize(m_egl_display, &major, &minor); +#if QT_CONFIG(egl_x11) if (!success) { m_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); qCDebug(lcQpaGl) << "Xcb EGL gl-integration retrying with display" << m_egl_display; success = eglInitialize(m_egl_display, &major, &minor); } +#endif m_native_interface_handler.reset(new QXcbEglNativeInterfaceHandler(connection->nativeInterface())); @@ -127,15 +143,6 @@ QPlatformOffscreenSurface *QXcbEglIntegration::createPlatformOffscreenSurface(QO return new QEGLPbuffer(eglDisplay(), screen->surfaceFormatFor(surface->requestedFormat()), surface); } -void *QXcbEglIntegration::xlib_display() const -{ -#if QT_CONFIG(xcb_xlib) - return m_connection->xlib_display(); -#else - return EGL_DEFAULT_DISPLAY; -#endif -} - xcb_visualid_t QXcbEglIntegration::getCompatibleVisualId(xcb_screen_t *screen, EGLConfig config) const { xcb_visualid_t visualId = 0; diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.h b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.h index 194b6d6e77..7caf4304bc 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.h +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.h @@ -38,12 +38,14 @@ public: bool supportsThreadedOpenGL() const override { return true; } EGLDisplay eglDisplay() const { return m_egl_display; } - void *xlib_display() const; + + bool usingPlatformDisplay() const { return m_using_platform_display; } xcb_visualid_t getCompatibleVisualId(xcb_screen_t *screen, EGLConfig config) const; private: QXcbConnection *m_connection; EGLDisplay m_egl_display; + bool m_using_platform_display; QScopedPointer<QXcbEglNativeInterfaceHandler> m_native_interface_handler; }; diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglwindow.cpp b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglwindow.cpp index 4d87b08db8..bf2ceb96f4 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglwindow.cpp +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglwindow.cpp @@ -7,6 +7,10 @@ #include <QtGui/private/qeglconvenience_p.h> +#ifndef EGL_EXT_platform_base +typedef EGLSurface (EGLAPIENTRYP PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC) (EGLDisplay dpy, EGLConfig config, void *native_window, const EGLint *attrib_list); +#endif + QT_BEGIN_NAMESPACE QXcbEglWindow::QXcbEglWindow(QWindow *window, QXcbEglIntegration *glIntegration) @@ -42,7 +46,20 @@ void QXcbEglWindow::create() { QXcbWindow::create(); + // this is always true without egl_x11 + if (m_glIntegration->usingPlatformDisplay()) { + auto createPlatformWindowSurface = reinterpret_cast<PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC>( + eglGetProcAddress("eglCreatePlatformWindowSurfaceEXT")); + m_surface = createPlatformWindowSurface(m_glIntegration->eglDisplay(), + m_config, + reinterpret_cast<void *>(&m_window), + nullptr); + return; + } + +#if QT_CONFIG(egl_x11) m_surface = eglCreateWindowSurface(m_glIntegration->eglDisplay(), m_config, m_window, nullptr); +#endif } QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/CMakeLists.txt b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/CMakeLists.txt index ae81eba545..f9f78ad1eb 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/CMakeLists.txt +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from xcb_glx.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QXcbGlxIntegrationPlugin Plugin: @@ -24,6 +25,7 @@ qt_internal_add_plugin(QXcbGlxIntegrationPlugin Qt::Gui Qt::GuiPrivate Qt::XcbQpaPrivate + NO_UNITY_BUILD # X11 define clashes ) ## Scopes: diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp index 5992ebd74e..04eac027cd 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp @@ -242,9 +242,9 @@ QGLXContext::QGLXContext(Display *display, QXcbScreen *screen, const QSurfaceFor // Robustness must match that of the shared context. if (share && share->format().testOption(QSurfaceFormat::ResetNotification)) m_format.setOption(QSurfaceFormat::ResetNotification); - Q_ASSERT(glVersions.count() > 0); + Q_ASSERT(glVersions.size() > 0); - for (int i = 0; !m_context && i < glVersions.count(); i++) { + for (int i = 0; !m_context && i < glVersions.size(); i++) { const int version = glVersions[i]; if (version > requestedVersion) continue; @@ -395,14 +395,19 @@ QGLXContext::QGLXContext(Display *display, GLXContext context, void *visualInfo, int numConfigs = 0; static const int attribs[] = { GLX_FBCONFIG_ID, configId, None }; configs = glXChooseFBConfig(m_display, screenNumber, attribs, &numConfigs); - if (!configs || numConfigs < 1) { + if (!configs) { + qWarning("QGLXContext: Failed to find config(invalid arguments for glXChooseFBConfig)"); + return; + } else if (numConfigs < 1) { qWarning("QGLXContext: Failed to find config"); + XFree(configs); return; } if (configs && numConfigs > 1) // this is suspicious so warn but let it continue qWarning("QGLXContext: Multiple configs for FBConfig ID %d", configId); m_config = configs[0]; + XFree(configs); } Q_ASSERT(vinfo || m_config); diff --git a/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp index 96a19552a5..d523eecc5a 100644 --- a/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp +++ b/src/plugins/platforms/xcb/nativepainting/qbackingstore_x11.cpp @@ -16,10 +16,6 @@ #include <X11/Xlib.h> #undef register -#ifndef None -#define None 0L -#endif - QT_BEGIN_NAMESPACE QXcbNativeBackingStore::QXcbNativeBackingStore(QWindow *window) @@ -78,11 +74,8 @@ void QXcbNativeBackingStore::flush(QWindow *window, const QRegion ®ion, const XRenderSetPictureClipRectangles(display(), dstPic, 0, 0, clipRects.constData(), clipRects.size()); - XRenderComposite(display(), PictOpSrc, srcPic, None, dstPic, - br.x() + offset.x(), br.y() + offset.y(), - 0, 0, - br.x(), br.y(), - br.width(), br.height()); + XRenderComposite(display(), PictOpSrc, srcPic, 0L /*None*/, dstPic, br.x() + offset.x(), + br.y() + offset.y(), 0, 0, br.x(), br.y(), br.width(), br.height()); XRenderFreePicture(display(), dstPic); } diff --git a/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp index f630f538aa..743ff92654 100644 --- a/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp +++ b/src/plugins/platforms/xcb/nativepainting/qpaintengine_x11.cpp @@ -2134,7 +2134,7 @@ void QX11PaintEngine::drawPixmap(const QRectF &r, const QPixmap &px, const QRect XFillRectangle(d->dpy, d->hd, d->gc, x, y, sw, sh); restore_clip = true; } else if (mono_dst && !mono_src) { - QBitmap bitmap(pixmap); + QBitmap bitmap = QBitmap::fromPixmap(pixmap); XCopyArea(d->dpy, qt_x11PixmapHandle(bitmap), d->hd, d->gc, sx, sy, sw, sh, x, y); } else { XCopyArea(d->dpy, qt_x11PixmapHandle(pixmap), d->hd, d->gc, sx, sy, sw, sh, x, y); diff --git a/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp b/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp index 186a3cf9d7..a4e820ea92 100644 --- a/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp +++ b/src/plugins/platforms/xcb/nativepainting/qpixmap_x11.cpp @@ -1314,7 +1314,7 @@ QBitmap QX11PlatformPixmap::mask() const #endif if (d == 1) { QX11PlatformPixmap *that = const_cast<QX11PlatformPixmap*>(this); - mask = QPixmap(that); + mask = QBitmap::fromPixmap(QPixmap(that)); } else { mask = mask_to_bitmap(xinfo.screen()); } diff --git a/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp b/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp index ecc21eb1f5..7b0094044b 100644 --- a/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp +++ b/src/plugins/platforms/xcb/nativepainting/qtessellator.cpp @@ -5,6 +5,7 @@ #include <QRect> #include <QList> +#include <QMap> #include <QDebug> #include <qmath.h> diff --git a/src/plugins/platforms/xcb/qt_xlib_wrapper.c b/src/plugins/platforms/xcb/qt_xlib_wrapper.c new file mode 100644 index 0000000000..f4614e5504 --- /dev/null +++ b/src/plugins/platforms/xcb/qt_xlib_wrapper.c @@ -0,0 +1,7 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qt_xlib_wrapper.h" + +#include <X11/Xlib.h> + +void qt_XFlush(Display *dpy) { XFlush(dpy); } diff --git a/src/plugins/platforms/xcb/qt_xlib_wrapper.h b/src/plugins/platforms/xcb/qt_xlib_wrapper.h new file mode 100644 index 0000000000..642dbdeadd --- /dev/null +++ b/src/plugins/platforms/xcb/qt_xlib_wrapper.h @@ -0,0 +1,17 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QT_XLIB_WRAPPER_H +#define QT_XLIB_WRAPPER_H + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct _XDisplay Display; + void qt_XFlush(Display *dpy); + +#ifdef __cplusplus +} +#endif + +#endif // QT_XLIB_WRAPPER_H diff --git a/src/plugins/platforms/xcb/qxcbatom.cpp b/src/plugins/platforms/xcb/qxcbatom.cpp index 8f984c7280..dd5596653c 100644 --- a/src/plugins/platforms/xcb/qxcbatom.cpp +++ b/src/plugins/platforms/xcb/qxcbatom.cpp @@ -54,6 +54,8 @@ static const char *xcb_atomnames = { "_QT_CLOSE_CONNECTION\0" + "_QT_GET_TIMESTAMP\0" + "_MOTIF_WM_HINTS\0" "DTWM_IS_RUNNING\0" @@ -109,11 +111,13 @@ static const char *xcb_atomnames = { "_NET_WM_WINDOW_TYPE_NORMAL\0" "_KDE_NET_WM_WINDOW_TYPE_OVERRIDE\0" + "_KDE_NET_WM_DESKTOP_FILE\0" "_KDE_NET_WM_FRAME_STRUT\0" "_NET_FRAME_EXTENTS\0" "_NET_STARTUP_INFO\0" "_NET_STARTUP_INFO_BEGIN\0" + "_NET_STARTUP_ID\0" "_NET_SUPPORTING_WM_CHECK\0" @@ -193,6 +197,7 @@ static const char *xcb_atomnames = { "_COMPIZ_DECOR_REQUEST\0" "_COMPIZ_DECOR_DELETE_PIXMAP\0" "_COMPIZ_TOOLKIT_ACTION\0" + "_GTK_APPLICATION_ID\0" "_GTK_LOAD_ICONTHEMES\0" "AT_SPI_BUS\0" "EDID\0" @@ -212,29 +217,25 @@ void QXcbAtom::initialize(xcb_connection_t *connection) } void QXcbAtom::initializeAllAtoms(xcb_connection_t *connection) { - const char *names[QXcbAtom::NAtoms]; - const char *ptr = xcb_atomnames; - + const char *name = xcb_atomnames; + size_t name_len; int i = 0; - while (*ptr) { - names[i++] = ptr; - while (*ptr) - ++ptr; - ++ptr; - } - - Q_ASSERT(i == QXcbAtom::NAtoms); - xcb_intern_atom_cookie_t cookies[QXcbAtom::NAtoms]; + while ((name_len = strlen(name)) != 0) { + cookies[i] = xcb_intern_atom(connection, false, name_len, name); + ++i; + name += name_len + 1; // jump over the \0 + } + Q_ASSERT(i == QXcbAtom::NAtoms); - for (i = 0; i < QXcbAtom::NAtoms; ++i) - cookies[i] = xcb_intern_atom(connection, false, strlen(names[i]), names[i]); for (i = 0; i < QXcbAtom::NAtoms; ++i) { xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(connection, cookies[i], nullptr); - m_allAtoms[i] = reply->atom; - free(reply); + if (reply) { + m_allAtoms[i] = reply->atom; + free(reply); + } } } diff --git a/src/plugins/platforms/xcb/qxcbatom.h b/src/plugins/platforms/xcb/qxcbatom.h index 48924dd626..bc677eaf3e 100644 --- a/src/plugins/platforms/xcb/qxcbatom.h +++ b/src/plugins/platforms/xcb/qxcbatom.h @@ -10,201 +10,206 @@ class QXcbAtom public: enum Atom { // window-manager <-> client protocols - WM_PROTOCOLS, - WM_DELETE_WINDOW, - WM_TAKE_FOCUS, - _NET_WM_PING, - _NET_WM_CONTEXT_HELP, - _NET_WM_SYNC_REQUEST, - _NET_WM_SYNC_REQUEST_COUNTER, - MANAGER, // System tray notification - _NET_SYSTEM_TRAY_OPCODE, // System tray operation + AtomWM_PROTOCOLS, + AtomWM_DELETE_WINDOW, + AtomWM_TAKE_FOCUS, + Atom_NET_WM_PING, + Atom_NET_WM_CONTEXT_HELP, + Atom_NET_WM_SYNC_REQUEST, + Atom_NET_WM_SYNC_REQUEST_COUNTER, + AtomMANAGER, // System tray notification + Atom_NET_SYSTEM_TRAY_OPCODE, // System tray operation // ICCCM window state - WM_STATE, - WM_CHANGE_STATE, - WM_CLASS, - WM_NAME, + AtomWM_STATE, + AtomWM_CHANGE_STATE, + AtomWM_CLASS, + AtomWM_NAME, // Session management - WM_CLIENT_LEADER, - WM_WINDOW_ROLE, - SM_CLIENT_ID, - WM_CLIENT_MACHINE, + AtomWM_CLIENT_LEADER, + AtomWM_WINDOW_ROLE, + AtomSM_CLIENT_ID, + AtomWM_CLIENT_MACHINE, // Clipboard - CLIPBOARD, - INCR, - TARGETS, - MULTIPLE, - TIMESTAMP, - SAVE_TARGETS, - CLIP_TEMPORARY, - _QT_SELECTION, - _QT_CLIPBOARD_SENTINEL, - _QT_SELECTION_SENTINEL, - CLIPBOARD_MANAGER, + AtomCLIPBOARD, + AtomINCR, + AtomTARGETS, + AtomMULTIPLE, + AtomTIMESTAMP, + AtomSAVE_TARGETS, + AtomCLIP_TEMPORARY, + Atom_QT_SELECTION, + Atom_QT_CLIPBOARD_SENTINEL, + Atom_QT_SELECTION_SENTINEL, + AtomCLIPBOARD_MANAGER, - RESOURCE_MANAGER, + AtomRESOURCE_MANAGER, - _XSETROOT_ID, + Atom_XSETROOT_ID, - _QT_SCROLL_DONE, - _QT_INPUT_ENCODING, + Atom_QT_SCROLL_DONE, + Atom_QT_INPUT_ENCODING, // Qt/XCB specific - _QT_CLOSE_CONNECTION, + Atom_QT_CLOSE_CONNECTION, - _MOTIF_WM_HINTS, + Atom_QT_GET_TIMESTAMP, - DTWM_IS_RUNNING, - ENLIGHTENMENT_DESKTOP, - _DT_SAVE_MODE, - _SGI_DESKS_MANAGER, + Atom_MOTIF_WM_HINTS, + + AtomDTWM_IS_RUNNING, + AtomENLIGHTENMENT_DESKTOP, + Atom_DT_SAVE_MODE, + Atom_SGI_DESKS_MANAGER, // EWMH (aka NETWM) - _NET_SUPPORTED, - _NET_VIRTUAL_ROOTS, - _NET_WORKAREA, - - _NET_MOVERESIZE_WINDOW, - _NET_WM_MOVERESIZE, - - _NET_WM_NAME, - _NET_WM_ICON_NAME, - _NET_WM_ICON, - - _NET_WM_PID, - - _NET_WM_WINDOW_OPACITY, - - _NET_WM_STATE, - _NET_WM_STATE_ABOVE, - _NET_WM_STATE_BELOW, - _NET_WM_STATE_FULLSCREEN, - _NET_WM_STATE_MAXIMIZED_HORZ, - _NET_WM_STATE_MAXIMIZED_VERT, - _NET_WM_STATE_MODAL, - _NET_WM_STATE_STAYS_ON_TOP, - _NET_WM_STATE_DEMANDS_ATTENTION, - _NET_WM_STATE_HIDDEN, - - _NET_WM_USER_TIME, - _NET_WM_USER_TIME_WINDOW, - _NET_WM_FULL_PLACEMENT, - - _NET_WM_WINDOW_TYPE, - _NET_WM_WINDOW_TYPE_DESKTOP, - _NET_WM_WINDOW_TYPE_DOCK, - _NET_WM_WINDOW_TYPE_TOOLBAR, - _NET_WM_WINDOW_TYPE_MENU, - _NET_WM_WINDOW_TYPE_UTILITY, - _NET_WM_WINDOW_TYPE_SPLASH, - _NET_WM_WINDOW_TYPE_DIALOG, - _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, - _NET_WM_WINDOW_TYPE_POPUP_MENU, - _NET_WM_WINDOW_TYPE_TOOLTIP, - _NET_WM_WINDOW_TYPE_NOTIFICATION, - _NET_WM_WINDOW_TYPE_COMBO, - _NET_WM_WINDOW_TYPE_DND, - _NET_WM_WINDOW_TYPE_NORMAL, - _KDE_NET_WM_WINDOW_TYPE_OVERRIDE, - - _KDE_NET_WM_FRAME_STRUT, - _NET_FRAME_EXTENTS, - - _NET_STARTUP_INFO, - _NET_STARTUP_INFO_BEGIN, - - _NET_SUPPORTING_WM_CHECK, - - _NET_WM_CM_S0, - - _NET_SYSTEM_TRAY_VISUAL, - - _NET_ACTIVE_WINDOW, + Atom_NET_SUPPORTED, + Atom_NET_VIRTUAL_ROOTS, + Atom_NET_WORKAREA, + + Atom_NET_MOVERESIZE_WINDOW, + Atom_NET_WM_MOVERESIZE, + + Atom_NET_WM_NAME, + Atom_NET_WM_ICON_NAME, + Atom_NET_WM_ICON, + + Atom_NET_WM_PID, + + Atom_NET_WM_WINDOW_OPACITY, + + Atom_NET_WM_STATE, + Atom_NET_WM_STATE_ABOVE, + Atom_NET_WM_STATE_BELOW, + Atom_NET_WM_STATE_FULLSCREEN, + Atom_NET_WM_STATE_MAXIMIZED_HORZ, + Atom_NET_WM_STATE_MAXIMIZED_VERT, + Atom_NET_WM_STATE_MODAL, + Atom_NET_WM_STATE_STAYS_ON_TOP, + Atom_NET_WM_STATE_DEMANDS_ATTENTION, + Atom_NET_WM_STATE_HIDDEN, + + Atom_NET_WM_USER_TIME, + Atom_NET_WM_USER_TIME_WINDOW, + Atom_NET_WM_FULL_PLACEMENT, + + Atom_NET_WM_WINDOW_TYPE, + Atom_NET_WM_WINDOW_TYPE_DESKTOP, + Atom_NET_WM_WINDOW_TYPE_DOCK, + Atom_NET_WM_WINDOW_TYPE_TOOLBAR, + Atom_NET_WM_WINDOW_TYPE_MENU, + Atom_NET_WM_WINDOW_TYPE_UTILITY, + Atom_NET_WM_WINDOW_TYPE_SPLASH, + Atom_NET_WM_WINDOW_TYPE_DIALOG, + Atom_NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + Atom_NET_WM_WINDOW_TYPE_POPUP_MENU, + Atom_NET_WM_WINDOW_TYPE_TOOLTIP, + Atom_NET_WM_WINDOW_TYPE_NOTIFICATION, + Atom_NET_WM_WINDOW_TYPE_COMBO, + Atom_NET_WM_WINDOW_TYPE_DND, + Atom_NET_WM_WINDOW_TYPE_NORMAL, + Atom_KDE_NET_WM_WINDOW_TYPE_OVERRIDE, + + Atom_KDE_NET_WM_DESKTOP_FILE, + Atom_KDE_NET_WM_FRAME_STRUT, + Atom_NET_FRAME_EXTENTS, + + Atom_NET_STARTUP_INFO, + Atom_NET_STARTUP_INFO_BEGIN, + Atom_NET_STARTUP_ID, + + Atom_NET_SUPPORTING_WM_CHECK, + + Atom_NET_WM_CM_S0, + + Atom_NET_SYSTEM_TRAY_VISUAL, + + Atom_NET_ACTIVE_WINDOW, // Property formats - TEXT, - UTF8_STRING, - CARDINAL, + AtomTEXT, + AtomUTF8_STRING, + AtomCARDINAL, // Xdnd - XdndEnter, - XdndPosition, - XdndStatus, - XdndLeave, - XdndDrop, - XdndFinished, - XdndTypelist, - XdndActionList, - - XdndSelection, - - XdndAware, - XdndProxy, - - XdndActionCopy, - XdndActionLink, - XdndActionMove, - XdndActionAsk, - XdndActionPrivate, + AtomXdndEnter, + AtomXdndPosition, + AtomXdndStatus, + AtomXdndLeave, + AtomXdndDrop, + AtomXdndFinished, + AtomXdndTypelist, + AtomXdndActionList, + + AtomXdndSelection, + + AtomXdndAware, + AtomXdndProxy, + + AtomXdndActionCopy, + AtomXdndActionLink, + AtomXdndActionMove, + AtomXdndActionAsk, + AtomXdndActionPrivate, // Xkb - _XKB_RULES_NAMES, + Atom_XKB_RULES_NAMES, // XEMBED - _XEMBED, - _XEMBED_INFO, + Atom_XEMBED, + Atom_XEMBED_INFO, // XInput2 - ButtonLeft, - ButtonMiddle, - ButtonRight, - ButtonWheelUp, - ButtonWheelDown, - ButtonHorizWheelLeft, - ButtonHorizWheelRight, - AbsMTPositionX, - AbsMTPositionY, - AbsMTTouchMajor, - AbsMTTouchMinor, - AbsMTOrientation, - AbsMTPressure, - AbsMTTrackingID, - MaxContacts, - RelX, - RelY, + AtomButtonLeft, + AtomButtonMiddle, + AtomButtonRight, + AtomButtonWheelUp, + AtomButtonWheelDown, + AtomButtonHorizWheelLeft, + AtomButtonHorizWheelRight, + AtomAbsMTPositionX, + AtomAbsMTPositionY, + AtomAbsMTTouchMajor, + AtomAbsMTTouchMinor, + AtomAbsMTOrientation, + AtomAbsMTPressure, + AtomAbsMTTrackingID, + AtomMaxContacts, + AtomRelX, + AtomRelY, // XInput2 tablet - AbsX, - AbsY, - AbsPressure, - AbsTiltX, - AbsTiltY, - AbsWheel, - AbsDistance, - WacomSerialIDs, - INTEGER, - RelHorizWheel, - RelVertWheel, - RelHorizScroll, - RelVertScroll, - - _XSETTINGS_SETTINGS, - - _COMPIZ_DECOR_PENDING, - _COMPIZ_DECOR_REQUEST, - _COMPIZ_DECOR_DELETE_PIXMAP, - _COMPIZ_TOOLKIT_ACTION, - _GTK_LOAD_ICONTHEMES, - - AT_SPI_BUS, - - EDID, - EDID_DATA, - XFree86_DDC_EDID1_RAWDATA, - - _ICC_PROFILE, + AtomAbsX, + AtomAbsY, + AtomAbsPressure, + AtomAbsTiltX, + AtomAbsTiltY, + AtomAbsWheel, + AtomAbsDistance, + AtomWacomSerialIDs, + AtomINTEGER, + AtomRelHorizWheel, + AtomRelVertWheel, + AtomRelHorizScroll, + AtomRelVertScroll, + + Atom_XSETTINGS_SETTINGS, + + Atom_COMPIZ_DECOR_PENDING, + Atom_COMPIZ_DECOR_REQUEST, + Atom_COMPIZ_DECOR_DELETE_PIXMAP, + Atom_COMPIZ_TOOLKIT_ACTION, + Atom_GTK_APPLICATION_ID, + Atom_GTK_LOAD_ICONTHEMES, + + AtomAT_SPI_BUS, + + AtomEDID, + AtomEDID_DATA, + AtomXFree86_DDC_EDID1_RAWDATA, + + Atom_ICC_PROFILE, NAtoms }; diff --git a/src/plugins/platforms/xcb/qxcbbackingstore.cpp b/src/plugins/platforms/xcb/qxcbbackingstore.cpp index f3905dff81..8353fac6a9 100644 --- a/src/plugins/platforms/xcb/qxcbbackingstore.cpp +++ b/src/plugins/platforms/xcb/qxcbbackingstore.cpp @@ -173,7 +173,7 @@ void QXcbBackingStoreImage::init(const QSize &size, uint depth, QImage::Format f m_qimage_format = format; m_hasAlpha = QImage::toPixelFormat(m_qimage_format).alphaUsage() == QPixelFormat::UsesAlpha; if (!m_hasAlpha) - m_qimage_format = qt_maybeAlphaVersionWithSameDepth(m_qimage_format); + m_qimage_format = qt_maybeDataCompatibleAlphaVersion(m_qimage_format); memset(&m_shm_info, 0, sizeof m_shm_info); @@ -877,8 +877,10 @@ QPlatformBackingStore::FlushResult QXcbBackingStore::rhiFlush(QWindow *window, m_image->flushScrolledRegion(true); - QPlatformBackingStore::rhiFlush(window, sourceDevicePixelRatio, region, offset, textures, translucentBackground); - + auto result = QPlatformBackingStore::rhiFlush(window, sourceDevicePixelRatio, region, offset, + textures, translucentBackground); + if (result != FlushSuccess) + return result; QXcbWindow *platformWindow = static_cast<QXcbWindow *>(window->handle()); if (platformWindow->needsSync()) { platformWindow->updateSyncRequestCounter(); diff --git a/src/plugins/platforms/xcb/qxcbclipboard.cpp b/src/plugins/platforms/xcb/qxcbclipboard.cpp index 6c15d7c142..40e2f47354 100644 --- a/src/plugins/platforms/xcb/qxcbclipboard.cpp +++ b/src/plugins/platforms/xcb/qxcbclipboard.cpp @@ -31,7 +31,7 @@ public: break; case QClipboard::Clipboard: - modeAtom = m_clipboard->atom(QXcbAtom::CLIPBOARD); + modeAtom = m_clipboard->atom(QXcbAtom::AtomCLIPBOARD); break; default: @@ -56,12 +56,12 @@ protected: if (isEmpty()) return QStringList(); - if (!formatList.count()) { + if (!formatList.size()) { QXcbClipboardMime *that = const_cast<QXcbClipboardMime *>(this); // get the list of targets from the current clipboard owner - we do this // once so that multiple calls to this function don't require multiple // server round trips... - that->format_atoms = m_clipboard->getDataInFormat(modeAtom, m_clipboard->atom(QXcbAtom::TARGETS)); + that->format_atoms = m_clipboard->getDataInFormat(modeAtom, m_clipboard->atom(QXcbAtom::AtomTARGETS)); if (format_atoms.size() > 0) { const xcb_atom_t *targets = (const xcb_atom_t *) format_atoms.data(); @@ -126,7 +126,7 @@ QXcbClipboardTransaction::QXcbClipboardTransaction(QXcbClipboard *clipboard, xcb xcb_change_window_attributes(m_clipboard->xcb_connection(), m_window, XCB_CW_EVENT_MASK, values); - m_abortTimerId = startTimer(m_clipboard->clipboardTimeout()); + m_abortTimerId = startTimer(std::chrono::milliseconds{m_clipboard->clipboardTimeout()}); } QXcbClipboardTransaction::~QXcbClipboardTransaction() @@ -145,7 +145,7 @@ bool QXcbClipboardTransaction::updateIncrementalProperty(const xcb_property_noti // restart the timer if (m_abortTimerId) killTimer(m_abortTimerId); - m_abortTimerId = startTimer(m_clipboard->clipboardTimeout()); + m_abortTimerId = startTimer(std::chrono::milliseconds{m_clipboard->clipboardTimeout()}); uint bytes_left = uint(m_data.size()) - m_offset; if (bytes_left > 0) { @@ -203,7 +203,7 @@ QXcbClipboard::QXcbClipboard(QXcbConnection *c) xcb_xfixes_select_selection_input_checked(xcb_connection(), connection()->qtSelectionOwner(), XCB_ATOM_PRIMARY, mask); xcb_xfixes_select_selection_input_checked(xcb_connection(), connection()->qtSelectionOwner(), - atom(QXcbAtom::CLIPBOARD), mask); + atom(QXcbAtom::AtomCLIPBOARD), mask); } // xcb_change_property_request_t and xcb_get_property_request_t are the same size @@ -218,13 +218,13 @@ QXcbClipboard::~QXcbClipboard() m_timestamp[QClipboard::Selection] != XCB_CURRENT_TIME) { // First we check if there is a clipboard manager. - if (connection()->selectionOwner(atom(QXcbAtom::CLIPBOARD_MANAGER)) != XCB_NONE) { + if (connection()->selectionOwner(atom(QXcbAtom::AtomCLIPBOARD_MANAGER)) != XCB_NONE) { // we delete the property so the manager saves all TARGETS. xcb_delete_property(xcb_connection(), connection()->qtSelectionOwner(), - atom(QXcbAtom::_QT_SELECTION)); + atom(QXcbAtom::Atom_QT_SELECTION)); xcb_convert_selection(xcb_connection(), connection()->qtSelectionOwner(), - atom(QXcbAtom::CLIPBOARD_MANAGER), atom(QXcbAtom::SAVE_TARGETS), - atom(QXcbAtom::_QT_SELECTION), connection()->time()); + atom(QXcbAtom::AtomCLIPBOARD_MANAGER), atom(QXcbAtom::AtomSAVE_TARGETS), + atom(QXcbAtom::Atom_QT_SELECTION), connection()->time()); connection()->sync(); // waiting until the clipboard manager fetches the content. @@ -259,7 +259,7 @@ bool QXcbClipboard::handlePropertyNotify(const xcb_generic_event_t *event) xcb_atom_t QXcbClipboard::atomForMode(QClipboard::Mode mode) const { if (mode == QClipboard::Clipboard) - return atom(QXcbAtom::CLIPBOARD); + return atom(QXcbAtom::AtomCLIPBOARD); if (mode == QClipboard::Selection) return XCB_ATOM_PRIMARY; return XCB_NONE; @@ -269,7 +269,7 @@ QClipboard::Mode QXcbClipboard::modeForAtom(xcb_atom_t a) const { if (a == XCB_ATOM_PRIMARY) return QClipboard::Selection; - if (a == atom(QXcbAtom::CLIPBOARD)) + if (a == atom(QXcbAtom::AtomCLIPBOARD)) return QClipboard::Clipboard; // not supported enum value, used to detect errors return QClipboard::FindBuffer; @@ -412,10 +412,10 @@ xcb_atom_t QXcbClipboard::sendTargetsSelection(QMimeData *d, xcb_window_t window types.append(atoms.at(j)); } } - types.append(atom(QXcbAtom::TARGETS)); - types.append(atom(QXcbAtom::MULTIPLE)); - types.append(atom(QXcbAtom::TIMESTAMP)); - types.append(atom(QXcbAtom::SAVE_TARGETS)); + types.append(atom(QXcbAtom::AtomTARGETS)); + types.append(atom(QXcbAtom::AtomMULTIPLE)); + types.append(atom(QXcbAtom::AtomTIMESTAMP)); + types.append(atom(QXcbAtom::AtomSAVE_TARGETS)); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, property, XCB_ATOM_ATOM, 32, types.size(), (const void *)types.constData()); @@ -439,7 +439,7 @@ xcb_atom_t QXcbClipboard::sendSelection(QMimeData *d, xcb_atom_t target, xcb_win // don't allow INCR transfers when using MULTIPLE or to // Motif clients (since Motif doesn't support INCR) - static xcb_atom_t motif_clip_temporary = atom(QXcbAtom::CLIP_TEMPORARY); + static xcb_atom_t motif_clip_temporary = atom(QXcbAtom::AtomCLIP_TEMPORARY); bool allow_incr = property != motif_clip_temporary; // This 'bool' can be removed once there is a proper fix for QTBUG-32853 if (m_clipboard_closing) @@ -448,7 +448,7 @@ xcb_atom_t QXcbClipboard::sendSelection(QMimeData *d, xcb_atom_t target, xcb_win if (data.size() > m_maxPropertyRequestDataBytes && allow_incr) { long bytes = data.size(); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, property, - atom(QXcbAtom::INCR), 32, 1, (const void *)&bytes); + atom(QXcbAtom::AtomINCR), 32, 1, (const void *)&bytes); auto transaction = new QXcbClipboardTransaction(this, window, property, data, atomFormat, dataFormat); m_transactions.insert(window, transaction); return property; @@ -532,9 +532,9 @@ void QXcbClipboard::handleSelectionRequest(xcb_selection_request_event_t *req) return; } - xcb_atom_t targetsAtom = atom(QXcbAtom::TARGETS); - xcb_atom_t multipleAtom = atom(QXcbAtom::MULTIPLE); - xcb_atom_t timestampAtom = atom(QXcbAtom::TIMESTAMP); + xcb_atom_t targetsAtom = atom(QXcbAtom::AtomTARGETS); + xcb_atom_t multipleAtom = atom(QXcbAtom::AtomMULTIPLE); + xcb_atom_t timestampAtom = atom(QXcbAtom::AtomTIMESTAMP); struct AtomPair { xcb_atom_t target; xcb_atom_t property; } *multi = nullptr; xcb_atom_t multi_type = XCB_NONE; @@ -708,7 +708,7 @@ bool QXcbClipboard::clipboardReadProperty(xcb_window_t win, xcb_atom_t property, // correct size, not 0-term. if (size) *size = buffer_offset; - if (*type == atom(QXcbAtom::INCR)) + if (*type == atom(QXcbAtom::AtomINCR)) m_incr_receive_time = connection()->getTimestamp(); if (deleteProperty) xcb_delete_property(xcb_connection(), win, property); @@ -747,12 +747,12 @@ xcb_generic_event_t *QXcbClipboard::waitForClipboardEvent(xcb_window_t window, i const QXcbEventNode *flushedTailNode = queue->flushedTail(); if (checkManager) { - if (connection()->selectionOwner(atom(QXcbAtom::CLIPBOARD_MANAGER)) == XCB_NONE) + if (connection()->selectionOwner(atom(QXcbAtom::AtomCLIPBOARD_MANAGER)) == XCB_NONE) return nullptr; } // process other clipboard events, since someone is probably requesting data from us - auto clipboardAtom = atom(QXcbAtom::CLIPBOARD); + auto clipboardAtom = atom(QXcbAtom::AtomCLIPBOARD); e = queue->peek([clipboardAtom](xcb_generic_event_t *event, int type) { xcb_atom_t selection = XCB_ATOM_NONE; if (type == XCB_SELECTION_REQUEST) @@ -846,7 +846,7 @@ QByteArray QXcbClipboard::clipboardReadIncrementalProperty(xcb_window_t win, xcb QByteArray QXcbClipboard::getDataInFormat(xcb_atom_t modeAtom, xcb_atom_t fmtAtom) { - return getSelection(modeAtom, fmtAtom, atom(QXcbAtom::_QT_SELECTION)); + return getSelection(modeAtom, fmtAtom, atom(QXcbAtom::Atom_QT_SELECTION)); } QByteArray QXcbClipboard::getSelection(xcb_atom_t selection, xcb_atom_t target, xcb_atom_t property, xcb_timestamp_t time) @@ -870,7 +870,7 @@ QByteArray QXcbClipboard::getSelection(xcb_atom_t selection, xcb_atom_t target, xcb_atom_t type; if (clipboardReadProperty(win, property, true, &buf, nullptr, &type, nullptr)) { - if (type == atom(QXcbAtom::INCR)) { + if (type == atom(QXcbAtom::AtomINCR)) { int nbytes = buf.size() >= 4 ? *((int*)buf.data()) : 0; buf = clipboardReadIncrementalProperty(win, property, nbytes, false); } diff --git a/src/plugins/platforms/xcb/qxcbconnection.cpp b/src/plugins/platforms/xcb/qxcbconnection.cpp index 51530e0055..1056c6408f 100644 --- a/src/plugins/platforms/xcb/qxcbconnection.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection.cpp @@ -35,6 +35,10 @@ #undef explicit #include <xcb/xinput.h> +#if QT_CONFIG(xcb_xlib) +#include "qt_xlib_wrapper.h" +#endif + QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; @@ -81,16 +85,16 @@ QXcbConnection::QXcbConnection(QXcbNativeInterface *nativeInterface, bool canGra m_drag = new QXcbDrag(this); #endif - m_startupId = qgetenv("DESKTOP_STARTUP_ID"); - if (!m_startupId.isNull()) + setStartupId(qgetenv("DESKTOP_STARTUP_ID")); + if (!startupId().isNull()) qunsetenv("DESKTOP_STARTUP_ID"); const int focusInDelay = 100; m_focusInTimer.setSingleShot(true); m_focusInTimer.setInterval(focusInDelay); - m_focusInTimer.callOnTimeout([]() { + m_focusInTimer.callOnTimeout(this, []() { // No FocusIn events for us, proceed with FocusOut normally. - QWindowSystemInterface::handleWindowActivated(nullptr, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason); }); sync(); @@ -141,12 +145,12 @@ void QXcbConnection::removeWindowEventListener(xcb_window_t id) QXcbWindowEventListener *QXcbConnection::windowEventListenerFromId(xcb_window_t id) { - return m_mapper.value(id, 0); + return m_mapper.value(id, nullptr); } QXcbWindow *QXcbConnection::platformWindowFromId(xcb_window_t id) { - QXcbWindowEventListener *listener = m_mapper.value(id, 0); + QXcbWindowEventListener *listener = m_mapper.value(id, nullptr); if (listener) return listener->toWindow(); return nullptr; @@ -598,12 +602,12 @@ void QXcbConnection::handleXcbEvent(xcb_generic_event_t *event) if (clientMessage->format != 32) return; #if QT_CONFIG(draganddrop) - if (clientMessage->type == atom(QXcbAtom::XdndStatus)) + if (clientMessage->type == atom(QXcbAtom::AtomXdndStatus)) drag()->handleStatus(clientMessage); - else if (clientMessage->type == atom(QXcbAtom::XdndFinished)) + else if (clientMessage->type == atom(QXcbAtom::AtomXdndFinished)) drag()->handleFinished(clientMessage); #endif - if (m_systemTrayTracker && clientMessage->type == atom(QXcbAtom::MANAGER)) + if (m_systemTrayTracker && clientMessage->type == atom(QXcbAtom::AtomMANAGER)) m_systemTrayTracker->notifyManagerClientMessageEvent(clientMessage); HANDLE_PLATFORM_WINDOW_EVENT(xcb_client_message_event_t, window, handleClientMessageEvent); } @@ -654,7 +658,7 @@ void QXcbConnection::handleXcbEvent(xcb_generic_event_t *event) setTime(selectionRequest->time); #endif #if QT_CONFIG(draganddrop) - if (selectionRequest->selection == atom(QXcbAtom::XdndSelection)) + if (selectionRequest->selection == atom(QXcbAtom::AtomXdndSelection)) m_drag->handleSelectionRequest(selectionRequest); else #endif @@ -682,11 +686,11 @@ void QXcbConnection::handleXcbEvent(xcb_generic_event_t *event) if (m_clipboard->handlePropertyNotify(event)) break; #endif - if (propertyNotify->atom == atom(QXcbAtom::_NET_WORKAREA)) { + if (propertyNotify->atom == atom(QXcbAtom::Atom_NET_WORKAREA)) { QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(propertyNotify->window); if (virtualDesktop) virtualDesktop->updateWorkArea(); - } else if (propertyNotify->atom == atom(QXcbAtom::_NET_SUPPORTED)) { + } else if (propertyNotify->atom == atom(QXcbAtom::Atom_NET_SUPPORTED)) { m_wmSupport->updateNetWMAtoms(); } else { HANDLE_PLATFORM_WINDOW_EVENT(xcb_property_notify_event_t, window, handlePropertyNotifyEvent); @@ -713,7 +717,7 @@ void QXcbConnection::handleXcbEvent(xcb_generic_event_t *event) #ifndef QT_NO_CLIPBOARD m_clipboard->handleXFixesSelectionRequest(notify_event); #endif - for (QXcbVirtualDesktop *virtualDesktop : qAsConst(m_virtualDesktops)) + for (QXcbVirtualDesktop *virtualDesktop : std::as_const(m_virtualDesktops)) virtualDesktop->handleXFixesSelectionNotify(notify_event); } else if (isXRandrType(response_type, XCB_RANDR_NOTIFY)) { if (!isAtLeastXRandR15()) @@ -771,6 +775,28 @@ void QXcbConnection::setMousePressWindow(QXcbWindow *w) m_mousePressWindow = w; } +QByteArray QXcbConnection::startupId() const +{ + return m_startupId; +} +void QXcbConnection::setStartupId(const QByteArray &nextId) +{ + m_startupId = nextId; + if (m_clientLeader) { + if (!nextId.isEmpty()) + xcb_change_property(xcb_connection(), + XCB_PROP_MODE_REPLACE, + clientLeader(), + atom(QXcbAtom::Atom_NET_STARTUP_ID), + atom(QXcbAtom::AtomUTF8_STRING), + 8, + nextId.size(), + nextId.constData()); + else + xcb_delete_property(xcb_connection(), clientLeader(), atom(QXcbAtom::Atom_NET_STARTUP_ID)); + } +} + void QXcbConnection::grabServer() { if (m_canGrabServer) @@ -796,8 +822,8 @@ xcb_timestamp_t QXcbConnection::getTimestamp() { // send a dummy event to myself to get the timestamp from X server. xcb_window_t window = rootWindow(); - xcb_atom_t dummyAtom = atom(QXcbAtom::CLIP_TEMPORARY); - xcb_change_property(xcb_connection(), XCB_PROP_MODE_APPEND, window, dummyAtom, + xcb_atom_t dummyAtom = atom(QXcbAtom::Atom_QT_GET_TIMESTAMP); + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, dummyAtom, XCB_ATOM_INTEGER, 32, 0, nullptr); connection()->flush(); @@ -829,8 +855,6 @@ xcb_timestamp_t QXcbConnection::getTimestamp() xcb_timestamp_t timestamp = pn->time; free(event); - xcb_delete_property(xcb_connection(), window, dummyAtom); - return timestamp; } @@ -897,7 +921,7 @@ xcb_window_t QXcbConnection::clientLeader() xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_clientLeader, - atom(QXcbAtom::WM_CLIENT_LEADER), + atom(QXcbAtom::AtomWM_CLIENT_LEADER), XCB_ATOM_WINDOW, 32, 1, @@ -910,13 +934,15 @@ xcb_window_t QXcbConnection::clientLeader() xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_clientLeader, - atom(QXcbAtom::SM_CLIENT_ID), + atom(QXcbAtom::AtomSM_CLIENT_ID), XCB_ATOM_STRING, 8, - session.length(), + session.size(), session.constData()); } #endif + + setStartupId(startupId()); } return m_clientLeader; } @@ -1029,8 +1055,8 @@ bool QXcbConnection::isUserInputEvent(xcb_generic_event_t *event) const if (eventType == XCB_CLIENT_MESSAGE) { auto clientMessage = reinterpret_cast<const xcb_client_message_event_t *>(event); - if (clientMessage->format == 32 && clientMessage->type == atom(QXcbAtom::WM_PROTOCOLS)) - if (clientMessage->data.data32[0] == atom(QXcbAtom::WM_DELETE_WINDOW)) + if (clientMessage->format == 32 && clientMessage->type == atom(QXcbAtom::AtomWM_PROTOCOLS)) + if (clientMessage->data.data32[0] == atom(QXcbAtom::AtomWM_DELETE_WINDOW)) isInputEvent = true; } @@ -1066,6 +1092,10 @@ void QXcbConnection::processXcbEvents(QEventLoop::ProcessEventsFlags flags) m_eventQueue->flushBufferedEvents(); } +#if QT_CONFIG(xcb_xlib) + qt_XFlush(static_cast<Display *>(xlib_display())); +#endif + xcb_flush(xcb_connection()); } @@ -1111,13 +1141,6 @@ Qt::MouseButtons QXcbConnection::queryMouseButtons() const return translateMouseButtons(stateMask); } -Qt::KeyboardModifiers QXcbConnection::queryKeyboardModifiers() const -{ - int stateMask = 0; - QXcbCursor::queryPointer(connection(), nullptr, nullptr, &stateMask); - return keyboard()->translateModifiers(stateMask); -} - QXcbGlIntegration *QXcbConnection::glIntegration() const { if (m_glIntegrationInitialized) diff --git a/src/plugins/platforms/xcb/qxcbconnection.h b/src/plugins/platforms/xcb/qxcbconnection.h index 2d55524f6f..527744fe81 100644 --- a/src/plugins/platforms/xcb/qxcbconnection.h +++ b/src/plugins/platforms/xcb/qxcbconnection.h @@ -170,9 +170,8 @@ public: QXcbWindow *mousePressWindow() const { return m_mousePressWindow; } void setMousePressWindow(QXcbWindow *); - QByteArray startupId() const { return m_startupId; } - void setStartupId(const QByteArray &nextId) { m_startupId = nextId; } - void clearStartupId() { m_startupId.clear(); } + QByteArray startupId() const; + void setStartupId(const QByteArray &nextId); void grabServer(); void ungrabServer(); @@ -184,7 +183,6 @@ public: QXcbSystemTrayTracker *systemTrayTracker() const; Qt::MouseButtons queryMouseButtons() const; - Qt::KeyboardModifiers queryKeyboardModifiers() const; bool isUserInputEvent(xcb_generic_event_t *event) const; @@ -368,7 +366,7 @@ Q_DECLARE_TYPEINFO(QXcbConnection::TabletData, Q_RELOCATABLE_TYPE); class QXcbConnectionGrabber { public: - QXcbConnectionGrabber(QXcbConnection *connection); + Q_NODISCARD_CTOR QXcbConnectionGrabber(QXcbConnection *connection); ~QXcbConnectionGrabber(); void release(); private: diff --git a/src/plugins/platforms/xcb/qxcbconnection_basic.cpp b/src/plugins/platforms/xcb/qxcbconnection_basic.cpp index 5f1b964afa..6416003bea 100644 --- a/src/plugins/platforms/xcb/qxcbconnection_basic.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection_basic.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qxcbconnection_basic.h" #include "qxcbbackingstore.h" // for createSystemVShmSegment() +#include "private/qoffsetstringarray_p.h" #include <xcb/randr.h> #include <xcb/shm.h> @@ -27,7 +28,7 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcQpaXcb, "qt.qpa.xcb") #if QT_CONFIG(xcb_xlib) -static const char * const xcbConnectionErrors[] = { +static constexpr auto xcbConnectionErrors = qOffsetStringArray( "No error", /* Error 0 */ "I/O error", /* XCB_CONN_ERROR */ "Unsupported extension used", /* XCB_CONN_CLOSED_EXT_NOTSUPPORTED */ @@ -36,7 +37,7 @@ static const char * const xcbConnectionErrors[] = { "Failed to parse display string", /* XCB_CONN_CLOSED_PARSE_ERR */ "No such screen on display", /* XCB_CONN_CLOSED_INVALID_SCREEN */ "Error during FD passing" /* XCB_CONN_CLOSED_FDPASSING_FAILED */ -}; +); static int nullErrorHandler(Display *dpy, XErrorEvent *err) { @@ -60,8 +61,7 @@ static int ioErrorHandler(Display *dpy) /* Print a message with a textual description of the error */ int code = xcb_connection_has_error(conn); const char *str = "Unknown error"; - int arrayLength = sizeof(xcbConnectionErrors) / sizeof(xcbConnectionErrors[0]); - if (code >= 0 && code < arrayLength) + if (code >= 0 && code < xcbConnectionErrors.count()) str = xcbConnectionErrors[code]; qWarning("The X11 connection broke: %s (code %d)", str, code); diff --git a/src/plugins/platforms/xcb/qxcbconnection_screens.cpp b/src/plugins/platforms/xcb/qxcbconnection_screens.cpp index aa0f2fef65..c31e9b1039 100644 --- a/src/plugins/platforms/xcb/qxcbconnection_screens.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection_screens.cpp @@ -155,7 +155,7 @@ void QXcbConnection::updateScreens(const xcb_randr_notify_event_t *event) } } - qCDebug(lcQpaScreen) << "updateScreens: primary output is" << qAsConst(m_screens).first()->name(); + qCDebug(lcQpaScreen) << "updateScreens: primary output is" << std::as_const(m_screens).first()->name(); } } @@ -184,7 +184,7 @@ void QXcbConnection::updateScreen(QXcbScreen *screen, const xcb_randr_output_cha // If the screen became primary, reshuffle the order in QGuiApplicationPrivate const int idx = m_screens.indexOf(screen); if (idx > 0) { - qAsConst(m_screens).first()->setPrimary(false); + std::as_const(m_screens).first()->setPrimary(false); m_screens.swapItemsAt(0, idx); } screen->virtualDesktop()->setPrimaryScreen(screen); @@ -204,7 +204,7 @@ QXcbScreen *QXcbConnection::createScreen(QXcbVirtualDesktop *virtualDesktop, if (screen->isPrimary()) { if (!m_screens.isEmpty()) - qAsConst(m_screens).first()->setPrimary(false); + std::as_const(m_screens).first()->setPrimary(false); m_screens.prepend(screen); } else { @@ -219,7 +219,7 @@ QXcbScreen *QXcbConnection::createScreen(QXcbVirtualDesktop *virtualDesktop, void QXcbConnection::destroyScreen(QXcbScreen *screen) { QXcbVirtualDesktop *virtualDesktop = screen->virtualDesktop(); - if (virtualDesktop->screens().count() == 1) { + if (virtualDesktop->screens().size() == 1) { // If there are no other screens on the same virtual desktop, // then transform the physical screen into a fake screen. const QString nameWas = screen->name(); @@ -253,7 +253,7 @@ void QXcbConnection::updateScreen_monitor(QXcbScreen *screen, xcb_randr_monitor_ if (screen->isPrimary()) { const int idx = m_screens.indexOf(screen); if (idx > 0) { - qAsConst(m_screens).first()->setPrimary(false); + std::as_const(m_screens).first()->setPrimary(false); m_screens.swapItemsAt(0, idx); } screen->virtualDesktop()->setPrimaryScreen(screen); @@ -268,7 +268,7 @@ QXcbScreen *QXcbConnection::createScreen_monitor(QXcbVirtualDesktop *virtualDesk if (screen->isPrimary()) { if (!m_screens.isEmpty()) - qAsConst(m_screens).first()->setPrimary(false); + std::as_const(m_screens).first()->setPrimary(false); m_screens.prepend(screen); } else { @@ -326,7 +326,7 @@ void QXcbConnection::initializeScreens(bool initialized) ++xcbScreenNumber; } - for (QXcbVirtualDesktop *virtualDesktop : qAsConst(m_virtualDesktops)) + for (QXcbVirtualDesktop *virtualDesktop : std::as_const(m_virtualDesktops)) virtualDesktop->subscribeToXFixesSelectionNotify(); if (m_virtualDesktops.isEmpty()) { @@ -334,7 +334,7 @@ void QXcbConnection::initializeScreens(bool initialized) } else { // Ensure the primary screen is first on the list if (primaryScreen) { - if (qAsConst(m_screens).first() != primaryScreen) { + if (std::as_const(m_screens).first() != primaryScreen) { m_screens.removeOne(primaryScreen); m_screens.prepend(primaryScreen); } @@ -342,14 +342,14 @@ void QXcbConnection::initializeScreens(bool initialized) // Push the screens to QGuiApplication if (!initialized) { - for (QXcbScreen *screen : qAsConst(m_screens)) { + for (QXcbScreen *screen : std::as_const(m_screens)) { qCDebug(lcQpaScreen) << "adding" << screen << "(Primary:" << screen->isPrimary() << ")"; QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary()); } } if (!m_screens.isEmpty()) - qCDebug(lcQpaScreen) << "initializeScreens: primary output is" << qAsConst(m_screens).first()->name(); + qCDebug(lcQpaScreen) << "initializeScreens: primary output is" << std::as_const(m_screens).first()->name(); } } @@ -489,6 +489,10 @@ void QXcbConnection::initializeScreensFromMonitor(xcb_screen_iterator_t *it, int virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber); m_virtualDesktops.append(virtualDesktop); } + + if (xcbScreenNumber != primaryScreenNumber()) + return; + QList<QPlatformScreen*> old = virtualDesktop->m_screens; QList<QPlatformScreen *> siblings; @@ -511,24 +515,22 @@ void QXcbConnection::initializeScreensFromMonitor(xcb_screen_iterator_t *it, int screen = findScreenForMonitorInfo(old, monitor_info); if (!screen) { screen = createScreen_monitor(virtualDesktop, monitor_info, monitors_r->timestamp); - QHighDpiScaling::updateHighDpiScaling(); } else { updateScreen_monitor(screen, monitor_info, monitors_r->timestamp); old.removeAll(screen); } } - m_screens << screen; + if (!m_screens.contains(screen)) + m_screens << screen; siblings << screen; // similar logic with QXcbConnection::initializeScreensFromOutput() - if (primaryScreenNumber() == xcbScreenNumber) { - if (!(*primaryScreen) || monitor_info->primary) { - if (*primaryScreen) - (*primaryScreen)->setPrimary(false); - *primaryScreen = screen; - (*primaryScreen)->setPrimary(true); - siblings.prepend(siblings.takeLast()); - } + if (!(*primaryScreen) || monitor_info->primary) { + if (*primaryScreen) + (*primaryScreen)->setPrimary(false); + *primaryScreen = screen; + (*primaryScreen)->setPrimary(true); + siblings.prepend(siblings.takeLast()); } xcb_randr_monitor_info_next(&monitor_iter); @@ -551,10 +553,8 @@ void QXcbConnection::initializeScreensFromMonitor(xcb_screen_iterator_t *it, int qCDebug(lcQpaScreen) << "create a fake screen: " << screen; } - if (primaryScreenNumber() == xcbScreenNumber) { - *primaryScreen = screen; - (*primaryScreen)->setPrimary(true); - } + *primaryScreen = screen; + (*primaryScreen)->setPrimary(true); siblings << screen; m_screens << screen; diff --git a/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp b/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp index 8cb4e5d603..4f62a1880b 100644 --- a/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp @@ -15,7 +15,9 @@ #include <xcb/xinput.h> +#if QT_CONFIG(gestures) #define QT_XCB_HAS_TOUCHPAD_GESTURES (XCB_INPUT_MINOR_VERSION >= 4) +#endif using namespace Qt::StringLiterals; @@ -226,7 +228,7 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, auto *deviceInfo = reinterpret_cast<xcb_input_xi_device_info_t *>(info); if (removeExisting) { #if QT_CONFIG(tabletevent) - for (int i = 0; i < m_tabletData.count(); ++i) { + for (int i = 0; i < m_tabletData.size(); ++i) { if (m_tabletData.at(i).deviceId == deviceInfo->deviceid) { m_tabletData.remove(i); break; @@ -239,6 +241,7 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, const QByteArray nameRaw = QByteArray(xcb_input_xi_device_info_name(deviceInfo), xcb_input_xi_device_info_name_length(deviceInfo)); const QString name = QString::fromUtf8(nameRaw); + m_xiSlavePointerIds.append(deviceInfo->deviceid); qCDebug(lcQpaXInputDevices) << "input device " << name << "ID" << deviceInfo->deviceid; #if QT_CONFIG(tabletevent) TabletData tabletData; @@ -270,9 +273,9 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, tabletData.valuatorInfo[valuatorAtom] = info; } #endif // QT_CONFIG(tabletevent) - if (valuatorAtom == QXcbAtom::RelHorizScroll || valuatorAtom == QXcbAtom::RelHorizWheel) + if (valuatorAtom == QXcbAtom::AtomRelHorizScroll || valuatorAtom == QXcbAtom::AtomRelHorizWheel) scrollingDevice()->lastScrollPosition.setX(fixed3232ToReal(vci->value)); - else if (valuatorAtom == QXcbAtom::RelVertScroll || valuatorAtom == QXcbAtom::RelVertWheel) + else if (valuatorAtom == QXcbAtom::AtomRelVertScroll || valuatorAtom == QXcbAtom::AtomRelVertWheel) scrollingDevice()->lastScrollPosition.setY(fixed3232ToReal(vci->value)); break; } @@ -300,14 +303,14 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, xcb_atom_t label5 = labels[4]; // Some drivers have no labels on the wheel buttons, some have no label on just one and some have no label on // button 4 and the wrong one on button 5. So we just check that they are not labelled with unrelated buttons. - if ((!label4 || qatom(label4) == QXcbAtom::ButtonWheelUp || qatom(label4) == QXcbAtom::ButtonWheelDown) && - (!label5 || qatom(label5) == QXcbAtom::ButtonWheelUp || qatom(label5) == QXcbAtom::ButtonWheelDown)) + if ((!label4 || qatom(label4) == QXcbAtom::AtomButtonWheelUp || qatom(label4) == QXcbAtom::AtomButtonWheelDown) && + (!label5 || qatom(label5) == QXcbAtom::AtomButtonWheelUp || qatom(label5) == QXcbAtom::AtomButtonWheelDown)) scrollingDevice()->legacyOrientations |= Qt::Vertical; } if (bci->num_buttons >= 7) { xcb_atom_t label6 = labels[5]; xcb_atom_t label7 = labels[6]; - if ((!label6 || qatom(label6) == QXcbAtom::ButtonHorizWheelLeft) && (!label7 || qatom(label7) == QXcbAtom::ButtonHorizWheelRight)) + if ((!label6 || qatom(label6) == QXcbAtom::AtomButtonHorizWheelLeft) && (!label7 || qatom(label7) == QXcbAtom::AtomButtonHorizWheelRight)) scrollingDevice()->legacyOrientations |= Qt::Horizontal; } buttonCount = bci->num_buttons; @@ -331,9 +334,9 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, bool isTablet = false; #if QT_CONFIG(tabletevent) // If we have found the valuators which we expect a tablet to have, it might be a tablet. - if (tabletData.valuatorInfo.contains(QXcbAtom::AbsX) && - tabletData.valuatorInfo.contains(QXcbAtom::AbsY) && - tabletData.valuatorInfo.contains(QXcbAtom::AbsPressure)) + if (tabletData.valuatorInfo.contains(QXcbAtom::AtomAbsX) && + tabletData.valuatorInfo.contains(QXcbAtom::AtomAbsY) && + tabletData.valuatorInfo.contains(QXcbAtom::AtomAbsPressure)) isTablet = true; // But we need to be careful not to take the touch and tablet-button devices as tablets. @@ -356,7 +359,7 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, // combined device (evdev) rather than separate pen/eraser (wacom driver) tabletData.pointerType = QPointingDevice::PointerType::Pen; dbgType = "pen"_L1; - } else if (nameLower.contains("aiptek") /* && device == QXcbAtom::KEYBOARD */) { + } else if (nameLower.contains("aiptek") /* && device == QXcbAtom::AtomKEYBOARD */) { // some "Genius" tablets isTablet = true; tabletData.pointerType = QPointingDevice::PointerType::Pen; @@ -384,9 +387,9 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, m_tabletData.append(tabletData); qCDebug(lcQpaXInputDevices) << " it's a tablet with pointer type" << dbgType; QPointingDevice::Capabilities capsOverride = QInputDevice::Capability::None; - if (tabletData.valuatorInfo.contains(QXcbAtom::AbsTiltX)) + if (tabletData.valuatorInfo.contains(QXcbAtom::AtomAbsTiltX)) capsOverride.setFlag(QInputDevice::Capability::XTilt); - if (tabletData.valuatorInfo.contains(QXcbAtom::AbsTiltY)) + if (tabletData.valuatorInfo.contains(QXcbAtom::AtomAbsTiltY)) capsOverride.setFlag(QInputDevice::Capability::YTilt); // TODO can we get USB ID? Q_ASSERT(deviceInfo->deviceid == tabletData.deviceId); @@ -442,12 +445,15 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, } } +/*! + Find all X11 input devices at startup, or react to a device hierarchy event, + and create/delete the corresponding QInputDevice instances as necessary. + Afterwards, we expect QInputDevice::devices() to contain only the + Qt-relevant devices that \c {xinput list} reports. The parent of each master + device is this QXcbConnection object; the parent of each slave is its master. +*/ void QXcbConnection::xi2SetupDevices() { -#if QT_CONFIG(tabletevent) - m_tabletData.clear(); -#endif - m_touchDevices.clear(); m_xiMasterPointerIds.clear(); auto reply = Q_XCB_REPLY(xcb_input_xi_query_device, xcb_connection(), XCB_INPUT_DEVICE_ALL); @@ -456,6 +462,18 @@ void QXcbConnection::xi2SetupDevices() return; } + // Start with all known devices; remove the ones that still exist. + // Afterwards, previousDevices will be the list of those that we should delete. + QList<const QInputDevice *> previousDevices = QInputDevice::devices(); + // Return true if the device with the given systemId is new; + // otherwise remove it from previousDevices and return false. + auto newOrKeep = [&previousDevices](qint64 systemId) { + // if nothing is removed from previousDevices, it's a new device + return !previousDevices.removeIf([systemId](const QInputDevice *dev) { + return dev->systemId() == systemId; + }); + }; + // XInput doesn't provide a way to identify "seats"; but each device has an attachment to another device. // So we make up a seatId: master-keyboard-id << 16 | master-pointer-id. @@ -464,17 +482,21 @@ void QXcbConnection::xi2SetupDevices() xcb_input_xi_device_info_t *deviceInfo = it.data; switch (deviceInfo->type) { case XCB_INPUT_DEVICE_TYPE_MASTER_KEYBOARD: { - auto dev = new QInputDevice(QString::fromUtf8(xcb_input_xi_device_info_name(deviceInfo)), - deviceInfo->deviceid, QInputDevice::DeviceType::Keyboard, - QString::number(deviceInfo->deviceid << 16 | deviceInfo->attachment, 16), this); - QWindowSystemInterface::registerInputDevice(dev); + if (newOrKeep(deviceInfo->deviceid)) { + auto dev = new QInputDevice(QString::fromUtf8(xcb_input_xi_device_info_name(deviceInfo)), + deviceInfo->deviceid, QInputDevice::DeviceType::Keyboard, + QString::number(deviceInfo->deviceid << 16 | deviceInfo->attachment, 16), this); + QWindowSystemInterface::registerInputDevice(dev); + } } break; case XCB_INPUT_DEVICE_TYPE_MASTER_POINTER: { m_xiMasterPointerIds.append(deviceInfo->deviceid); - auto dev = new QXcbScrollingDevice(QString::fromUtf8(xcb_input_xi_device_info_name(deviceInfo)), deviceInfo->deviceid, - QInputDevice::Capability::Position | QInputDevice::Capability::Scroll | QInputDevice::Capability::Hover, - 32, QString::number(deviceInfo->attachment << 16 | deviceInfo->deviceid, 16), this); - QWindowSystemInterface::registerInputDevice(dev); + if (newOrKeep(deviceInfo->deviceid)) { + auto dev = new QXcbScrollingDevice(QString::fromUtf8(xcb_input_xi_device_info_name(deviceInfo)), deviceInfo->deviceid, + QInputDevice::Capability::Position | QInputDevice::Capability::Scroll | QInputDevice::Capability::Hover, + 32, QString::number(deviceInfo->attachment << 16 | deviceInfo->deviceid, 16), this); + QWindowSystemInterface::registerInputDevice(dev); + } continue; } break; default: @@ -491,23 +513,36 @@ void QXcbConnection::xi2SetupDevices() // already registered break; case XCB_INPUT_DEVICE_TYPE_SLAVE_POINTER: { - m_xiSlavePointerIds.append(deviceInfo->deviceid); - QInputDevice *master = const_cast<QInputDevice *>(QInputDevicePrivate::fromId(deviceInfo->attachment)); - Q_ASSERT(master); - xi2SetupSlavePointerDevice(deviceInfo, false, qobject_cast<QPointingDevice *>(master)); + if (newOrKeep(deviceInfo->deviceid)) { + m_xiSlavePointerIds.append(deviceInfo->deviceid); + QInputDevice *master = const_cast<QInputDevice *>(QInputDevicePrivate::fromId(deviceInfo->attachment)); + Q_ASSERT(master); + xi2SetupSlavePointerDevice(deviceInfo, false, qobject_cast<QPointingDevice *>(master)); + } } break; case XCB_INPUT_DEVICE_TYPE_SLAVE_KEYBOARD: { - QInputDevice *master = const_cast<QInputDevice *>(QInputDevicePrivate::fromId(deviceInfo->attachment)); - Q_ASSERT(master); - QWindowSystemInterface::registerInputDevice(new QInputDevice( - QString::fromUtf8(xcb_input_xi_device_info_name(deviceInfo)), deviceInfo->deviceid, - QInputDevice::DeviceType::Keyboard, master->seatName(), master)); + if (newOrKeep(deviceInfo->deviceid)) { + QInputDevice *master = const_cast<QInputDevice *>(QInputDevicePrivate::fromId(deviceInfo->attachment)); + Q_ASSERT(master); + QWindowSystemInterface::registerInputDevice(new QInputDevice( + QString::fromUtf8(xcb_input_xi_device_info_name(deviceInfo)), deviceInfo->deviceid, + QInputDevice::DeviceType::Keyboard, master->seatName(), master)); + } } break; case XCB_INPUT_DEVICE_TYPE_FLOATING_SLAVE: break; } } + // previousDevices is now the list of those that are no longer found + qCDebug(lcQpaXInputDevices) << "removed" << previousDevices; + for (auto it = previousDevices.constBegin(); it != previousDevices.constEnd(); ++it) { + const auto id = (*it)->systemId(); + m_xiSlavePointerIds.removeAll(id); + m_touchDevices.remove(id); + } + qDeleteAll(previousDevices); + if (m_xiMasterPointerIds.size() > 1) qCDebug(lcQpaXInputDevices) << "multi-pointer X detected"; } @@ -573,27 +608,27 @@ QXcbConnection::TouchDeviceData *QXcbConnection::populateTouchDevices(void *info // Some devices (mice) report a resolution of 0; they will be excluded later, // for now just prevent a division by zero const int vciResolution = vci->resolution ? vci->resolution : 1; - if (valuatorAtom == QXcbAtom::AbsMTPositionX) + if (valuatorAtom == QXcbAtom::AtomAbsMTPositionX) caps |= QInputDevice::Capability::Position | QInputDevice::Capability::NormalizedPosition; - else if (valuatorAtom == QXcbAtom::AbsMTTouchMajor) + else if (valuatorAtom == QXcbAtom::AtomAbsMTTouchMajor) caps |= QInputDevice::Capability::Area; - else if (valuatorAtom == QXcbAtom::AbsMTOrientation) + else if (valuatorAtom == QXcbAtom::AtomAbsMTOrientation) dev.providesTouchOrientation = true; - else if (valuatorAtom == QXcbAtom::AbsMTPressure || valuatorAtom == QXcbAtom::AbsPressure) + else if (valuatorAtom == QXcbAtom::AtomAbsMTPressure || valuatorAtom == QXcbAtom::AtomAbsPressure) caps |= QInputDevice::Capability::Pressure; - else if (valuatorAtom == QXcbAtom::RelX) { + else if (valuatorAtom == QXcbAtom::AtomRelX) { hasRelativeCoords = true; dev.size.setWidth((fixed3232ToReal(vci->max) - fixed3232ToReal(vci->min)) * 1000.0 / vciResolution); - } else if (valuatorAtom == QXcbAtom::RelY) { + } else if (valuatorAtom == QXcbAtom::AtomRelY) { hasRelativeCoords = true; dev.size.setHeight((fixed3232ToReal(vci->max) - fixed3232ToReal(vci->min)) * 1000.0 / vciResolution); - } else if (valuatorAtom == QXcbAtom::AbsX) { + } else if (valuatorAtom == QXcbAtom::AtomAbsX) { caps |= QInputDevice::Capability::Position; dev.size.setWidth((fixed3232ToReal(vci->max) - fixed3232ToReal(vci->min)) * 1000.0 / vciResolution); - } else if (valuatorAtom == QXcbAtom::AbsY) { + } else if (valuatorAtom == QXcbAtom::AtomAbsY) { caps |= QInputDevice::Capability::Position; dev.size.setHeight((fixed3232ToReal(vci->max) - fixed3232ToReal(vci->min)) * 1000.0 / vciResolution); - } else if (valuatorAtom == QXcbAtom::RelVertWheel || valuatorAtom == QXcbAtom::RelHorizWheel) { + } else if (valuatorAtom == QXcbAtom::AtomRelVertWheel || valuatorAtom == QXcbAtom::AtomRelHorizWheel) { caps |= QInputDevice::Capability::Scroll; } break; @@ -651,7 +686,7 @@ void QXcbConnection::xi2HandleEvent(xcb_ge_event_t *event) { auto *xiEvent = reinterpret_cast<qt_xcb_input_device_event_t *>(event); setTime(xiEvent->time); - if (m_xiSlavePointerIds.contains(xiEvent->deviceid)) { + if (m_xiSlavePointerIds.contains(xiEvent->deviceid) && xiEvent->event_type != XCB_INPUT_PROPERTY) { if (!m_duringSystemMoveResize) return; if (xiEvent->event == XCB_NONE) @@ -730,6 +765,8 @@ void QXcbConnection::xi2HandleEvent(xcb_ge_event_t *event) if (auto device = QPointingDevicePrivate::pointingDeviceById(sourceDeviceId)) xi2HandleScrollEvent(event, device); + else + qCDebug(lcQpaXInputEvents) << "scroll event from unregistered device" << sourceDeviceId; if (xiDeviceEvent) { switch (xiDeviceEvent->event_type) { @@ -776,7 +813,16 @@ void QXcbConnection::xi2ProcessTouch(void *xiDevEvent, QXcbWindow *platformWindo { auto *xiDeviceEvent = reinterpret_cast<xcb_input_touch_begin_event_t *>(xiDevEvent); TouchDeviceData *dev = touchDeviceForId(xiDeviceEvent->sourceid); - Q_ASSERT(dev); + if (!dev) { + qCDebug(lcQpaXInputEvents) << "didn't find the dev for given sourceid - " << xiDeviceEvent->sourceid + << ", try to repopulate xi2 devices"; + xi2SetupDevices(); + dev = touchDeviceForId(xiDeviceEvent->sourceid); + if (!dev) { + qCDebug(lcQpaXInputEvents) << "still can't find the dev for it, give up."; + return; + } + } const bool firstTouch = dev->touchPoints.isEmpty(); if (xiDeviceEvent->event_type == XCB_INPUT_TOUCH_BEGIN) { QWindowSystemInterface::TouchPoint tp; @@ -792,7 +838,7 @@ void QXcbConnection::xi2ProcessTouch(void *xiDevEvent, QXcbWindow *platformWindo qreal nx = -1.0, ny = -1.0; qreal w = 0.0, h = 0.0; bool majorAxisIsY = touchPoint.area.height() > touchPoint.area.width(); - for (const TouchDeviceData::ValuatorClassInfo &vci : qAsConst(dev->valuatorInfo)) { + for (const TouchDeviceData::ValuatorClassInfo &vci : std::as_const(dev->valuatorInfo)) { double value; if (!xi2GetValuatorValueIfSet(xiDeviceEvent, vci.number, &value)) continue; @@ -804,27 +850,27 @@ void QXcbConnection::xi2ProcessTouch(void *xiDevEvent, QXcbWindow *platformWindo if (value < vci.min) value = vci.min; qreal valuatorNormalized = (value - vci.min) / (vci.max - vci.min); - if (vci.label == QXcbAtom::RelX) { + if (vci.label == QXcbAtom::AtomRelX) { nx = valuatorNormalized; - } else if (vci.label == QXcbAtom::RelY) { + } else if (vci.label == QXcbAtom::AtomRelY) { ny = valuatorNormalized; - } else if (vci.label == QXcbAtom::AbsX) { + } else if (vci.label == QXcbAtom::AtomAbsX) { nx = valuatorNormalized; - } else if (vci.label == QXcbAtom::AbsY) { + } else if (vci.label == QXcbAtom::AtomAbsY) { ny = valuatorNormalized; - } else if (vci.label == QXcbAtom::AbsMTPositionX) { + } else if (vci.label == QXcbAtom::AtomAbsMTPositionX) { nx = valuatorNormalized; - } else if (vci.label == QXcbAtom::AbsMTPositionY) { + } else if (vci.label == QXcbAtom::AtomAbsMTPositionY) { ny = valuatorNormalized; - } else if (vci.label == QXcbAtom::AbsMTTouchMajor) { + } else if (vci.label == QXcbAtom::AtomAbsMTTouchMajor) { const qreal sw = screen->geometry().width(); const qreal sh = screen->geometry().height(); w = valuatorNormalized * qHypot(sw, sh); - } else if (vci.label == QXcbAtom::AbsMTTouchMinor) { + } else if (vci.label == QXcbAtom::AtomAbsMTTouchMinor) { const qreal sw = screen->geometry().width(); const qreal sh = screen->geometry().height(); h = valuatorNormalized * qHypot(sw, sh); - } else if (vci.label == QXcbAtom::AbsMTOrientation) { + } else if (vci.label == QXcbAtom::AtomAbsMTOrientation) { // Find the closest axis. // 0 corresponds to the Y axis, vci.max to the X axis. // Flipping over the Y axis and rotating by 180 degrees @@ -835,7 +881,7 @@ void QXcbConnection::xi2ProcessTouch(void *xiDevEvent, QXcbWindow *platformWindo value -= 2 * vci.max; value = qAbs(value); majorAxisIsY = value < vci.max - value; - } else if (vci.label == QXcbAtom::AbsMTPressure || vci.label == QXcbAtom::AbsPressure) { + } else if (vci.label == QXcbAtom::AtomAbsMTPressure || vci.label == QXcbAtom::AtomAbsPressure) { touchPoint.pressure = valuatorNormalized; } @@ -967,7 +1013,7 @@ void QXcbConnection::abortSystemMoveResize(xcb_window_t window) qCDebug(lcQpaXInputDevices) << "sending client message NET_WM_MOVERESIZE_CANCEL to window: " << window; m_startSystemMoveResizeInfo.window = XCB_NONE; - const xcb_atom_t moveResize = connection()->atom(QXcbAtom::_NET_WM_MOVERESIZE); + const xcb_atom_t moveResize = connection()->atom(QXcbAtom::Atom_NET_WM_MOVERESIZE); xcb_client_message_event_t xev; xev.response_type = XCB_CLIENT_MESSAGE; xev.type = moveResize; @@ -1023,7 +1069,7 @@ bool QXcbConnection::xi2SetMouseGrabEnabled(xcb_window_t w, bool grab) } #endif // QT_CONFIG(gestures) && QT_XCB_HAS_TOUCHPAD_GESTURES - for (int id : qAsConst(m_xiMasterPointerIds)) { + for (int id : std::as_const(m_xiMasterPointerIds)) { xcb_generic_error_t *error = nullptr; auto cookie = xcb_input_xi_grab_device(xcb_connection(), w, XCB_CURRENT_TIME, XCB_CURSOR_NONE, id, XCB_INPUT_GRAB_MODE_22_ASYNC, XCB_INPUT_GRAB_MODE_22_ASYNC, @@ -1041,7 +1087,7 @@ bool QXcbConnection::xi2SetMouseGrabEnabled(xcb_window_t w, bool grab) free(reply); } } else { // ungrab - for (int id : qAsConst(m_xiMasterPointerIds)) { + for (int id : std::as_const(m_xiMasterPointerIds)) { auto cookie = xcb_input_xi_ungrab_device_checked(xcb_connection(), XCB_CURRENT_TIME, id); xcb_generic_error_t *error = xcb_request_check(xcb_connection(), cookie); if (error) { @@ -1065,11 +1111,15 @@ bool QXcbConnection::xi2SetMouseGrabEnabled(xcb_window_t w, bool grab) void QXcbConnection::xi2HandleHierarchyEvent(void *event) { auto *xiEvent = reinterpret_cast<xcb_input_hierarchy_event_t *>(event); - // We only care about hotplugged devices - if (!(xiEvent->flags & (XCB_INPUT_HIERARCHY_MASK_SLAVE_REMOVED | XCB_INPUT_HIERARCHY_MASK_SLAVE_ADDED))) - return; - - xi2SetupDevices(); + // We care about hotplugged devices (slaves) and master devices. + // We don't report anything for DEVICE_ENABLED or DEVICE_DISABLED + // (but often that goes with adding or removal anyway). + // We don't react to SLAVE_ATTACHED or SLAVE_DETACHED either. + if (xiEvent->flags & (XCB_INPUT_HIERARCHY_MASK_MASTER_ADDED | + XCB_INPUT_HIERARCHY_MASK_MASTER_REMOVED | + XCB_INPUT_HIERARCHY_MASK_SLAVE_REMOVED | + XCB_INPUT_HIERARCHY_MASK_SLAVE_ADDED)) + xi2SetupDevices(); } #if QT_XCB_HAS_TOUCHPAD_GESTURES @@ -1234,6 +1284,9 @@ void QXcbConnection::xi2HandleDeviceChangedEvent(void *event) auto *xiEvent = reinterpret_cast<xcb_input_device_changed_event_t *>(event); switch (xiEvent->reason) { case XCB_INPUT_CHANGE_REASON_DEVICE_CHANGE: { + // Don't call xi2SetupSlavePointerDevice() again for an already-known device, and never for a master. + if (m_xiMasterPointerIds.contains(xiEvent->deviceid) || m_xiSlavePointerIds.contains(xiEvent->deviceid)) + return; auto reply = Q_XCB_REPLY(xcb_input_xi_query_device, xcb_connection(), xiEvent->sourceid); if (!reply || reply->num_infos <= 0) return; @@ -1275,9 +1328,9 @@ void QXcbConnection::xi2UpdateScrollingDevice(QInputDevice *dev) if (classInfo->type == XCB_INPUT_DEVICE_CLASS_TYPE_VALUATOR) { auto *vci = reinterpret_cast<xcb_input_valuator_class_t *>(classInfo); const int valuatorAtom = qatom(vci->label); - if (valuatorAtom == QXcbAtom::RelHorizScroll || valuatorAtom == QXcbAtom::RelHorizWheel) + if (valuatorAtom == QXcbAtom::AtomRelHorizScroll || valuatorAtom == QXcbAtom::AtomRelHorizWheel) scrollingDevice->lastScrollPosition.setX(fixed3232ToReal(vci->value)); - else if (valuatorAtom == QXcbAtom::RelVertScroll || valuatorAtom == QXcbAtom::RelVertWheel) + else if (valuatorAtom == QXcbAtom::AtomRelVertScroll || valuatorAtom == QXcbAtom::AtomRelVertWheel) scrollingDevice->lastScrollPosition.setY(fixed3232ToReal(vci->value)); } } @@ -1464,7 +1517,7 @@ bool QXcbConnection::xi2HandleTabletEvent(const void *event, TabletData *tabletD // The evdev driver doesn't do it this way. const auto *ev = reinterpret_cast<const xcb_input_property_event_t *>(event); if (ev->what == XCB_INPUT_PROPERTY_FLAG_MODIFIED) { - if (ev->property == atom(QXcbAtom::WacomSerialIDs)) { + if (ev->property == atom(QXcbAtom::AtomWacomSerialIDs)) { enum WacomSerialIndex { _WACSER_USB_ID = 0, _WACSER_LAST_TOOL_SERIAL, @@ -1477,7 +1530,7 @@ bool QXcbConnection::xi2HandleTabletEvent(const void *event, TabletData *tabletD auto reply = Q_XCB_REPLY(xcb_input_xi_get_property, xcb_connection(), tabletData->deviceId, 0, ev->property, XCB_GET_PROPERTY_TYPE_ANY, 0, 100); if (reply) { - if (reply->type == atom(QXcbAtom::INTEGER) && reply->format == 32 && reply->num_items == _WACSER_COUNT) { + if (reply->type == atom(QXcbAtom::AtomINTEGER) && reply->format == 32 && reply->num_items == _WACSER_COUNT) { quint32 *ptr = reinterpret_cast<quint32 *>(xcb_input_xi_get_property_items(reply.get())); quint32 tool = ptr[_WACSER_TOOL_ID]; // Workaround for http://sourceforge.net/p/linuxwacom/bugs/246/ @@ -1485,6 +1538,7 @@ bool QXcbConnection::xi2HandleTabletEvent(const void *event, TabletData *tabletD if (!tool && ptr[_WACSER_TOOL_SERIAL]) tool = ptr[_WACSER_TOOL_SERIAL]; + QWindow *win = nullptr; // TODO QTBUG-111400 get the position somehow, then the window // The property change event informs us which tool is in proximity or which one left proximity. if (tool) { const QPointingDevice *dev = tabletToolInstance(nullptr, tabletData->name, @@ -1493,30 +1547,26 @@ bool QXcbConnection::xi2HandleTabletEvent(const void *event, TabletData *tabletD tabletData->inProximity = true; tabletData->tool = dev->type(); tabletData->serialId = qint64(ptr[_WACSER_TOOL_SERIAL]); - QWindowSystemInterface::handleTabletEnterProximityEvent(ev->time, - int(tabletData->tool), int(tabletData->pointerType), tabletData->serialId); + QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(win, ev->time, dev, true); // enter } else { tool = ptr[_WACSER_LAST_TOOL_ID]; // Workaround for http://sourceforge.net/p/linuxwacom/bugs/246/ // e.g. on Thinkpad Helix, tool ID will be 0 and serial will be 1 if (!tool) tool = ptr[_WACSER_LAST_TOOL_SERIAL]; - const QInputDevice *dev = QInputDevicePrivate::fromId(tabletData->deviceId); + auto *dev = qobject_cast<const QPointingDevice *>(QInputDevicePrivate::fromId(tabletData->deviceId)); Q_ASSERT(dev); tabletData->tool = dev->type(); tabletData->inProximity = false; tabletData->serialId = qint64(ptr[_WACSER_LAST_TOOL_SERIAL]); - // TODO why doesn't it just take QPointingDevice* - QWindowSystemInterface::handleTabletLeaveProximityEvent(ev->time, - int(tabletData->tool), int(tabletData->pointerType), tabletData->serialId); + QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(win, ev->time, dev, false); // leave } // TODO maybe have a hash of tabletData->deviceId to device data so we can // look up the tablet name here, and distinguish multiple tablets - if (Q_UNLIKELY(lcQpaXInputEvents().isDebugEnabled())) - qCDebug(lcQpaXInputDevices, "XI2 proximity change on tablet %d %s (USB %x): last tool: %x id %x current tool: %x id %x %s", - tabletData->deviceId, qPrintable(tabletData->name), ptr[_WACSER_USB_ID], - ptr[_WACSER_LAST_TOOL_SERIAL], ptr[_WACSER_LAST_TOOL_ID], - ptr[_WACSER_TOOL_SERIAL], ptr[_WACSER_TOOL_ID], toolName(tabletData->tool)); + qCDebug(lcQpaXInputDevices, "XI2 proximity change on tablet %d %s (USB %x): last tool: %x id %x current tool: %x id %x %s", + tabletData->deviceId, qPrintable(tabletData->name), ptr[_WACSER_USB_ID], + ptr[_WACSER_LAST_TOOL_SERIAL], ptr[_WACSER_LAST_TOOL_ID], + ptr[_WACSER_TOOL_SERIAL], ptr[_WACSER_TOOL_ID], toolName(tabletData->tool)); } } } @@ -1550,6 +1600,9 @@ void QXcbConnection::xi2ReportTabletEvent(const void *event, TabletData *tabletD double pressure = 0, rotation = 0, tangentialPressure = 0; int xTilt = 0, yTilt = 0; static const bool useValuators = !qEnvironmentVariableIsSet("QT_XCB_TABLET_LEGACY_COORDINATES"); + const QPointingDevice *dev = QPointingDevicePrivate::tabletDevice(QInputDevice::DeviceType(tabletData->tool), + QPointingDevice::PointerType(tabletData->pointerType), + QPointingDeviceUniqueId::fromNumericId(tabletData->serialId)); // Valuators' values are relative to the physical size of the current virtual // screen. Therefore we cannot use QScreen/QWindow geometry and should use @@ -1568,36 +1621,37 @@ void QXcbConnection::xi2ReportTabletEvent(const void *event, TabletData *tabletD xi2GetValuatorValueIfSet(event, classInfo.number, &classInfo.curVal); double normalizedValue = (classInfo.curVal - classInfo.minVal) / (classInfo.maxVal - classInfo.minVal); switch (valuator) { - case QXcbAtom::AbsX: + case QXcbAtom::AtomAbsX: if (Q_LIKELY(useValuators)) { const qreal value = scaleOneValuator(normalizedValue, physicalScreenArea.x(), physicalScreenArea.width()); global.setX(value); local.setX(xcbWindow->mapFromGlobalF(global).x()); } break; - case QXcbAtom::AbsY: + case QXcbAtom::AtomAbsY: if (Q_LIKELY(useValuators)) { qreal value = scaleOneValuator(normalizedValue, physicalScreenArea.y(), physicalScreenArea.height()); global.setY(value); local.setY(xcbWindow->mapFromGlobalF(global).y()); } break; - case QXcbAtom::AbsPressure: + case QXcbAtom::AtomAbsPressure: pressure = normalizedValue; break; - case QXcbAtom::AbsTiltX: + case QXcbAtom::AtomAbsTiltX: xTilt = classInfo.curVal; break; - case QXcbAtom::AbsTiltY: + case QXcbAtom::AtomAbsTiltY: yTilt = classInfo.curVal; break; - case QXcbAtom::AbsWheel: + case QXcbAtom::AtomAbsWheel: switch (tabletData->tool) { case QInputDevice::DeviceType::Airbrush: tangentialPressure = normalizedValue * 2.0 - 1.0; // Convert 0..1 range to -1..+1 range break; case QInputDevice::DeviceType::Stylus: - rotation = normalizedValue * 360.0 - 180.0; // Convert 0..1 range to -180..+180 degrees + if (dev->capabilities().testFlag(QInputDevice::Capability::Rotation)) + rotation = normalizedValue * 360.0 - 180.0; // Convert 0..1 range to -180..+180 degrees break; default: // Other types of styli do not use this valuator break; @@ -1616,16 +1670,15 @@ void QXcbConnection::xi2ReportTabletEvent(const void *event, TabletData *tabletD local.x(), local.y(), global.x(), global.y(), (int)tabletData->buttons, pressure, xTilt, yTilt, rotation, (int)modifiers); - QWindowSystemInterface::handleTabletEvent(window, ev->time, local, global, - int(tabletData->tool), int(tabletData->pointerType), + QWindowSystemInterface::handleTabletEvent(window, ev->time, dev, local, global, tabletData->buttons, pressure, xTilt, yTilt, tangentialPressure, - rotation, 0, tabletData->serialId, modifiers); + rotation, 0, modifiers); } QXcbConnection::TabletData *QXcbConnection::tabletDataForDevice(int id) { - for (int i = 0; i < m_tabletData.count(); ++i) { + for (int i = 0; i < m_tabletData.size(); ++i) { if (m_tabletData.at(i).deviceId == id) return &m_tabletData[i]; } diff --git a/src/plugins/platforms/xcb/qxcbcursor.cpp b/src/plugins/platforms/xcb/qxcbcursor.cpp index abda9c9d0a..dc9ed46956 100644 --- a/src/plugins/platforms/xcb/qxcbcursor.cpp +++ b/src/plugins/platforms/xcb/qxcbcursor.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qxcbcursor.h" @@ -7,13 +7,17 @@ #include "qxcbimage.h" #include "qxcbxsettings.h" -#if QT_CONFIG(library) -#include <QtCore/QLibrary> -#endif #include <QtGui/QWindow> #include <QtGui/QBitmap> #include <QtGui/private/qguiapplication_p.h> +#include <qpa/qplatformtheme.h> + +#if QT_CONFIG(xcb_xlib) #include <X11/cursorfont.h> +#else +#include "qxcbcursorfont.h" +#endif + #include <xcb/xfixes.h> #include <xcb/xcb_image.h> @@ -21,24 +25,6 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -typedef int (*PtrXcursorLibraryLoadCursor)(void *, const char *); -typedef char *(*PtrXcursorLibraryGetTheme)(void *); -typedef int (*PtrXcursorLibrarySetTheme)(void *, const char *); -typedef int (*PtrXcursorLibraryGetDefaultSize)(void *); - -#if QT_CONFIG(xcb_xlib) && QT_CONFIG(library) -#include <X11/Xlib.h> -enum { - XCursorShape = CursorShape -}; -#undef CursorShape - -static PtrXcursorLibraryLoadCursor ptrXcursorLibraryLoadCursor = nullptr; -static PtrXcursorLibraryGetTheme ptrXcursorLibraryGetTheme = nullptr; -static PtrXcursorLibrarySetTheme ptrXcursorLibrarySetTheme = nullptr; -static PtrXcursorLibraryGetDefaultSize ptrXcursorLibraryGetDefaultSize = nullptr; -#endif - static xcb_font_t cursorFont = 0; static int cursorCount = 0; @@ -255,10 +241,10 @@ QXcbCursorCacheKey::QXcbCursorCacheKey(const QCursor &c) if (pixmapCacheKey) { bitmapCacheKey = pixmapCacheKey; } else { - Q_ASSERT(!c.bitmap(Qt::ReturnByValue).isNull()); - Q_ASSERT(!c.mask(Qt::ReturnByValue).isNull()); - bitmapCacheKey = c.bitmap(Qt::ReturnByValue).cacheKey(); - maskCacheKey = c.mask(Qt::ReturnByValue).cacheKey(); + Q_ASSERT(!c.bitmap().isNull()); + Q_ASSERT(!c.mask().isNull()); + bitmapCacheKey = c.bitmap().cacheKey(); + maskCacheKey = c.mask().cacheKey(); } } } @@ -266,50 +252,28 @@ QXcbCursorCacheKey::QXcbCursorCacheKey(const QCursor &c) #endif // !QT_NO_CURSOR QXcbCursor::QXcbCursor(QXcbConnection *conn, QXcbScreen *screen) - : QXcbObject(conn), m_screen(screen), m_gtkCursorThemeInitialized(false) + : QXcbObject(conn), m_screen(screen), m_cursorContext(nullptr), m_callbackForPropertyRegistered(false) { #if QT_CONFIG(cursor) // see NUM_BITMAPS in libXcursor/src/xcursorint.h m_bitmapCache.setMaxCost(8); #endif + updateContext(); + if (cursorCount++) return; cursorFont = xcb_generate_id(xcb_connection()); const char *cursorStr = "cursor"; xcb_open_font(xcb_connection(), cursorFont, strlen(cursorStr), cursorStr); - -#if QT_CONFIG(xcb_xlib) && QT_CONFIG(library) - static bool function_ptrs_not_initialized = true; - if (function_ptrs_not_initialized) { - QLibrary xcursorLib("Xcursor"_L1, 1); - bool xcursorFound = xcursorLib.load(); - if (!xcursorFound) { // try without the version number - xcursorLib.setFileName("Xcursor"_L1); - xcursorFound = xcursorLib.load(); - } - if (xcursorFound) { - ptrXcursorLibraryLoadCursor = - (PtrXcursorLibraryLoadCursor) xcursorLib.resolve("XcursorLibraryLoadCursor"); - ptrXcursorLibraryGetTheme = - (PtrXcursorLibraryGetTheme) xcursorLib.resolve("XcursorGetTheme"); - ptrXcursorLibrarySetTheme = - (PtrXcursorLibrarySetTheme) xcursorLib.resolve("XcursorSetTheme"); - ptrXcursorLibraryGetDefaultSize = - (PtrXcursorLibraryGetDefaultSize) xcursorLib.resolve("XcursorGetDefaultSize"); - } - function_ptrs_not_initialized = false; - } - -#endif } QXcbCursor::~QXcbCursor() { xcb_connection_t *conn = xcb_connection(); - if (m_gtkCursorThemeInitialized) { + if (m_callbackForPropertyRegistered) { m_screen->xSettings()->removeCallbackForHandle(this); } @@ -317,9 +281,33 @@ QXcbCursor::~QXcbCursor() xcb_close_font(conn, cursorFont); #ifndef QT_NO_CURSOR - for (xcb_cursor_t cursor : qAsConst(m_cursorHash)) + for (xcb_cursor_t cursor : std::as_const(m_cursorHash)) xcb_free_cursor(conn, cursor); #endif + + if (m_cursorContext) + xcb_cursor_context_free(m_cursorContext); +} + +QSize QXcbCursor::size() const +{ + if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) + return theme->themeHint(QPlatformTheme::MouseCursorSize).toSize(); + return QSize(24, 24); +} + +void QXcbCursor::updateContext() +{ + if (m_cursorContext) + xcb_cursor_context_free(m_cursorContext); + + m_cursorContext = nullptr; + + xcb_connection_t *conn = xcb_connection(); + if (xcb_cursor_context_new(conn, m_screen->screen(), &m_cursorContext) < 0) { + qWarning() << "xcb: Could not initialize xcb-cursor"; + m_cursorContext = nullptr; + } } #ifndef QT_NO_CURSOR @@ -482,76 +470,39 @@ xcb_cursor_t QXcbCursor::createNonStandardCursor(int cshape) return cursor; } -#if QT_CONFIG(xcb_xlib) && QT_CONFIG(library) -bool updateCursorTheme(void *dpy, const QByteArray &theme) { - if (!ptrXcursorLibraryGetTheme - || !ptrXcursorLibrarySetTheme) - return false; - QByteArray oldTheme = ptrXcursorLibraryGetTheme(dpy); - if (oldTheme == theme) - return false; - - int setTheme = ptrXcursorLibrarySetTheme(dpy,theme.constData()); - return setTheme; -} - - void QXcbCursor::cursorThemePropertyChanged(QXcbVirtualDesktop *screen, const QByteArray &name, const QVariant &property, void *handle) +void QXcbCursor::cursorThemePropertyChanged(QXcbVirtualDesktop *screen, const QByteArray &name, const QVariant &property, void *handle) { Q_UNUSED(screen); Q_UNUSED(name); + Q_UNUSED(property); QXcbCursor *self = static_cast<QXcbCursor *>(handle); self->m_cursorHash.clear(); - - updateCursorTheme(self->connection()->xlib_display(),property.toByteArray()); + self->updateContext(); } -static xcb_cursor_t loadCursor(void *dpy, int cshape) -{ - xcb_cursor_t cursor = XCB_NONE; - if (!ptrXcursorLibraryLoadCursor || !dpy) - return cursor; - - for (const char *cursorName: cursorNames[cshape]) { - cursor = ptrXcursorLibraryLoadCursor(dpy, cursorName); - if (cursor != XCB_NONE) - break; - } - - return cursor; -} -#endif // QT_CONFIG(xcb_xlib) / QT_CONFIG(library) - xcb_cursor_t QXcbCursor::createFontCursor(int cshape) { + if (!m_cursorContext) + return XCB_NONE; + xcb_connection_t *conn = xcb_connection(); int cursorId = cursorIdForShape(cshape); xcb_cursor_t cursor = XCB_NONE; -#if QT_CONFIG(xcb_xlib) && QT_CONFIG(library) - if (m_screen->xSettings()->initialized()) - m_screen->xSettings()->registerCallbackForProperty("Gtk/CursorThemeName",cursorThemePropertyChanged,this); + if (!m_callbackForPropertyRegistered && m_screen->xSettings()->initialized()) { + m_screen->xSettings()->registerCallbackForProperty("Gtk/CursorThemeName", cursorThemePropertyChanged, this); + + m_callbackForPropertyRegistered = true; + } - // Try Xcursor first + // Try xcb-cursor first if (cshape >= 0 && cshape <= Qt::LastCursor) { - void *dpy = connection()->xlib_display(); - cursor = loadCursor(dpy, cshape); - if (!cursor && !m_gtkCursorThemeInitialized && m_screen->xSettings()->initialized()) { - QByteArray gtkCursorTheme = m_screen->xSettings()->setting("Gtk/CursorThemeName").toByteArray(); - if (updateCursorTheme(dpy,gtkCursorTheme)) { - cursor = loadCursor(dpy, cshape); - } - m_gtkCursorThemeInitialized = true; + for (const char *cursorName : cursorNames[cshape]) { + cursor = xcb_cursor_load_cursor(m_cursorContext, cursorName); + if (cursor != XCB_NONE) + return cursor; } } - if (cursor) - return cursor; - if (!cursor && cursorId) { - cursor = XCreateFontCursor(static_cast<Display *>(connection()->xlib_display()), cursorId); - if (cursor) - return cursor; - } - -#endif // Non-standard X11 cursors are created from bitmaps cursor = createNonStandardCursor(cshape); @@ -583,8 +534,8 @@ xcb_cursor_t QXcbCursor::createBitmapCursor(QCursor *cursor) qCWarning(lcQpaXcb, "xrender >= 0.5 required to create pixmap cursors"); } else { xcb_connection_t *conn = xcb_connection(); - xcb_pixmap_t cp = qt_xcb_XPixmapFromBitmap(m_screen, cursor->bitmap(Qt::ReturnByValue).toImage()); - xcb_pixmap_t mp = qt_xcb_XPixmapFromBitmap(m_screen, cursor->mask(Qt::ReturnByValue).toImage()); + xcb_pixmap_t cp = qt_xcb_XPixmapFromBitmap(m_screen, cursor->bitmap().toImage()); + xcb_pixmap_t mp = qt_xcb_XPixmapFromBitmap(m_screen, cursor->mask().toImage()); c = xcb_generate_id(conn); xcb_create_cursor(conn, c, cp, mp, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, spot.x(), spot.y()); diff --git a/src/plugins/platforms/xcb/qxcbcursor.h b/src/plugins/platforms/xcb/qxcbcursor.h index 83438321ef..bf26861e8f 100644 --- a/src/plugins/platforms/xcb/qxcbcursor.h +++ b/src/plugins/platforms/xcb/qxcbcursor.h @@ -1,4 +1,4 @@ -// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QXCBCURSOR_H @@ -6,6 +6,7 @@ #include <qpa/qplatformcursor.h> #include "qxcbscreen.h" +#include <xcb/xcb_cursor.h> #include <QtCore/QCache> @@ -47,6 +48,10 @@ public: QPoint pos() const override; void setPos(const QPoint &pos) override; + QSize size() const override; + + void updateContext(); + static void queryPointer(QXcbConnection *c, QXcbVirtualDesktop **virtualDesktop, QPoint *pos, int *keybMask = nullptr); #ifndef QT_NO_CURSOR @@ -75,17 +80,16 @@ private: #endif QXcbScreen *m_screen; + xcb_cursor_context_t *m_cursorContext; #ifndef QT_NO_CURSOR CursorHash m_cursorHash; BitmapCursorCache m_bitmapCache; #endif -#if QT_CONFIG(xcb_xlib) && QT_CONFIG(library) static void cursorThemePropertyChanged(QXcbVirtualDesktop *screen, const QByteArray &name, const QVariant &property, void *handle); -#endif - bool m_gtkCursorThemeInitialized; + bool m_callbackForPropertyRegistered; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/qxcbcursorfont.h b/src/plugins/platforms/xcb/qxcbcursorfont.h new file mode 100644 index 0000000000..fe74e27691 --- /dev/null +++ b/src/plugins/platforms/xcb/qxcbcursorfont.h @@ -0,0 +1,88 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// copied from <X11/cursorfont.h> + +#ifndef QXCBCURSORFONT_H +#define QXCBCURSORFONT_H + +#define XC_num_glyphs 154 +#define XC_X_cursor 0 +#define XC_arrow 2 +#define XC_based_arrow_down 4 +#define XC_based_arrow_up 6 +#define XC_boat 8 +#define XC_bogosity 10 +#define XC_bottom_left_corner 12 +#define XC_bottom_right_corner 14 +#define XC_bottom_side 16 +#define XC_bottom_tee 18 +#define XC_box_spiral 20 +#define XC_center_ptr 22 +#define XC_circle 24 +#define XC_clock 26 +#define XC_coffee_mug 28 +#define XC_cross 30 +#define XC_cross_reverse 32 +#define XC_crosshair 34 +#define XC_diamond_cross 36 +#define XC_dot 38 +#define XC_dotbox 40 +#define XC_double_arrow 42 +#define XC_draft_large 44 +#define XC_draft_small 46 +#define XC_draped_box 48 +#define XC_exchange 50 +#define XC_fleur 52 +#define XC_gobbler 54 +#define XC_gumby 56 +#define XC_hand1 58 +#define XC_hand2 60 +#define XC_heart 62 +#define XC_icon 64 +#define XC_iron_cross 66 +#define XC_left_ptr 68 +#define XC_left_side 70 +#define XC_left_tee 72 +#define XC_leftbutton 74 +#define XC_ll_angle 76 +#define XC_lr_angle 78 +#define XC_man 80 +#define XC_middlebutton 82 +#define XC_mouse 84 +#define XC_pencil 86 +#define XC_pirate 88 +#define XC_plus 90 +#define XC_question_arrow 92 +#define XC_right_ptr 94 +#define XC_right_side 96 +#define XC_right_tee 98 +#define XC_rightbutton 100 +#define XC_rtl_logo 102 +#define XC_sailboat 104 +#define XC_sb_down_arrow 106 +#define XC_sb_h_double_arrow 108 +#define XC_sb_left_arrow 110 +#define XC_sb_right_arrow 112 +#define XC_sb_up_arrow 114 +#define XC_sb_v_double_arrow 116 +#define XC_shuttle 118 +#define XC_sizing 120 +#define XC_spider 122 +#define XC_spraycan 124 +#define XC_star 126 +#define XC_target 128 +#define XC_tcross 130 +#define XC_top_left_arrow 132 +#define XC_top_left_corner 134 +#define XC_top_right_corner 136 +#define XC_top_side 138 +#define XC_top_tee 140 +#define XC_trek 142 +#define XC_ul_angle 144 +#define XC_umbrella 146 +#define XC_ur_angle 148 +#define XC_watch 150 +#define XC_xterm 152 + +#endif /* QXCBCURSORFONT_H */ diff --git a/src/plugins/platforms/xcb/qxcbdrag.cpp b/src/plugins/platforms/xcb/qxcbdrag.cpp index f5cb50d9c6..6e5dd770b9 100644 --- a/src/plugins/platforms/xcb/qxcbdrag.cpp +++ b/src/plugins/platforms/xcb/qxcbdrag.cpp @@ -5,6 +5,7 @@ #include <xcb/xcb.h> #include "qxcbconnection.h" #include "qxcbclipboard.h" +#include "qxcbkeyboard.h" #include "qxcbmime.h" #include "qxcbwindow.h" #include "qxcbscreen.h" @@ -27,6 +28,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + const int xdnd_version = 5; static inline xcb_window_t xcb_window(QPlatformWindow *w) @@ -44,7 +47,7 @@ static xcb_window_t xdndProxy(QXcbConnection *c, xcb_window_t w) xcb_window_t proxy = XCB_NONE; auto reply = Q_XCB_REPLY(xcb_get_property, c->xcb_connection(), - false, w, c->atom(QXcbAtom::XdndProxy), XCB_ATOM_WINDOW, 0, 1); + false, w, c->atom(QXcbAtom::AtomXdndProxy), XCB_ATOM_WINDOW, 0, 1); if (reply && reply->type == XCB_ATOM_WINDOW) proxy = *((xcb_window_t *)xcb_get_property_value(reply.get())); @@ -54,7 +57,7 @@ static xcb_window_t xdndProxy(QXcbConnection *c, xcb_window_t w) // exists and is real? reply = Q_XCB_REPLY(xcb_get_property, c->xcb_connection(), - false, proxy, c->atom(QXcbAtom::XdndProxy), XCB_ATOM_WINDOW, 0, 1); + false, proxy, c->atom(QXcbAtom::AtomXdndProxy), XCB_ATOM_WINDOW, 0, 1); if (reply && reply->type == XCB_ATOM_WINDOW) { xcb_window_t p = *((xcb_window_t *)xcb_get_property_value(reply.get())); @@ -140,7 +143,7 @@ void QXcbDrag::startDrag() qCDebug(lcQpaXDnd) << "starting drag where source:" << connection()->qtSelectionOwner(); xcb_set_selection_owner(xcb_connection(), connection()->qtSelectionOwner(), - atom(QXcbAtom::XdndSelection), connection()->time()); + atom(QXcbAtom::AtomXdndSelection), connection()->time()); QStringList fmts = QXcbMime::formatsHelper(drag()->mimeData()); for (int i = 0; i < fmts.size(); ++i) { @@ -153,7 +156,7 @@ void QXcbDrag::startDrag() if (drag_types.size() > 3) xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, connection()->qtSelectionOwner(), - atom(QXcbAtom::XdndTypelist), + atom(QXcbAtom::AtomXdndTypelist), XCB_ATOM_ATOM, 32, drag_types.size(), (const void *)drag_types.constData()); setUseCompositing(current_virtual_desktop->compositingActive()); @@ -187,7 +190,7 @@ Qt::DropAction QXcbDrag::defaultAction(Qt::DropActions possibleActions, Qt::Keyb void QXcbDrag::handlePropertyNotifyEvent(const xcb_property_notify_event_t *event) { - if (event->window != xdnd_dragsource || event->atom != atom(QXcbAtom::XdndActionList)) + if (event->window != xdnd_dragsource || event->atom != atom(QXcbAtom::AtomXdndActionList)) return; readActionList(); @@ -233,7 +236,7 @@ xcb_window_t QXcbDrag::findRealWindow(const QPoint & pos, xcb_window_t w, int md bool windowContainsMouse = !ignoreNonXdndAwareWindows; { auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), - false, w, connection()->atom(QXcbAtom::XdndAware), + false, w, connection()->atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); bool isAware = reply && reply->type != XCB_NONE; if (isAware) { @@ -306,7 +309,7 @@ bool QXcbDrag::findXdndAwareTarget(const QPoint &globalPos, xcb_window_t *target xcb_window_t child = translate->child; auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, target, - atom(QXcbAtom::XdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); + atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); bool aware = reply && reply->type != XCB_NONE; if (aware) { qCDebug(lcQpaXDnd) << "found XdndAware on" << target; @@ -379,7 +382,7 @@ void QXcbDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod if (proxy_target) { auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, proxy_target, - atom(QXcbAtom::XdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 1); + atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 1); if (!reply || reply->type == XCB_NONE) { target = 0; } else { @@ -404,7 +407,7 @@ void QXcbDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod enter.sequence = 0; enter.window = target; enter.format = 32; - enter.type = atom(QXcbAtom::XdndEnter); + enter.type = atom(QXcbAtom::AtomXdndEnter); enter.data.data32[0] = connection()->qtSelectionOwner(); enter.data.data32[1] = flags; enter.data.data32[2] = drag_types.size() > 0 ? drag_types.at(0) : 0; @@ -435,7 +438,7 @@ void QXcbDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod move.sequence = 0; move.window = target; move.format = 32; - move.type = atom(QXcbAtom::XdndPosition); + move.type = atom(QXcbAtom::AtomXdndPosition); move.data.data32[0] = connection()->qtSelectionOwner(); move.data.data32[1] = 0; // flags move.data.data32[2] = (globalPos.x() << 16) + globalPos.y(); @@ -459,7 +462,7 @@ void QXcbDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod static const bool isUnity = qgetenv("XDG_CURRENT_DESKTOP").toLower() == "unity"; if (isUnity && xdndCollectionWindow == XCB_NONE) { QString name = QXcbWindow::windowTitle(connection(), target); - if (name == QStringLiteral("XdndCollectionWindowImp")) + if (name == "XdndCollectionWindowImp"_L1) xdndCollectionWindow = target; } if (target == xdndCollectionWindow) { @@ -481,7 +484,7 @@ void QXcbDrag::drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod drop.sequence = 0; drop.window = current_target; drop.format = 32; - drop.type = atom(QXcbAtom::XdndDrop); + drop.type = atom(QXcbAtom::AtomXdndDrop); drop.data.data32[0] = connection()->qtSelectionOwner(); drop.data.data32[1] = 0; // flags drop.data.data32[2] = connection()->time(); @@ -521,11 +524,11 @@ void QXcbDrag::drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardMod Qt::DropAction QXcbDrag::toDropAction(xcb_atom_t a) const { - if (a == atom(QXcbAtom::XdndActionCopy) || a == 0) + if (a == atom(QXcbAtom::AtomXdndActionCopy) || a == 0) return Qt::CopyAction; - if (a == atom(QXcbAtom::XdndActionLink)) + if (a == atom(QXcbAtom::AtomXdndActionLink)) return Qt::LinkAction; - if (a == atom(QXcbAtom::XdndActionMove)) + if (a == atom(QXcbAtom::AtomXdndActionMove)) return Qt::MoveAction; return Qt::CopyAction; } @@ -534,7 +537,7 @@ Qt::DropActions QXcbDrag::toDropActions(const QList<xcb_atom_t> &atoms) const { Qt::DropActions actions; for (const auto actionAtom : atoms) { - if (actionAtom != atom(QXcbAtom::XdndActionAsk)) + if (actionAtom != atom(QXcbAtom::AtomXdndActionAsk)) actions |= toDropAction(actionAtom); } return actions; @@ -544,16 +547,16 @@ xcb_atom_t QXcbDrag::toXdndAction(Qt::DropAction a) const { switch (a) { case Qt::CopyAction: - return atom(QXcbAtom::XdndActionCopy); + return atom(QXcbAtom::AtomXdndActionCopy); case Qt::LinkAction: - return atom(QXcbAtom::XdndActionLink); + return atom(QXcbAtom::AtomXdndActionLink); case Qt::MoveAction: case Qt::TargetMoveAction: - return atom(QXcbAtom::XdndActionMove); + return atom(QXcbAtom::AtomXdndActionMove); case Qt::IgnoreAction: return XCB_NONE; default: - return atom(QXcbAtom::XdndActionCopy); + return atom(QXcbAtom::AtomXdndActionCopy); } } @@ -561,7 +564,7 @@ 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, + atom(QXcbAtom::AtomXdndActionList), XCB_ATOM_ATOM, 0, 1024); if (reply && reply->type != XCB_NONE && reply->format == 32) { int length = xcb_get_property_value_length(reply.get()) / 4; @@ -590,7 +593,7 @@ void QXcbDrag::setActionList(Qt::DropAction requestedAction, Qt::DropActions sup if (current_actions != actions) { xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, connection()->qtSelectionOwner(), - atom(QXcbAtom::XdndActionList), + atom(QXcbAtom::AtomXdndActionList), XCB_ATOM_ATOM, 32, actions.size(), actions.constData()); current_actions = actions; } @@ -614,7 +617,7 @@ void QXcbDrag::stopListeningForActionListChanges() int QXcbDrag::findTransactionByWindow(xcb_window_t window) { int at = -1; - for (int i = 0; i < transactions.count(); ++i) { + for (int i = 0; i < transactions.size(); ++i) { const Transaction &t = transactions.at(i); if (t.target == window || t.proxy_target == window) { at = i; @@ -627,7 +630,7 @@ int QXcbDrag::findTransactionByWindow(xcb_window_t window) int QXcbDrag::findTransactionByTime(xcb_timestamp_t timestamp) { int at = -1; - for (int i = 0; i < transactions.count(); ++i) { + for (int i = 0; i < transactions.size(); ++i) { const Transaction &t = transactions.at(i); if (t.timestamp == timestamp) { at = i; @@ -701,7 +704,7 @@ void QXcbDrag::handleEnter(QPlatformWindow *, const xcb_client_message_event_t * if (event->data.data32[1] & 1) { // get the types from XdndTypeList auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, xdnd_dragsource, - atom(QXcbAtom::XdndTypelist), XCB_ATOM_ATOM, + atom(QXcbAtom::AtomXdndTypelist), XCB_ATOM_ATOM, 0, xdnd_max_type); if (reply && reply->type != XCB_NONE && reply->format == 32) { int length = xcb_get_property_value_length(reply.get()) / 4; @@ -720,7 +723,7 @@ void QXcbDrag::handleEnter(QPlatformWindow *, const xcb_client_message_event_t * xdnd_types.append(event->data.data32[i]); } } - for(int i = 0; i < xdnd_types.length(); ++i) + for(int i = 0; i < xdnd_types.size(); ++i) qCDebug(lcQpaXDnd) << " " << connection()->atomName(xdnd_types.at(i)); } @@ -734,7 +737,7 @@ void QXcbDrag::handle_xdnd_position(QPlatformWindow *w, const xcb_client_message QPoint p((e->data.data32[2] & 0xffff0000) >> 16, e->data.data32[2] & 0x0000ffff); Q_ASSERT(w); QRect geometry = w->geometry(); - p -= geometry.topLeft(); + p -= w->isEmbedded() ? w->mapToGlobal(geometry.topLeft()) : geometry.topLeft(); if (!w || !w->window() || (w->window()->type() == Qt::Desktop)) return; @@ -761,12 +764,12 @@ void QXcbDrag::handle_xdnd_position(QPlatformWindow *w, const xcb_client_message } else { dropData = m_dropData; supported_actions = toDropActions(drop_actions); - if (e->data.data32[4] != atom(QXcbAtom::XdndActionAsk)) + if (e->data.data32[4] != atom(QXcbAtom::AtomXdndActionAsk)) supported_actions |= Qt::DropActions(toDropAction(e->data.data32[4])); } auto buttons = currentDrag() ? b : connection()->queryMouseButtons(); - auto modifiers = currentDrag() ? mods : connection()->queryKeyboardModifiers(); + auto modifiers = currentDrag() ? mods : connection()->keyboard()->queryKeyboardModifiers(); QPlatformDragQtResponse qt_response = QWindowSystemInterface::handleDrag( w->window(), dropData, p, supported_actions, buttons, modifiers); @@ -783,7 +786,7 @@ void QXcbDrag::handle_xdnd_position(QPlatformWindow *w, const xcb_client_message response.sequence = 0; response.window = xdnd_dragsource; response.format = 32; - response.type = atom(QXcbAtom::XdndStatus); + response.type = atom(QXcbAtom::AtomXdndStatus); response.data.data32[0] = xcb_window(w); response.data.data32[1] = qt_response.isAccepted(); // flags response.data.data32[2] = 0; // x, y @@ -835,7 +838,7 @@ namespace void QXcbDrag::handlePosition(QPlatformWindow * w, const xcb_client_message_event_t *event) { xcb_client_message_event_t *lastEvent = const_cast<xcb_client_message_event_t *>(event); - ClientMessageScanner scanner(atom(QXcbAtom::XdndPosition)); + ClientMessageScanner scanner(atom(QXcbAtom::AtomXdndPosition)); while (auto nextEvent = connection()->eventQueue()->peek(scanner)) { if (lastEvent != event) free(lastEvent); @@ -883,7 +886,7 @@ void QXcbDrag::handleStatus(const xcb_client_message_event_t *event) xcb_client_message_event_t *lastEvent = const_cast<xcb_client_message_event_t *>(event); xcb_generic_event_t *nextEvent; - ClientMessageScanner scanner(atom(QXcbAtom::XdndStatus)); + ClientMessageScanner scanner(atom(QXcbAtom::AtomXdndStatus)); while ((nextEvent = connection()->eventQueue()->peek(scanner))) { if (lastEvent != event) free(lastEvent); @@ -934,7 +937,7 @@ void QXcbDrag::send_leave() leave.sequence = 0; leave.window = current_target; leave.format = 32; - leave.type = atom(QXcbAtom::XdndLeave); + leave.type = atom(QXcbAtom::AtomXdndLeave); leave.data.data32[0] = connection()->qtSelectionOwner(); leave.data.data32[1] = 0; // flags leave.data.data32[2] = 0; // x, y @@ -980,40 +983,54 @@ void QXcbDrag::handleDrop(QPlatformWindow *, const xcb_client_message_event_t *e Qt::DropActions supported_drop_actions; QMimeData *dropData = nullptr; + // this could be a same-application drop, just proxied due to + // some XEMBEDding, so try to find the real QMimeData used + // based on the timestamp for this drop. + int at = findTransactionByTime(target_time); + if (at != -1) { + qCDebug(lcQpaXDnd) << "found one transaction via findTransactionByTime()"; + dropData = transactions.at(at).drag->mimeData(); + // Can't use the source QMimeData if we need the image conversion code from xdndObtainData + if (dropData && dropData->hasImage()) + dropData = 0; + } + // if we can't find it, then use the data in the drag manager if (currentDrag()) { - dropData = currentDrag()->mimeData(); + if (!dropData) + dropData = currentDrag()->mimeData(); supported_drop_actions = Qt::DropActions(l[4]); } else { - dropData = m_dropData; + if (!dropData) + dropData = m_dropData; supported_drop_actions = accepted_drop_action | toDropActions(drop_actions); } if (!dropData) return; - // ### - // int at = findXdndDropTransactionByTime(target_time); - // if (at != -1) - // dropData = QDragManager::dragPrivate(X11->dndDropTransactions.at(at).object)->data; - // if we can't find it, then use the data in the drag manager auto buttons = currentDrag() ? b : connection()->queryMouseButtons(); - auto modifiers = currentDrag() ? mods : connection()->queryKeyboardModifiers(); + auto modifiers = currentDrag() ? mods : connection()->keyboard()->queryKeyboardModifiers(); QPlatformDropQtResponse response = QWindowSystemInterface::handleDrop( currentWindow.data(), dropData, currentPosition, supported_drop_actions, buttons, modifiers); - setExecutedDropAction(response.acceptedAction()); + Qt::DropAction acceptedAaction = response.acceptedAction(); + if (!response.isAccepted()) { + // Ignore a failed drag + acceptedAaction = Qt::IgnoreAction; + } + setExecutedDropAction(acceptedAaction); xcb_client_message_event_t finished = {}; finished.response_type = XCB_CLIENT_MESSAGE; finished.sequence = 0; finished.window = xdnd_dragsource; finished.format = 32; - finished.type = atom(QXcbAtom::XdndFinished); + finished.type = atom(QXcbAtom::AtomXdndFinished); finished.data.data32[0] = currentWindow ? xcb_window(currentWindow.data()) : XCB_NONE; finished.data.data32[1] = response.isAccepted(); // flags - finished.data.data32[2] = toXdndAction(response.acceptedAction()); + finished.data.data32[2] = toXdndAction(acceptedAaction); qCDebug(lcQpaXDnd) << "sending XdndFinished to source:" << xdnd_dragsource; @@ -1075,7 +1092,7 @@ void QXcbDrag::timerEvent(QTimerEvent* e) { if (e->timerId() == cleanup_timer) { bool stopTimer = true; - for (int i = 0; i < transactions.count(); ++i) { + for (int i = 0; i < transactions.size(); ++i) { const Transaction &t = transactions.at(i); if (t.targetWindow) { // dnd within the same process, don't delete, these are taken care of @@ -1083,7 +1100,7 @@ void QXcbDrag::timerEvent(QTimerEvent* e) continue; } QTime currentTime = QTime::currentTime(); - int delta = t.time.msecsTo(currentTime); + std::chrono::milliseconds delta{t.time.msecsTo(currentTime)}; if (delta > XdndDropTransactionTimeout) { /* delete transactions which are older than XdndDropTransactionTimeout. It could mean one of these: @@ -1127,7 +1144,7 @@ static xcb_window_t findXdndAwareParent(QXcbConnection *c, xcb_window_t window) forever { // check if window has XdndAware auto gpReply = Q_XCB_REPLY(xcb_get_property, c->xcb_connection(), false, window, - c->atom(QXcbAtom::XdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); + c->atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); bool aware = gpReply && gpReply->type != XCB_NONE; if (aware) { target = window; @@ -1217,6 +1234,7 @@ void QXcbDrag::handleSelectionRequest(const xcb_selection_request_event_t *event bool QXcbDrag::dndEnable(QXcbWindow *w, bool on) { + qCDebug(lcQpaXDnd) << "dndEnable" << static_cast<QPlatformWindow *>(w) << on; // Windows announce that they support the XDND protocol by creating a window property XdndAware. if (on) { QXcbWindow *window = nullptr; @@ -1233,7 +1251,7 @@ bool QXcbDrag::dndEnable(QXcbWindow *w, bool on) desktop_proxy = new QWindow; window = static_cast<QXcbWindow *>(desktop_proxy->handle()); proxy_id = window->xcb_window(); - xcb_atom_t xdnd_proxy = atom(QXcbAtom::XdndProxy); + xcb_atom_t xdnd_proxy = atom(QXcbAtom::AtomXdndProxy); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, w->xcb_window(), xdnd_proxy, XCB_ATOM_WINDOW, 32, 1, &proxy_id); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, proxy_id, xdnd_proxy, @@ -1247,14 +1265,14 @@ bool QXcbDrag::dndEnable(QXcbWindow *w, bool on) qCDebug(lcQpaXDnd) << "setting XdndAware for" << window->xcb_window(); xcb_atom_t atm = xdnd_version; xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window->xcb_window(), - atom(QXcbAtom::XdndAware), XCB_ATOM_ATOM, 32, 1, &atm); + atom(QXcbAtom::AtomXdndAware), XCB_ATOM_ATOM, 32, 1, &atm); return true; } else { return false; } } else { if (w->window()->type() == Qt::Desktop) { - xcb_delete_property(xcb_connection(), w->xcb_window(), atom(QXcbAtom::XdndProxy)); + xcb_delete_property(xcb_connection(), w->xcb_window(), atom(QXcbAtom::AtomXdndProxy)); delete desktop_proxy; desktop_proxy = nullptr; } else { @@ -1306,10 +1324,10 @@ QVariant QXcbDropData::xdndObtainData(const QByteArray &format, QMetaType reques return result; #ifndef QT_NO_CLIPBOARD - if (c->selectionOwner(c->atom(QXcbAtom::XdndSelection)) == XCB_NONE) + if (c->selectionOwner(c->atom(QXcbAtom::AtomXdndSelection)) == XCB_NONE) return result; // should never happen? - xcb_atom_t xdnd_selection = c->atom(QXcbAtom::XdndSelection); + xcb_atom_t xdnd_selection = c->atom(QXcbAtom::AtomXdndSelection); result = c->clipboard()->getSelection(xdnd_selection, a, xdnd_selection, drag->targetTime()); #endif diff --git a/src/plugins/platforms/xcb/qxcbdrag.h b/src/plugins/platforms/xcb/qxcbdrag.h index a58b7e850e..ae7cc915c8 100644 --- a/src/plugins/platforms/xcb/qxcbdrag.h +++ b/src/plugins/platforms/xcb/qxcbdrag.h @@ -14,7 +14,6 @@ #include <qpoint.h> #include <qpointer.h> #include <qrect.h> -#include <qsharedpointer.h> #include <qxcbobject.h> #include <QtCore/QDebug> @@ -128,7 +127,7 @@ private: QXcbVirtualDesktop *current_virtual_desktop; // 10 minute timer used to discard old XdndDrop transactions - enum { XdndDropTransactionTimeout = 600000 }; + static constexpr std::chrono::minutes XdndDropTransactionTimeout{10}; int cleanup_timer; QList<xcb_atom_t> drag_types; diff --git a/src/plugins/platforms/xcb/qxcbeventqueue.cpp b/src/plugins/platforms/xcb/qxcbeventqueue.cpp index 5c87cba80d..33795d63cf 100644 --- a/src/plugins/platforms/xcb/qxcbeventqueue.cpp +++ b/src/plugins/platforms/xcb/qxcbeventqueue.cpp @@ -345,7 +345,7 @@ void QXcbEventQueue::sendCloseConnectionEvent() const event.format = 32; event.sequence = 0; event.window = window; - event.type = m_connection->atom(QXcbAtom::_QT_CLOSE_CONNECTION); + event.type = m_connection->atom(QXcbAtom::Atom_QT_CLOSE_CONNECTION); event.data.data32[0] = 0; xcb_send_event(c, false, window, XCB_EVENT_MASK_NO_EVENT, reinterpret_cast<const char *>(&event)); @@ -357,7 +357,7 @@ bool QXcbEventQueue::isCloseConnectionEvent(const xcb_generic_event_t *event) { if (event && (event->response_type & ~0x80) == XCB_CLIENT_MESSAGE) { auto clientMessage = reinterpret_cast<const xcb_client_message_event_t *>(event); - if (clientMessage->type == m_connection->atom(QXcbAtom::_QT_CLOSE_CONNECTION)) + if (clientMessage->type == m_connection->atom(QXcbAtom::Atom_QT_CLOSE_CONNECTION)) m_closeConnectionDetected = true; } return m_closeConnectionDetected; diff --git a/src/plugins/platforms/xcb/qxcbimage.h b/src/plugins/platforms/xcb/qxcbimage.h index e550102881..c022fae639 100644 --- a/src/plugins/platforms/xcb/qxcbimage.h +++ b/src/plugins/platforms/xcb/qxcbimage.h @@ -5,7 +5,6 @@ #define QXCBIMAGE_H #include "qxcbscreen.h" -#include <QtCore/QPair> #include <QtGui/QImage> #include <QtGui/QPixmap> #include <xcb/xcb_image.h> diff --git a/src/plugins/platforms/xcb/qxcbintegration.cpp b/src/plugins/platforms/xcb/qxcbintegration.cpp index 8b49fb73ca..4dafae31e3 100644 --- a/src/plugins/platforms/xcb/qxcbintegration.cpp +++ b/src/plugins/platforms/xcb/qxcbintegration.cpp @@ -93,10 +93,17 @@ static bool runningUnderDebugger() #endif } +class QXcbUnixServices : public QGenericUnixServices +{ +public: + QString portalWindowIdentifier(QWindow *window) override; +}; + + QXcbIntegration *QXcbIntegration::m_instance = nullptr; QXcbIntegration::QXcbIntegration(const QStringList ¶meters, int &argc, char **argv) - : m_services(new QGenericUnixServices) + : m_services(new QXcbUnixServices) , m_instanceName(nullptr) , m_canGrab(true) , m_defaultVisualId(UINT_MAX) @@ -197,7 +204,7 @@ QPlatformPixmap *QXcbIntegration::createPlatformPixmap(QPlatformPixmap::PixelTyp QPlatformWindow *QXcbIntegration::createPlatformWindow(QWindow *window) const { QXcbGlIntegration *glIntegration = nullptr; - const bool isTrayIconWindow = QXcbWindow::isTrayIconWindow(window);; + const bool isTrayIconWindow = QXcbWindow::isTrayIconWindow(window); if (window->type() != Qt::Desktop && !isTrayIconWindow) { if (window->supportsOpenGL()) { glIntegration = connection()->glIntegration(); @@ -325,19 +332,38 @@ QAbstractEventDispatcher *QXcbIntegration::createEventDispatcher() const return QXcbEventDispatcher::createEventDispatcher(connection()); } +using namespace Qt::Literals::StringLiterals; +static const auto xsNetCursorBlink = "Net/CursorBlink"_ba; +static const auto xsNetCursorBlinkTime = "Net/CursorBlinkTime"_ba; +static const auto xsNetDoubleClickTime = "Net/DoubleClickTime"_ba; +static const auto xsNetDoubleClickDistance = "Net/DoubleClickDistance"_ba; +static const auto xsNetDndDragThreshold = "Net/DndDragThreshold"_ba; + void QXcbIntegration::initialize() { const auto defaultInputContext = "compose"_L1; // Perform everything that may potentially need the event dispatcher (timers, socket // notifiers) here instead of the constructor. - QString icStr = QPlatformInputContextFactory::requested(); - if (icStr.isNull()) - icStr = defaultInputContext; - m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); - if (!m_inputContext && icStr != defaultInputContext && icStr != "none"_L1) + auto icStrs = QPlatformInputContextFactory::requested(); + if (icStrs.isEmpty()) + icStrs = { defaultInputContext }; + m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); + if (!m_inputContext && !icStrs.contains(defaultInputContext) + && icStrs != QStringList{"none"_L1}) m_inputContext.reset(QPlatformInputContextFactory::create(defaultInputContext)); connection()->keyboard()->initialize(); + + auto notifyThemeChanged = [](QXcbVirtualDesktop *, const QByteArray &, const QVariant &, void *) { + QWindowSystemInterface::handleThemeChange(); + }; + + auto *xsettings = connection()->primaryScreen()->xSettings(); + xsettings->registerCallbackForProperty(xsNetCursorBlink, notifyThemeChanged, this); + xsettings->registerCallbackForProperty(xsNetCursorBlinkTime, notifyThemeChanged, this); + xsettings->registerCallbackForProperty(xsNetDoubleClickTime, notifyThemeChanged, this); + xsettings->registerCallbackForProperty(xsNetDoubleClickDistance, notifyThemeChanged, this); + xsettings->registerCallbackForProperty(xsNetDndDragThreshold, notifyThemeChanged, this); } void QXcbIntegration::moveToScreen(QWindow *window, int screen) @@ -404,14 +430,9 @@ QPlatformServices *QXcbIntegration::services() const return m_services.data(); } -Qt::KeyboardModifiers QXcbIntegration::queryKeyboardModifiers() const +QPlatformKeyMapper *QXcbIntegration::keyMapper() const { - return m_connection->queryKeyboardModifiers(); -} - -QList<int> QXcbIntegration::possibleKeys(const QKeyEvent *e) const -{ - return m_connection->keyboard()->possibleKeys(e); + return m_connection->keyboard(); } QStringList QXcbIntegration::themeNames() const @@ -435,17 +456,17 @@ QVariant QXcbIntegration::styleHint(QPlatformIntegration::StyleHint hint) const case QPlatformIntegration::CursorFlashTime: { bool ok = false; // If cursor blinking is off, returns 0 to keep the cursor awlays display. - if (connection()->primaryScreen()->xSettings()->setting(QByteArrayLiteral("Net/CursorBlink")).toInt(&ok) == 0 && ok) + if (connection()->primaryScreen()->xSettings()->setting(xsNetCursorBlink).toInt(&ok) == 0 && ok) return 0; - RETURN_VALID_XSETTINGS(QByteArrayLiteral("Net/CursorBlinkTime")); + RETURN_VALID_XSETTINGS(xsNetCursorBlinkTime); break; } case QPlatformIntegration::MouseDoubleClickInterval: - RETURN_VALID_XSETTINGS(QByteArrayLiteral("Net/DoubleClickTime")); + RETURN_VALID_XSETTINGS(xsNetDoubleClickTime); break; case QPlatformIntegration::MouseDoubleClickDistance: - RETURN_VALID_XSETTINGS(QByteArrayLiteral("Net/DoubleClickDistance")); + RETURN_VALID_XSETTINGS(xsNetDoubleClickDistance); break; case QPlatformIntegration::KeyboardInputInterval: case QPlatformIntegration::StartDragTime: @@ -454,21 +475,23 @@ QVariant QXcbIntegration::styleHint(QPlatformIntegration::StyleHint hint) const case QPlatformIntegration::StartDragVelocity: case QPlatformIntegration::UseRtlExtensions: case QPlatformIntegration::PasswordMaskCharacter: + case QPlatformIntegration::FlickMaximumVelocity: + case QPlatformIntegration::FlickDeceleration: // TODO using various xcb, gnome or KDE settings break; // Not implemented, use defaults + case QPlatformIntegration::FlickStartDistance: case QPlatformIntegration::StartDragDistance: { - RETURN_VALID_XSETTINGS(QByteArrayLiteral("Net/DndDragThreshold")); - + RETURN_VALID_XSETTINGS(xsNetDndDragThreshold); // The default (in QPlatformTheme::defaultThemeHint) is 10 pixels, but // on a high-resolution screen it makes sense to increase it. - qreal dpi = 100.0; + qreal dpi = 100; if (const QXcbScreen *screen = connection()->primaryScreen()) { if (screen->logicalDpi().first > dpi) dpi = screen->logicalDpi().first; if (screen->logicalDpi().second > dpi) dpi = screen->logicalDpi().second; } - return 10.0 * dpi / 100.0; + return (hint == QPlatformIntegration::FlickStartDistance ? qreal(15) : qreal(10)) * dpi / qreal(100); } case QPlatformIntegration::ShowIsFullScreen: // X11 always has support for windows, but the @@ -567,4 +590,15 @@ QPlatformVulkanInstance *QXcbIntegration::createPlatformVulkanInstance(QVulkanIn } #endif +void QXcbIntegration::setApplicationBadge(qint64 number) +{ + auto unixServices = dynamic_cast<QGenericUnixServices *>(services()); + unixServices->setApplicationBadge(number); +} + +QString QXcbUnixServices::portalWindowIdentifier(QWindow *window) +{ + return "x11:"_L1 + QString::number(window->winId(), 16); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/xcb/qxcbintegration.h b/src/plugins/platforms/xcb/qxcbintegration.h index abdfe7112c..a1e0c3f3e1 100644 --- a/src/plugins/platforms/xcb/qxcbintegration.h +++ b/src/plugins/platforms/xcb/qxcbintegration.h @@ -74,8 +74,7 @@ public: QPlatformServices *services() const override; - Qt::KeyboardModifiers queryKeyboardModifiers() const override; - QList<int> possibleKeys(const QKeyEvent *e) const override; + QPlatformKeyMapper *keyMapper() const override; QStringList themeNames() const override; QPlatformTheme *createPlatformTheme(const QString &name) const override; @@ -102,6 +101,8 @@ public: static QXcbIntegration *instance() { return m_instance; } + void setApplicationBadge(qint64 number) override; + private: QXcbConnection *m_connection = nullptr; diff --git a/src/plugins/platforms/xcb/qxcbkeyboard.cpp b/src/plugins/platforms/xcb/qxcbkeyboard.cpp index 0666fac735..17da54bc7a 100644 --- a/src/plugins/platforms/xcb/qxcbkeyboard.cpp +++ b/src/plugins/platforms/xcb/qxcbkeyboard.cpp @@ -3,6 +3,7 @@ #include "qxcbkeyboard.h" #include "qxcbwindow.h" #include "qxcbscreen.h" +#include "qxcbcursor.h" #include <qpa/qwindowsysteminterface.h> #include <qpa/qplatforminputcontext.h> @@ -377,9 +378,18 @@ void QXcbKeyboard::updateKeymap() QXkbCommon::verifyHasLatinLayout(m_xkbKeymap.get()); } -QList<int> QXcbKeyboard::possibleKeys(const QKeyEvent *event) const +QList<QKeyCombination> QXcbKeyboard::possibleKeyCombinations(const QKeyEvent *event) const { - return QXkbCommon::possibleKeys(m_xkbState.get(), event, m_superAsMeta, m_hyperAsMeta); + return QXkbCommon::possibleKeyCombinations( + m_xkbState.get(), event, m_superAsMeta, m_hyperAsMeta); +} + +Qt::KeyboardModifiers QXcbKeyboard::queryKeyboardModifiers() const +{ + // FIXME: Should we base this on m_xkbState? + int stateMask = 0; + QXcbCursor::queryPointer(connection(), nullptr, nullptr, &stateMask); + return translateModifiers(stateMask); } void QXcbKeyboard::updateXKBState(xcb_xkb_state_notify_event_t *state) diff --git a/src/plugins/platforms/xcb/qxcbkeyboard.h b/src/plugins/platforms/xcb/qxcbkeyboard.h index 15b08fbead..62d9268c64 100644 --- a/src/plugins/platforms/xcb/qxcbkeyboard.h +++ b/src/plugins/platforms/xcb/qxcbkeyboard.h @@ -14,11 +14,13 @@ #include <QtGui/private/qxkbcommon_p.h> #include <xkbcommon/xkbcommon-x11.h> +#include <qpa/qplatformkeymapper.h> + #include <QEvent> QT_BEGIN_NAMESPACE -class QXcbKeyboard : public QXcbObject +class QXcbKeyboard : public QXcbObject, public QPlatformKeyMapper { public: QXcbKeyboard(QXcbConnection *connection); @@ -34,7 +36,9 @@ public: Qt::KeyboardModifiers translateModifiers(int s) const; void updateKeymap(xcb_mapping_notify_event_t *event); void updateKeymap(); - QList<int> possibleKeys(const QKeyEvent *event) const; + + QList<QKeyCombination> possibleKeyCombinations(const QKeyEvent *event) const override; + Qt::KeyboardModifiers queryKeyboardModifiers() const override; void updateXKBMods(); xkb_mod_mask_t xkbModMask(quint16 state); diff --git a/src/plugins/platforms/xcb/qxcbmime.cpp b/src/plugins/platforms/xcb/qxcbmime.cpp index 7161af279c..860d195d13 100644 --- a/src/plugins/platforms/xcb/qxcbmime.cpp +++ b/src/plugins/platforms/xcb/qxcbmime.cpp @@ -27,8 +27,8 @@ QString QXcbMime::mimeAtomToString(QXcbConnection *connection, xcb_atom_t a) // special cases for string type if (a == XCB_ATOM_STRING - || a == connection->atom(QXcbAtom::UTF8_STRING) - || a == connection->atom(QXcbAtom::TEXT)) + || a == connection->atom(QXcbAtom::AtomUTF8_STRING) + || a == connection->atom(QXcbAtom::AtomTEXT)) return "text/plain"_L1; // special case for images @@ -54,15 +54,15 @@ bool QXcbMime::mimeDataForAtom(QXcbConnection *connection, xcb_atom_t a, QMimeDa *atomFormat = a; *dataFormat = 8; - if ((a == connection->atom(QXcbAtom::UTF8_STRING) + if ((a == connection->atom(QXcbAtom::AtomUTF8_STRING) || a == XCB_ATOM_STRING - || a == connection->atom(QXcbAtom::TEXT)) + || a == connection->atom(QXcbAtom::AtomTEXT)) && QInternalMimeData::hasFormatHelper("text/plain"_L1, mimeData)) { - if (a == connection->atom(QXcbAtom::UTF8_STRING)) { + if (a == connection->atom(QXcbAtom::AtomUTF8_STRING)) { *data = QInternalMimeData::renderDataHelper("text/plain"_L1, mimeData); ret = true; } else if (a == XCB_ATOM_STRING || - a == connection->atom(QXcbAtom::TEXT)) { + a == connection->atom(QXcbAtom::AtomTEXT)) { // ICCCM says STRING is latin1 *data = QString::fromUtf8(QInternalMimeData::renderDataHelper( "text/plain"_L1, mimeData)).toLatin1(); @@ -79,8 +79,7 @@ bool QXcbMime::mimeDataForAtom(QXcbConnection *connection, xcb_atom_t a, QMimeDa if (atomName == "text/uri-list"_L1 && connection->atomName(a) == "text/x-moz-url") { const QString mozUri = QLatin1StringView(data->split('\n').constFirst()) + u'\n'; - *data = QByteArray(reinterpret_cast<const char *>(mozUri.data()), - mozUri.length() * 2); + data->assign({reinterpret_cast<const char *>(mozUri.data()), mozUri.size() * 2}); } else if (atomName == "application/x-color"_L1) *dataFormat = 16; ret = true; @@ -102,9 +101,9 @@ QList<xcb_atom_t> QXcbMime::mimeAtomsForFormat(QXcbConnection *connection, const // special cases for strings if (format == "text/plain"_L1) { - atoms.append(connection->atom(QXcbAtom::UTF8_STRING)); + atoms.append(connection->atom(QXcbAtom::AtomUTF8_STRING)); atoms.append(XCB_ATOM_STRING); - atoms.append(connection->atom(QXcbAtom::TEXT)); + atoms.append(connection->atom(QXcbAtom::AtomTEXT)); } // special cases for uris @@ -139,11 +138,11 @@ QVariant QXcbMime::mimeConvertToFormat(QXcbConnection *connection, xcb_atom_t a, if (format == "text/plain"_L1) { if (data.endsWith('\0')) data.chop(1); - if (a == connection->atom(QXcbAtom::UTF8_STRING)) { + if (a == connection->atom(QXcbAtom::AtomUTF8_STRING)) { return QString::fromUtf8(data); } if (a == XCB_ATOM_STRING || - a == connection->atom(QXcbAtom::TEXT)) + a == connection->atom(QXcbAtom::AtomTEXT)) return QString::fromLatin1(data); } // If data contains UTF16 text, convert it to a string. @@ -156,7 +155,7 @@ QVariant QXcbMime::mimeConvertToFormat(QXcbConnection *connection, xcb_atom_t a, const quint8 byte1 = data.at(1); if ((byte0 == 0xff && byte1 == 0xfe) || (byte0 == 0xfe && byte1 == 0xff) || (byte0 != 0 && byte1 == 0) || (byte0 == 0 && byte1 != 0)) { - const QString str = QString::fromUtf16( + const QStringView str( reinterpret_cast<const char16_t *>(data.constData()), data.size() / 2); if (!str.isNull()) { if (format == "text/uri-list"_L1) { @@ -175,7 +174,7 @@ QVariant QXcbMime::mimeConvertToFormat(QXcbConnection *connection, xcb_atom_t a, return list.constFirst(); return list; } else { - return str; + return str.toString(); } } } @@ -227,12 +226,12 @@ xcb_atom_t QXcbMime::mimeAtomForFormat(QXcbConnection *connection, const QString // find matches for string types if (format == "text/plain"_L1) { - if (atoms.contains(connection->atom(QXcbAtom::UTF8_STRING))) - return connection->atom(QXcbAtom::UTF8_STRING); + if (atoms.contains(connection->atom(QXcbAtom::AtomUTF8_STRING))) + return connection->atom(QXcbAtom::AtomUTF8_STRING); if (atoms.contains(XCB_ATOM_STRING)) return XCB_ATOM_STRING; - if (atoms.contains(connection->atom(QXcbAtom::TEXT))) - return connection->atom(QXcbAtom::TEXT); + if (atoms.contains(connection->atom(QXcbAtom::AtomTEXT))) + return connection->atom(QXcbAtom::AtomTEXT); } // find matches for uri types diff --git a/src/plugins/platforms/xcb/qxcbnativeinterface.cpp b/src/plugins/platforms/xcb/qxcbnativeinterface.cpp index cd6ff40df9..06f5241d8c 100644 --- a/src/plugins/platforms/xcb/qxcbnativeinterface.cpp +++ b/src/plugins/platforms/xcb/qxcbnativeinterface.cpp @@ -350,7 +350,7 @@ void *QXcbNativeInterface::atspiBus() QXcbIntegration *integration = static_cast<QXcbIntegration *>(QGuiApplicationPrivate::platformIntegration()); QXcbConnection *connection = integration->connection(); if (connection) { - auto atspiBusAtom = connection->atom(QXcbAtom::AT_SPI_BUS); + auto atspiBusAtom = connection->atom(QXcbAtom::AtomAT_SPI_BUS); auto reply = Q_XCB_REPLY(xcb_get_property, connection->xcb_connection(), false, connection->rootWindow(), atspiBusAtom, XCB_ATOM_STRING, 0, 128); diff --git a/src/plugins/platforms/xcb/qxcbscreen.cpp b/src/plugins/platforms/xcb/qxcbscreen.cpp index 2a538c9b6e..06f4b66edb 100644 --- a/src/plugins/platforms/xcb/qxcbscreen.cpp +++ b/src/plugins/platforms/xcb/qxcbscreen.cpp @@ -50,7 +50,7 @@ QXcbVirtualDesktop::QXcbVirtualDesktop(QXcbConnection *connection, xcb_screen_t auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, xcb_connection(), false, screen->root, - atom(QXcbAtom::_NET_SUPPORTING_WM_CHECK), + atom(QXcbAtom::Atom_NET_SUPPORTING_WM_CHECK), XCB_ATOM_WINDOW, 0, 1024); if (reply && reply->format == 32 && reply->type == XCB_ATOM_WINDOW) { xcb_window_t windowManager = *((xcb_window_t *)xcb_get_property_value(reply.get())); @@ -92,7 +92,7 @@ QXcbVirtualDesktop::~QXcbVirtualDesktop() { delete m_xSettings; - for (auto cmap : qAsConst(m_visualColormaps)) + for (auto cmap : std::as_const(m_visualColormaps)) xcb_free_colormap(xcb_connection(), cmap); } @@ -222,7 +222,7 @@ void QXcbVirtualDesktop::handleScreenChange(xcb_randr_screen_change_notify_event case XCB_RANDR_ROTATION_REFLECT_Y: break; } - for (QPlatformScreen *platformScreen : qAsConst(m_screens)) { + for (QPlatformScreen *platformScreen : std::as_const(m_screens)) { QDpi ldpi = platformScreen->logicalDpi(); QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(platformScreen->screen(), ldpi.first, ldpi.second); } @@ -249,7 +249,7 @@ QRect QXcbVirtualDesktop::getWorkArea() const { QRect r; auto workArea = Q_XCB_REPLY_UNCHECKED(xcb_get_property, xcb_connection(), false, screen()->root, - atom(QXcbAtom::_NET_WORKAREA), + atom(QXcbAtom::Atom_NET_WORKAREA), XCB_ATOM_CARDINAL, 0, 1024); if (workArea && workArea->type == XCB_ATOM_CARDINAL && workArea->format == 32 && workArea->value_len >= 4) { // If workArea->value_len > 4, the remaining ones seem to be for WM's virtual desktops @@ -271,7 +271,7 @@ void QXcbVirtualDesktop::updateWorkArea() QRect workArea = getWorkArea(); if (m_workArea != workArea) { m_workArea = workArea; - for (QPlatformScreen *screen : qAsConst(m_screens)) + for (QPlatformScreen *screen : std::as_const(m_screens)) ((QXcbScreen *)screen)->updateAvailableGeometry(); } } @@ -529,7 +529,7 @@ void QXcbScreen::updateColorSpaceAndEdid() // Read colord ICC data (from GNOME settings) auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, xcb_connection(), false, screen()->root, - connection()->atom(QXcbAtom::_ICC_PROFILE), + connection()->atom(QXcbAtom::Atom_ICC_PROFILE), XCB_ATOM_CARDINAL, 0, 8192); if (reply->format == 8 && reply->type == XCB_ATOM_CARDINAL) { QByteArray data(reinterpret_cast<const char *>(xcb_get_property_value(reply.get())), reply->value_len); @@ -556,11 +556,11 @@ void QXcbScreen::updateColorSpaceAndEdid() m_edid.greenChromaticity, m_edid.blueChromaticity, QColorSpace::TransferFunction::Gamma, m_edid.gamma); } else { - if (m_edid.tables.length() == 1) { + if (m_edid.tables.size() == 1) { m_colorSpace = QColorSpace(m_edid.whiteChromaticity, m_edid.redChromaticity, m_edid.greenChromaticity, m_edid.blueChromaticity, m_edid.tables[0]); - } else if (m_edid.tables.length() == 3) { + } else if (m_edid.tables.size() == 3) { m_colorSpace = QColorSpace(m_edid.whiteChromaticity, m_edid.redChromaticity, m_edid.greenChromaticity, m_edid.blueChromaticity, m_edid.tables[0], m_edid.tables[1], m_edid.tables[2]); @@ -796,7 +796,7 @@ void QXcbScreen::windowShown(QXcbWindow *window) // Freedesktop.org Startup Notification if (!connection()->startupId().isEmpty() && window->window()->isTopLevel()) { sendStartupMessage(QByteArrayLiteral("remove: ID=") + connection()->startupId()); - connection()->clearStartupId(); + connection()->setStartupId({}); } } @@ -817,15 +817,15 @@ void QXcbScreen::sendStartupMessage(const QByteArray &message) const xcb_client_message_event_t ev; ev.response_type = XCB_CLIENT_MESSAGE; ev.format = 8; - ev.type = connection()->atom(QXcbAtom::_NET_STARTUP_INFO_BEGIN); + ev.type = connection()->atom(QXcbAtom::Atom_NET_STARTUP_INFO_BEGIN); ev.sequence = 0; ev.window = rootWindow; int sent = 0; - int length = message.length() + 1; // include NUL byte + int length = message.size() + 1; // include NUL byte const char *data = message.constData(); do { if (sent == 20) - ev.type = connection()->atom(QXcbAtom::_NET_STARTUP_INFO); + ev.type = connection()->atom(QXcbAtom::Atom_NET_STARTUP_INFO); const int start = sent; const int numBytes = qMin(length - start, 20); @@ -839,7 +839,7 @@ void QXcbScreen::sendStartupMessage(const QByteArray &message) const QRect QXcbScreen::availableGeometry() const { static bool enforceNetWorkarea = !qEnvironmentVariableIsEmpty("QT_RELY_ON_NET_WORKAREA_ATOM"); - bool isMultiHeadSystem = virtualSiblings().length() > 1; + bool isMultiHeadSystem = virtualSiblings().size() > 1; bool useScreenGeometry = isMultiHeadSystem && !enforceNetWorkarea; return useScreenGeometry ? m_geometry : m_availableGeometry; } @@ -1095,11 +1095,11 @@ QByteArray QXcbScreen::getEdid() const return result; // Try a bunch of atoms - result = getOutputProperty(atom(QXcbAtom::EDID)); + result = getOutputProperty(atom(QXcbAtom::AtomEDID)); if (result.isEmpty()) - result = getOutputProperty(atom(QXcbAtom::EDID_DATA)); + result = getOutputProperty(atom(QXcbAtom::AtomEDID_DATA)); if (result.isEmpty()) - result = getOutputProperty(atom(QXcbAtom::XFree86_DDC_EDID1_RAWDATA)); + result = getOutputProperty(atom(QXcbAtom::AtomXFree86_DDC_EDID1_RAWDATA)); return result; } diff --git a/src/plugins/platforms/xcb/qxcbsessionmanager.cpp b/src/plugins/platforms/xcb/qxcbsessionmanager.cpp index ec305d5030..b4e28ab831 100644 --- a/src/plugins/platforms/xcb/qxcbsessionmanager.cpp +++ b/src/plugins/platforms/xcb/qxcbsessionmanager.cpp @@ -101,19 +101,19 @@ static void sm_setProperty(const QString &name, const QString &value) { QByteArray v = value.toUtf8(); SmPropValue prop; - prop.length = v.length(); + prop.length = v.size(); prop.value = (SmPointer) const_cast<char *>(v.constData()); sm_setProperty(name.toLatin1().data(), SmARRAY8, 1, &prop); } static void sm_setProperty(const QString &name, const QStringList &value) { - SmPropValue *prop = new SmPropValue[value.count()]; + SmPropValue *prop = new SmPropValue[value.size()]; int count = 0; QList<QByteArray> vl; vl.reserve(value.size()); for (QStringList::ConstIterator it = value.begin(); it != value.end(); ++it) { - prop[count].length = (*it).length(); + prop[count].length = (*it).size(); vl.append((*it).toUtf8()); prop[count].value = (char*)vl.constLast().data(); ++count; diff --git a/src/plugins/platforms/xcb/qxcbsystemtraytracker.cpp b/src/plugins/platforms/xcb/qxcbsystemtraytracker.cpp index e184c07128..84d2d73f91 100644 --- a/src/plugins/platforms/xcb/qxcbsystemtraytracker.cpp +++ b/src/plugins/platforms/xcb/qxcbsystemtraytracker.cpp @@ -26,7 +26,7 @@ enum { QXcbSystemTrayTracker *QXcbSystemTrayTracker::create(QXcbConnection *connection) { // Selection, tray atoms for GNOME, NET WM Specification - const xcb_atom_t trayAtom = connection->atom(QXcbAtom::_NET_SYSTEM_TRAY_OPCODE); + const xcb_atom_t trayAtom = connection->atom(QXcbAtom::Atom_NET_SYSTEM_TRAY_OPCODE); if (!trayAtom) return nullptr; const QByteArray netSysTray = QByteArrayLiteral("_NET_SYSTEM_TRAY_S") + QByteArray::number(connection->primaryScreenNumber()); @@ -113,7 +113,7 @@ xcb_visualid_t QXcbSystemTrayTracker::netSystemTrayVisual() if (m_trayWindow == XCB_WINDOW_NONE) return XCB_NONE; - xcb_atom_t tray_atom = m_connection->atom(QXcbAtom::_NET_SYSTEM_TRAY_VISUAL); + xcb_atom_t tray_atom = m_connection->atom(QXcbAtom::Atom_NET_SYSTEM_TRAY_VISUAL); // Get the xcb property for the _NET_SYSTEM_TRAY_VISUAL atom auto systray_atom_reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, m_connection->xcb_connection(), diff --git a/src/plugins/platforms/xcb/qxcbwindow.cpp b/src/plugins/platforms/xcb/qxcbwindow.cpp index a37c03cc60..d3e4fa9548 100644 --- a/src/plugins/platforms/xcb/qxcbwindow.cpp +++ b/src/plugins/platforms/xcb/qxcbwindow.cpp @@ -6,6 +6,7 @@ #include <QtDebug> #include <QMetaEnum> #include <QScreen> +#include <QtCore/QFileInfo> #include <QtGui/QIcon> #include <QtGui/QRegion> #include <QtGui/private/qhighdpiscaling_p.h> @@ -59,6 +60,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window"); +Q_LOGGING_CATEGORY(lcQpaXcbWindow, "qt.qpa.xcb.window"); Q_DECLARE_TYPEINFO(xcb_rectangle_t, Q_PRIMITIVE_TYPE); @@ -94,12 +96,14 @@ const quint32 XEMBED_VERSION = 0; QXcbScreen *QXcbWindow::parentScreen() { - return parent() ? static_cast<QXcbWindow*>(parent())->parentScreen() : xcbScreen(); + return QPlatformWindow::parent() ? static_cast<QXcbWindow*>(QPlatformWindow::parent())->parentScreen() : xcbScreen(); } -//QPlatformWindow::screenForGeometry version that uses deviceIndependentGeometry QXcbScreen *QXcbWindow::initialScreen() const { + // Resolve initial screen via QWindowPrivate::screenForGeometry(), + // which works in platform independent coordinates, as opposed to + // QPlatformWindow::screenForGeometry() that uses native coordinates. QWindowPrivate *windowPrivate = qt_window_private(window()); QScreen *screen = windowPrivate->screenForGeometry(window()->geometry()); return static_cast<QXcbScreen*>(screen->handle()); @@ -131,6 +135,7 @@ void QXcbWindow::setImageFormatForVisual(const xcb_visualtype_t *visual) case 16: qWarning("Using RGB16 fallback, if this works your X11 server is reporting a bad screen format."); m_imageFormat = QImage::Format_RGB16; + break; default: break; } @@ -166,7 +171,7 @@ static inline XTextProperty* qstringToXTP(Display *dpy, const QString& s) tp.value = (uchar*)qcs.data(); tp.encoding = XA_STRING; tp.format = 8; - tp.nitems = qcs.length(); + tp.nitems = qcs.size(); free_prop = false; } return &tp; @@ -221,6 +226,7 @@ enum : quint32 { void QXcbWindow::create() { + xcb_window_t old_m_window = m_window; destroy(); m_windowState = Qt::WindowNoState; @@ -229,8 +235,8 @@ void QXcbWindow::create() Qt::WindowType type = window()->type(); QXcbScreen *currentScreen = xcbScreen(); - QXcbScreen *platformScreen = parent() ? parentScreen() : initialScreen(); - QRect rect = parent() + QXcbScreen *platformScreen = QPlatformWindow::parent() ? parentScreen() : initialScreen(); + QRect rect = QPlatformWindow::parent() ? QHighDpi::toNativeLocalPosition(window()->geometry(), platformScreen) : QHighDpi::toNativePixels(window()->geometry(), platformScreen); @@ -253,11 +259,6 @@ void QXcbWindow::create() return; } - QPlatformWindow::setGeometry(rect); - - if (platformScreen != currentScreen) - QWindowSystemInterface::handleWindowScreenChanged(window(), platformScreen->QPlatformScreen::screen()); - const QSize minimumSize = windowMinimumSize(); if (rect.width() > 0 || rect.height() > 0) { rect.setWidth(qBound(1, rect.width(), XCOORD_MAX)); @@ -269,12 +270,17 @@ void QXcbWindow::create() rect.setHeight(QHighDpi::toNativePixels(int(defaultWindowHeight), platformScreen->QPlatformScreen::screen())); } + QPlatformWindow::setGeometry(rect); + + if (platformScreen != currentScreen) + QWindowSystemInterface::handleWindowScreenChanged(window(), platformScreen->QPlatformScreen::screen()); + xcb_window_t xcb_parent_id = platformScreen->root(); - if (parent()) { - xcb_parent_id = static_cast<QXcbWindow *>(parent())->xcb_window(); - m_embedded = parent()->isForeignWindow(); + if (QPlatformWindow::parent()) { + xcb_parent_id = static_cast<QXcbWindow *>(QPlatformWindow::parent())->xcb_window(); + m_embedded = QPlatformWindow::parent()->isForeignWindow(); - QSurfaceFormat parentFormat = parent()->window()->requestedFormat(); + QSurfaceFormat parentFormat = QPlatformWindow::parent()->window()->requestedFormat(); if (window()->surfaceType() != QSurface::OpenGLSurface && parentFormat.hasAlpha()) { window()->setFormat(parentFormat); } @@ -292,16 +298,16 @@ void QXcbWindow::create() qWarning() << "Failed to use requested visual id."; } - if (parent()) { + if (QPlatformWindow::parent()) { // When using a Vulkan QWindow via QWidget::createWindowContainer() we // must make sure the visuals are compatible. Now, the parent will be // of RasterGLSurface which typically chooses a GLX/EGL compatible // visual which may not be what the Vulkan window would choose. // Therefore, take the parent's visual. if (window()->surfaceType() == QSurface::VulkanSurface - && parent()->window()->surfaceType() != QSurface::VulkanSurface) + && QPlatformWindow::parent()->window()->surfaceType() != QSurface::VulkanSurface) { - visual = platformScreen->visualForId(static_cast<QXcbWindow *>(parent())->visualId()); + visual = platformScreen->visualForId(static_cast<QXcbWindow *>(QPlatformWindow::parent())->visualId()); } } @@ -358,20 +364,20 @@ void QXcbWindow::create() xcb_atom_t properties[5]; int propertyCount = 0; - properties[propertyCount++] = atom(QXcbAtom::WM_DELETE_WINDOW); - properties[propertyCount++] = atom(QXcbAtom::WM_TAKE_FOCUS); - properties[propertyCount++] = atom(QXcbAtom::_NET_WM_PING); + properties[propertyCount++] = atom(QXcbAtom::AtomWM_DELETE_WINDOW); + properties[propertyCount++] = atom(QXcbAtom::AtomWM_TAKE_FOCUS); + properties[propertyCount++] = atom(QXcbAtom::Atom_NET_WM_PING); if (connection()->hasXSync()) - properties[propertyCount++] = atom(QXcbAtom::_NET_WM_SYNC_REQUEST); + properties[propertyCount++] = atom(QXcbAtom::Atom_NET_WM_SYNC_REQUEST); if (window()->flags() & Qt::WindowContextHelpButtonHint) - properties[propertyCount++] = atom(QXcbAtom::_NET_WM_CONTEXT_HELP); + properties[propertyCount++] = atom(QXcbAtom::Atom_NET_WM_CONTEXT_HELP); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::WM_PROTOCOLS), + atom(QXcbAtom::AtomWM_PROTOCOLS), XCB_ATOM_ATOM, 32, propertyCount, @@ -382,10 +388,35 @@ void QXcbWindow::create() const QByteArray wmClass = QXcbIntegration::instance()->wmClass(); if (!wmClass.isEmpty()) { xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, - m_window, atom(QXcbAtom::WM_CLASS), + m_window, atom(QXcbAtom::AtomWM_CLASS), XCB_ATOM_STRING, 8, wmClass.size(), wmClass.constData()); } + QString desktopFileName = QGuiApplication::desktopFileName(); + if (QGuiApplication::desktopFileName().isEmpty()) { + QFileInfo fi = QFileInfo(QCoreApplication::instance()->applicationFilePath()); + QStringList domainName = + QCoreApplication::instance()->organizationDomain().split(QLatin1Char('.'), + Qt::SkipEmptyParts); + + if (domainName.isEmpty()) { + desktopFileName = fi.baseName(); + } else { + for (int i = 0; i < domainName.size(); ++i) + desktopFileName.prepend(QLatin1Char('.')).prepend(domainName.at(i)); + desktopFileName.append(fi.baseName()); + } + } + if (!desktopFileName.isEmpty()) { + const QByteArray dfName = desktopFileName.toUtf8(); + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, + m_window, atom(QXcbAtom::Atom_KDE_NET_WM_DESKTOP_FILE), + atom(QXcbAtom::AtomUTF8_STRING), 8, dfName.size(), dfName.constData()); + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, + m_window, atom(QXcbAtom::Atom_GTK_APPLICATION_ID), + atom(QXcbAtom::AtomUTF8_STRING), 8, dfName.size(), dfName.constData()); + } + if (connection()->hasXSync()) { m_syncCounter = xcb_generate_id(xcb_connection()); xcb_sync_create_counter(xcb_connection(), m_syncCounter, m_syncValue); @@ -393,7 +424,7 @@ void QXcbWindow::create() xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_SYNC_REQUEST_COUNTER), + atom(QXcbAtom::Atom_NET_WM_SYNC_REQUEST_COUNTER), XCB_ATOM_CARDINAL, 32, 1, @@ -403,13 +434,13 @@ void QXcbWindow::create() // set the PID to let the WM kill the application if unresponsive quint32 pid = getpid(); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_PID), XCB_ATOM_CARDINAL, 32, + atom(QXcbAtom::Atom_NET_WM_PID), XCB_ATOM_CARDINAL, 32, 1, &pid); const QByteArray clientMachine = QSysInfo::machineHostName().toLocal8Bit(); if (!clientMachine.isEmpty()) { xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::WM_CLIENT_MACHINE), XCB_ATOM_STRING, 8, + atom(QXcbAtom::AtomWM_CLIENT_MACHINE), XCB_ATOM_STRING, 8, clientMachine.size(), clientMachine.constData()); } @@ -423,14 +454,14 @@ void QXcbWindow::create() xcb_window_t leader = connection()->clientLeader(); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::WM_CLIENT_LEADER), XCB_ATOM_WINDOW, 32, + atom(QXcbAtom::AtomWM_CLIENT_LEADER), XCB_ATOM_WINDOW, 32, 1, &leader); /* Add XEMBED info; this operation doesn't initiate the embedding. */ quint32 data[] = { XEMBED_VERSION, XEMBED_MAPPED }; xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_XEMBED_INFO), - atom(QXcbAtom::_XEMBED_INFO), + atom(QXcbAtom::Atom_XEMBED_INFO), + atom(QXcbAtom::Atom_XEMBED_INFO), 32, 2, (void *)data); if (connection()->hasXInput2()) @@ -463,6 +494,17 @@ void QXcbWindow::create() if (m_trayIconWindow) m_embedded = requestSystemTrayWindowDock(); + + if (m_window != old_m_window) { + if (!m_wmTransientForChildren.isEmpty()) { + QList<QPointer<QXcbWindow>> transientChildren = m_wmTransientForChildren; + m_wmTransientForChildren.clear(); + for (auto transientChild : transientChildren) { + if (transientChild) + transientChild->updateWmTransientFor(); + } + } + } } QXcbWindow::~QXcbWindow() @@ -470,6 +512,22 @@ QXcbWindow::~QXcbWindow() destroy(); } +QXcbForeignWindow::QXcbForeignWindow(QWindow *window, WId nativeHandle) + : QXcbWindow(window) +{ + m_window = nativeHandle; + + // Reflect the foreign window's geometry as our own + if (auto geometry = Q_XCB_REPLY(xcb_get_geometry, xcb_connection(), m_window)) { + QRect nativeGeometry(geometry->x, geometry->y, geometry->width, geometry->height); + QPlatformWindow::setGeometry(nativeGeometry); + } + + // And reparent, if we have a parent already + if (QPlatformWindow::parent()) + setParent(QPlatformWindow::parent()); +} + QXcbForeignWindow::~QXcbForeignWindow() { // Clear window so that destroy() does not affect it @@ -487,12 +545,14 @@ void QXcbWindow::destroy() doFocusOut(); if (connection()->mouseGrabber() == this) connection()->setMouseGrabber(nullptr); + if (connection()->mousePressWindow() == this) + connection()->setMousePressWindow(nullptr); if (m_syncCounter && connection()->hasXSync()) xcb_sync_destroy_counter(xcb_connection(), m_syncCounter); if (m_window) { if (m_netWmUserTimeWindow) { - xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::_NET_WM_USER_TIME_WINDOW)); + xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::Atom_NET_WM_USER_TIME_WINDOW)); // Some window managers, like metacity, do XSelectInput on the _NET_WM_USER_TIME_WINDOW window, // without trapping BadWindow (which crashes when the user time window is destroyed). connection()->sync(); @@ -520,7 +580,7 @@ void QXcbWindow::setGeometry(const QRect &rect) propagateSizeHints(); QXcbScreen *currentScreen = xcbScreen(); - QXcbScreen *newScreen = parent() ? parentScreen() : static_cast<QXcbScreen*>(screenForGeometry(rect)); + QXcbScreen *newScreen = QPlatformWindow::parent() ? parentScreen() : static_cast<QXcbScreen*>(screenForGeometry(rect)); if (!newScreen) newScreen = xcbScreen(); @@ -561,9 +621,9 @@ void QXcbWindow::setGeometry(const QRect &rect) QMargins QXcbWindow::frameMargins() const { if (m_dirtyFrameMargins) { - if (connection()->wmSupport()->isSupportedByWM(atom(QXcbAtom::_NET_FRAME_EXTENTS))) { + if (connection()->wmSupport()->isSupportedByWM(atom(QXcbAtom::Atom_NET_FRAME_EXTENTS))) { auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, m_window, - atom(QXcbAtom::_NET_FRAME_EXTENTS), XCB_ATOM_CARDINAL, 0, 4); + atom(QXcbAtom::Atom_NET_FRAME_EXTENTS), XCB_ATOM_CARDINAL, 0, 4); if (reply && reply->type == XCB_ATOM_CARDINAL && reply->format == 32 && reply->value_len == 4) { quint32 *data = (quint32 *)xcb_get_property_value(reply.get()); // _NET_FRAME_EXTENTS format is left, right, top, bottom @@ -639,6 +699,44 @@ void QXcbWindow::setVisible(bool visible) hide(); } +void QXcbWindow::updateWmTransientFor() +{ + xcb_window_t transientXcbParent = XCB_NONE; + if (isTransient(window())) { + QWindow *tp = window()->transientParent(); + if (tp && tp->handle()) { + QXcbWindow *handle = static_cast<QXcbWindow *>(tp->handle()); + transientXcbParent = tp->handle()->winId(); + if (transientXcbParent) { + handle->registerWmTransientForChild(this); + qCDebug(lcQpaXcbWindow) << Q_FUNC_INFO << static_cast<QPlatformWindow *>(handle) + << " registerWmTransientForChild " << static_cast<QPlatformWindow *>(this); + } + } + // Default to client leader if there is no transient parent, else modal dialogs can + // be hidden by their parents. + if (!transientXcbParent) + transientXcbParent = connection()->clientLeader(); + if (transientXcbParent) { // ICCCM 4.1.2.6 + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, + XCB_ATOM_WM_TRANSIENT_FOR, XCB_ATOM_WINDOW, 32, + 1, &transientXcbParent); + qCDebug(lcQpaXcbWindow, "0x%x added XCB_ATOM_WM_TRANSIENT_FOR 0x%x", m_window, transientXcbParent); + } + } + if (!transientXcbParent) + xcb_delete_property(xcb_connection(), m_window, XCB_ATOM_WM_TRANSIENT_FOR); +} + +void QXcbWindow::registerWmTransientForChild(QXcbWindow *child) +{ + if (!child) + return; + + if (!m_wmTransientForChildren.contains(child)) + m_wmTransientForChildren.append(child); +} + void QXcbWindow::show() { if (window()->isTopLevel()) { @@ -652,23 +750,7 @@ void QXcbWindow::show() propagateSizeHints(); // update WM_TRANSIENT_FOR - xcb_window_t transientXcbParent = 0; - if (isTransient(window())) { - const QWindow *tp = window()->transientParent(); - if (tp && tp->handle()) - transientXcbParent = tp->handle()->winId(); - // Default to client leader if there is no transient parent, else modal dialogs can - // be hidden by their parents. - if (!transientXcbParent) - transientXcbParent = connection()->clientLeader(); - if (transientXcbParent) { // ICCCM 4.1.2.6 - xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - XCB_ATOM_WM_TRANSIENT_FOR, XCB_ATOM_WINDOW, 32, - 1, &transientXcbParent); - } - } - if (!transientXcbParent) - xcb_delete_property(xcb_connection(), m_window, XCB_ATOM_WM_TRANSIENT_FOR); + updateWmTransientFor(); // update _NET_WM_STATE setNetWmStateOnUnmappedWindow(); @@ -779,7 +861,7 @@ void QXcbWindow::doFocusIn() return; QWindow *w = static_cast<QWindowPrivate *>(QObjectPrivate::get(window()))->eventReceiver(); connection()->setFocusWindow(w); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(w, Qt::ActiveWindowFocusReason); } void QXcbWindow::doFocusOut() @@ -822,29 +904,29 @@ QXcbWindow::NetWmStates QXcbWindow::netWmStates() NetWmStates result; auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, xcb_connection(), - 0, m_window, atom(QXcbAtom::_NET_WM_STATE), + 0, m_window, atom(QXcbAtom::Atom_NET_WM_STATE), XCB_ATOM_ATOM, 0, 1024); if (reply && reply->format == 32 && reply->type == XCB_ATOM_ATOM) { const xcb_atom_t *states = static_cast<const xcb_atom_t *>(xcb_get_property_value(reply.get())); const xcb_atom_t *statesEnd = states + reply->length; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_ABOVE))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_ABOVE))) result |= NetWmStateAbove; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_BELOW))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_BELOW))) result |= NetWmStateBelow; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_FULLSCREEN))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_FULLSCREEN))) result |= NetWmStateFullScreen; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_HORZ))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_HORZ))) result |= NetWmStateMaximizedHorz; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_VERT))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT))) result |= NetWmStateMaximizedVert; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_MODAL))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_MODAL))) result |= NetWmStateModal; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_STAYS_ON_TOP))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_STAYS_ON_TOP))) result |= NetWmStateStaysOnTop; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_DEMANDS_ATTENTION))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_DEMANDS_ATTENTION))) result |= NetWmStateDemandsAttention; - if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::_NET_WM_STATE_HIDDEN))) + if (statesEnd != std::find(states, statesEnd, atom(QXcbAtom::Atom_NET_WM_STATE_HIDDEN))) result |= NetWmStateHidden; } else { qCDebug(lcQpaXcb, "getting net wm state (%x), empty\n", m_window); @@ -959,13 +1041,13 @@ void QXcbWindow::setMotifWmHints(Qt::WindowFlags flags) xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_MOTIF_WM_HINTS), - atom(QXcbAtom::_MOTIF_WM_HINTS), + atom(QXcbAtom::Atom_MOTIF_WM_HINTS), + atom(QXcbAtom::Atom_MOTIF_WM_HINTS), 32, 5, &mwmhints); } else { - xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::_MOTIF_WM_HINTS)); + xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::Atom_MOTIF_WM_HINTS)); } } @@ -977,7 +1059,7 @@ void QXcbWindow::setNetWmState(bool set, xcb_atom_t one, xcb_atom_t two) event.format = 32; event.sequence = 0; event.window = m_window; - event.type = atom(QXcbAtom::_NET_WM_STATE); + event.type = atom(QXcbAtom::Atom_NET_WM_STATE); event.data.data32[0] = set ? 1 : 0; event.data.data32[1] = one; event.data.data32[2] = two; @@ -993,26 +1075,26 @@ void QXcbWindow::setNetWmState(Qt::WindowStates state) { if ((m_windowState ^ state) & Qt::WindowMaximized) { setNetWmState(state & Qt::WindowMaximized, - atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_HORZ), - atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_VERT)); + atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_HORZ), + atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT)); } if ((m_windowState ^ state) & Qt::WindowFullScreen) - setNetWmState(state & Qt::WindowFullScreen, atom(QXcbAtom::_NET_WM_STATE_FULLSCREEN)); + setNetWmState(state & Qt::WindowFullScreen, atom(QXcbAtom::Atom_NET_WM_STATE_FULLSCREEN)); } void QXcbWindow::setNetWmState(Qt::WindowFlags flags) { setNetWmState(flags & Qt::WindowStaysOnTopHint, - atom(QXcbAtom::_NET_WM_STATE_ABOVE), - atom(QXcbAtom::_NET_WM_STATE_STAYS_ON_TOP)); - setNetWmState(flags & Qt::WindowStaysOnBottomHint, atom(QXcbAtom::_NET_WM_STATE_BELOW)); + atom(QXcbAtom::Atom_NET_WM_STATE_ABOVE), + atom(QXcbAtom::Atom_NET_WM_STATE_STAYS_ON_TOP)); + setNetWmState(flags & Qt::WindowStaysOnBottomHint, atom(QXcbAtom::Atom_NET_WM_STATE_BELOW)); } void QXcbWindow::setNetWmStateOnUnmappedWindow() { if (Q_UNLIKELY(m_mapped)) - qCWarning(lcQpaXcb()) << "internal error: " << Q_FUNC_INFO << "called on mapped window"; + qCDebug(lcQpaXcb()) << "internal info: " << Q_FUNC_INFO << "called on mapped window"; NetWmStates states; const Qt::WindowFlags flags = window()->flags(); @@ -1047,7 +1129,7 @@ void QXcbWindow::setNetWmStateOnUnmappedWindow() QList<xcb_atom_t> atoms; auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, xcb_connection(), - 0, m_window, atom(QXcbAtom::_NET_WM_STATE), + 0, m_window, atom(QXcbAtom::Atom_NET_WM_STATE), XCB_ATOM_ATOM, 0, 1024); if (reply && reply->format == 32 && reply->type == XCB_ATOM_ATOM && reply->value_len > 0) { const xcb_atom_t *data = static_cast<const xcb_atom_t *>(xcb_get_property_value(reply.get())); @@ -1055,31 +1137,31 @@ void QXcbWindow::setNetWmStateOnUnmappedWindow() memcpy((void *)&atoms.first(), (void *)data, reply->value_len * sizeof(xcb_atom_t)); } - if (states & NetWmStateAbove && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_ABOVE))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_ABOVE)); - if (states & NetWmStateBelow && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_BELOW))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_BELOW)); - if (states & NetWmStateHidden && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_HIDDEN))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_HIDDEN)); - if (states & NetWmStateFullScreen && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_FULLSCREEN))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_FULLSCREEN)); - if (states & NetWmStateMaximizedHorz && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_HORZ))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_HORZ)); - if (states & NetWmStateMaximizedVert && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_VERT))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_VERT)); - if (states & NetWmStateModal && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_MODAL))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_MODAL)); - if (states & NetWmStateStaysOnTop && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_STAYS_ON_TOP))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_STAYS_ON_TOP)); - if (states & NetWmStateDemandsAttention && !atoms.contains(atom(QXcbAtom::_NET_WM_STATE_DEMANDS_ATTENTION))) - atoms.push_back(atom(QXcbAtom::_NET_WM_STATE_DEMANDS_ATTENTION)); + if (states & NetWmStateAbove && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_ABOVE))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_ABOVE)); + if (states & NetWmStateBelow && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_BELOW))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_BELOW)); + if (states & NetWmStateHidden && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_HIDDEN))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_HIDDEN)); + if (states & NetWmStateFullScreen && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_FULLSCREEN))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_FULLSCREEN)); + if (states & NetWmStateMaximizedHorz && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_HORZ))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_HORZ)); + if (states & NetWmStateMaximizedVert && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT)); + if (states & NetWmStateModal && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_MODAL))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_MODAL)); + if (states & NetWmStateStaysOnTop && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_STAYS_ON_TOP))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_STAYS_ON_TOP)); + if (states & NetWmStateDemandsAttention && !atoms.contains(atom(QXcbAtom::Atom_NET_WM_STATE_DEMANDS_ATTENTION))) + atoms.push_back(atom(QXcbAtom::Atom_NET_WM_STATE_DEMANDS_ATTENTION)); if (atoms.isEmpty()) { - xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::_NET_WM_STATE)); + xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::Atom_NET_WM_STATE)); } else { xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_STATE), XCB_ATOM_ATOM, 32, - atoms.count(), atoms.constData()); + atom(QXcbAtom::Atom_NET_WM_STATE), XCB_ATOM_ATOM, 32, + atoms.size(), atoms.constData()); } xcb_flush(xcb_connection()); } @@ -1089,18 +1171,21 @@ void QXcbWindow::setWindowState(Qt::WindowStates state) if (state == m_windowState) return; + Qt::WindowStates unsetState = m_windowState & ~state; + Qt::WindowStates newState = state & ~m_windowState; + // unset old state - if (m_windowState & Qt::WindowMinimized) + if (unsetState & Qt::WindowMinimized) xcb_map_window(xcb_connection(), m_window); - if (m_windowState & Qt::WindowMaximized) + if (unsetState & Qt::WindowMaximized) setNetWmState(false, - atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_HORZ), - atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_VERT)); - if (m_windowState & Qt::WindowFullScreen) - setNetWmState(false, atom(QXcbAtom::_NET_WM_STATE_FULLSCREEN)); + atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_HORZ), + atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT)); + if (unsetState & Qt::WindowFullScreen) + setNetWmState(false, atom(QXcbAtom::Atom_NET_WM_STATE_FULLSCREEN)); // set new state - if (state & Qt::WindowMinimized) { + if (newState & Qt::WindowMinimized) { { xcb_client_message_event_t event; @@ -1108,7 +1193,7 @@ void QXcbWindow::setWindowState(Qt::WindowStates state) event.format = 32; event.sequence = 0; event.window = m_window; - event.type = atom(QXcbAtom::WM_CHANGE_STATE); + event.type = atom(QXcbAtom::AtomWM_CHANGE_STATE); event.data.data32[0] = XCB_ICCCM_WM_STATE_ICONIC; event.data.data32[1] = 0; event.data.data32[2] = 0; @@ -1121,13 +1206,8 @@ void QXcbWindow::setWindowState(Qt::WindowStates state) } m_minimized = true; } - if (state & Qt::WindowMaximized) - setNetWmState(true, - atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_HORZ), - atom(QXcbAtom::_NET_WM_STATE_MAXIMIZED_VERT)); - if (state & Qt::WindowFullScreen) - setNetWmState(true, atom(QXcbAtom::_NET_WM_STATE_FULLSCREEN)); + // set Maximized && FullScreen state if need setNetWmState(state); xcb_get_property_cookie_t cookie = xcb_icccm_get_wm_hints_unchecked(xcb_connection(), m_window); @@ -1153,7 +1233,7 @@ void QXcbWindow::updateNetWmUserTime(xcb_timestamp_t timestamp) if (timestamp != 0) connection()->setNetWmUserTime(timestamp); - const bool isSupportedByWM = connection()->wmSupport()->isSupportedByWM(atom(QXcbAtom::_NET_WM_USER_TIME_WINDOW)); + const bool isSupportedByWM = connection()->wmSupport()->isSupportedByWM(atom(QXcbAtom::Atom_NET_WM_USER_TIME_WINDOW)); if (m_netWmUserTimeWindow || isSupportedByWM) { if (!m_netWmUserTimeWindow) { m_netWmUserTimeWindow = xcb_generate_id(xcb_connection()); @@ -1168,9 +1248,9 @@ void QXcbWindow::updateNetWmUserTime(xcb_timestamp_t timestamp) 0, // value mask nullptr); // value list wid = m_netWmUserTimeWindow; - xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, atom(QXcbAtom::_NET_WM_USER_TIME_WINDOW), + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, atom(QXcbAtom::Atom_NET_WM_USER_TIME_WINDOW), XCB_ATOM_WINDOW, 32, 1, &m_netWmUserTimeWindow); - xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::_NET_WM_USER_TIME)); + xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::Atom_NET_WM_USER_TIME)); QXcbWindow::setWindowTitle(connection(), m_netWmUserTimeWindow, QStringLiteral("Qt NET_WM User Time Window")); @@ -1178,14 +1258,14 @@ void QXcbWindow::updateNetWmUserTime(xcb_timestamp_t timestamp) } else if (!isSupportedByWM) { // WM no longer supports it, then we should remove the // _NET_WM_USER_TIME_WINDOW atom. - xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::_NET_WM_USER_TIME_WINDOW)); + xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::Atom_NET_WM_USER_TIME_WINDOW)); xcb_destroy_window(xcb_connection(), m_netWmUserTimeWindow); m_netWmUserTimeWindow = XCB_NONE; } else { wid = m_netWmUserTimeWindow; } } - xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, wid, atom(QXcbAtom::_NET_WM_USER_TIME), + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, wid, atom(QXcbAtom::Atom_NET_WM_USER_TIME), XCB_ATOM_CARDINAL, 32, 1, ×tamp); } @@ -1260,10 +1340,10 @@ void QXcbWindow::setWindowIconText(const QString &title) xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_ICON_NAME), - atom(QXcbAtom::UTF8_STRING), + atom(QXcbAtom::Atom_NET_WM_ICON_NAME), + atom(QXcbAtom::AtomUTF8_STRING), 8, - ba.length(), + ba.size(), ba.constData()); } @@ -1295,23 +1375,24 @@ void QXcbWindow::setWindowIcon(const QIcon &icon) if (!icon_data.isEmpty()) { // Ignore icon exceeding maximum xcb request length - if (icon_data.size() > xcb_get_maximum_request_length(xcb_connection())) { - qWarning("Ignoring window icon: Size %llu exceeds maximum xcb request length %u.", - icon_data.size(), xcb_get_maximum_request_length(xcb_connection())); + if (quint64(icon_data.size()) > quint64(xcb_get_maximum_request_length(xcb_connection()))) { + qWarning() << "Ignoring window icon" << icon_data.size() + << "exceeds maximum xcb request length" + << xcb_get_maximum_request_length(xcb_connection()); return; } xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_ICON), - atom(QXcbAtom::CARDINAL), + atom(QXcbAtom::Atom_NET_WM_ICON), + atom(QXcbAtom::AtomCARDINAL), 32, icon_data.size(), (unsigned char *) icon_data.data()); } else { xcb_delete_property(xcb_connection(), m_window, - atom(QXcbAtom::_NET_WM_ICON)); + atom(QXcbAtom::Atom_NET_WM_ICON)); } } @@ -1368,7 +1449,8 @@ void QXcbWindow::propagateSizeHints() qMin(XCOORD_MAX, maximumSize.height())); if (sizeIncrement.width() > 0 || sizeIncrement.height() > 0) { - xcb_icccm_size_hints_set_base_size(&hints, baseSize.width(), baseSize.height()); + if (!baseSize.isNull() && baseSize.isValid()) + xcb_icccm_size_hints_set_base_size(&hints, baseSize.width(), baseSize.height()); xcb_icccm_size_hints_set_resize_inc(&hints, sizeIncrement.width(), sizeIncrement.height()); } @@ -1386,29 +1468,37 @@ void QXcbWindow::requestActivateWindow() return; } - if (!m_mapped) { - m_deferredActivation = true; - return; + { + QMutexLocker locker(&m_mappedMutex); + if (!m_mapped) { + m_deferredActivation = true; + return; + } + m_deferredActivation = false; } - m_deferredActivation = false; updateNetWmUserTime(connection()->time()); QWindow *focusWindow = QGuiApplication::focusWindow(); + xcb_window_t current = XCB_NONE; + if (focusWindow) { + if (QPlatformWindow *pw = focusWindow->handle()) + current = pw->winId(); + } if (window()->isTopLevel() && !(window()->flags() & Qt::X11BypassWindowManagerHint) && (!focusWindow || !window()->isAncestorOf(focusWindow)) - && connection()->wmSupport()->isSupportedByWM(atom(QXcbAtom::_NET_ACTIVE_WINDOW))) { + && connection()->wmSupport()->isSupportedByWM(atom(QXcbAtom::Atom_NET_ACTIVE_WINDOW))) { xcb_client_message_event_t event; event.response_type = XCB_CLIENT_MESSAGE; event.format = 32; event.sequence = 0; event.window = m_window; - event.type = atom(QXcbAtom::_NET_ACTIVE_WINDOW); + event.type = atom(QXcbAtom::Atom_NET_ACTIVE_WINDOW); event.data.data32[0] = 1; event.data.data32[1] = connection()->time(); - event.data.data32[2] = focusWindow ? focusWindow->winId() : XCB_NONE; + event.data.data32[2] = current; event.data.data32[3] = 0; event.data.data32[4] = 0; @@ -1432,7 +1522,7 @@ QXcbWindow::WindowTypes QXcbWindow::wmWindowTypes() const WindowTypes result; auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, xcb_connection(), - 0, m_window, atom(QXcbAtom::_NET_WM_WINDOW_TYPE), + 0, m_window, atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE), XCB_ATOM_ATOM, 0, 1024); if (reply && reply->format == 32 && reply->type == XCB_ATOM_ATOM) { const xcb_atom_t *types = static_cast<const xcb_atom_t *>(xcb_get_property_value(reply.get())); @@ -1440,49 +1530,49 @@ QXcbWindow::WindowTypes QXcbWindow::wmWindowTypes() const for (; types != types_end; types++) { QXcbAtom::Atom type = connection()->qatom(*types); switch (type) { - case QXcbAtom::_NET_WM_WINDOW_TYPE_NORMAL: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_NORMAL: result |= WindowType::Normal; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_DESKTOP: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DESKTOP: result |= WindowType::Desktop; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_DOCK: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DOCK: result |= WindowType::Dock; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_TOOLBAR: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_TOOLBAR: result |= WindowType::Toolbar; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_MENU: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_MENU: result |= WindowType::Menu; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_UTILITY: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_UTILITY: result |= WindowType::Utility; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_SPLASH: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_SPLASH: result |= WindowType::Splash; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_DIALOG: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DIALOG: result |= WindowType::Dialog; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_DROPDOWN_MENU: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DROPDOWN_MENU: result |= WindowType::DropDownMenu; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_POPUP_MENU: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_POPUP_MENU: result |= WindowType::PopupMenu; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_TOOLTIP: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_TOOLTIP: result |= WindowType::Tooltip; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_NOTIFICATION: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_NOTIFICATION: result |= WindowType::Notification; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_COMBO: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_COMBO: result |= WindowType::Combo; break; - case QXcbAtom::_NET_WM_WINDOW_TYPE_DND: + case QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DND: result |= WindowType::Dnd; break; - case QXcbAtom::_KDE_NET_WM_WINDOW_TYPE_OVERRIDE: + case QXcbAtom::Atom_KDE_NET_WM_WINDOW_TYPE_OVERRIDE: result |= WindowType::KdeOverride; break; default: @@ -1499,41 +1589,41 @@ void QXcbWindow::setWmWindowType(WindowTypes types, Qt::WindowFlags flags) // manual selection 1 (these are never set by Qt and take precedence) if (types & WindowType::Normal) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_NORMAL)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_NORMAL)); if (types & WindowType::Desktop) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_DESKTOP)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DESKTOP)); if (types & WindowType::Dock) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_DOCK)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DOCK)); if (types & WindowType::Notification) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_NOTIFICATION)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_NOTIFICATION)); // manual selection 2 (Qt uses these during auto selection); if (types & WindowType::Utility) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_UTILITY)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_UTILITY)); if (types & WindowType::Splash) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_SPLASH)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_SPLASH)); if (types & WindowType::Dialog) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_DIALOG)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DIALOG)); if (types & WindowType::Tooltip) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_TOOLTIP)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_TOOLTIP)); if (types & WindowType::KdeOverride) - atoms.append(atom(QXcbAtom::_KDE_NET_WM_WINDOW_TYPE_OVERRIDE)); + atoms.append(atom(QXcbAtom::Atom_KDE_NET_WM_WINDOW_TYPE_OVERRIDE)); // manual selection 3 (these can be set by Qt, but don't have a // corresponding Qt::WindowType). note that order of the *MENU // atoms is important if (types & WindowType::Menu) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_MENU)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_MENU)); if (types & WindowType::DropDownMenu) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_DROPDOWN_MENU)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DROPDOWN_MENU)); if (types & WindowType::PopupMenu) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_POPUP_MENU)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_POPUP_MENU)); if (types & WindowType::Toolbar) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_TOOLBAR)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_TOOLBAR)); if (types & WindowType::Combo) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_COMBO)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_COMBO)); if (types & WindowType::Dnd) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_DND)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DND)); // automatic selection Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask)); @@ -1541,20 +1631,20 @@ void QXcbWindow::setWmWindowType(WindowTypes types, Qt::WindowFlags flags) case Qt::Dialog: case Qt::Sheet: if (!(types & WindowType::Dialog)) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_DIALOG)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_DIALOG)); break; case Qt::Tool: case Qt::Drawer: if (!(types & WindowType::Utility)) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_UTILITY)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_UTILITY)); break; case Qt::ToolTip: if (!(types & WindowType::Tooltip)) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_TOOLTIP)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_TOOLTIP)); break; case Qt::SplashScreen: if (!(types & WindowType::Splash)) - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_SPLASH)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_SPLASH)); break; default: break; @@ -1562,20 +1652,20 @@ void QXcbWindow::setWmWindowType(WindowTypes types, Qt::WindowFlags flags) if ((flags & Qt::FramelessWindowHint) && !(types & WindowType::KdeOverride)) { // override netwm type - quick and easy for KDE noborder - atoms.append(atom(QXcbAtom::_KDE_NET_WM_WINDOW_TYPE_OVERRIDE)); + atoms.append(atom(QXcbAtom::Atom_KDE_NET_WM_WINDOW_TYPE_OVERRIDE)); } - if (atoms.size() == 1 && atoms.first() == atom(QXcbAtom::_NET_WM_WINDOW_TYPE_NORMAL)) + if (atoms.size() == 1 && atoms.first() == atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_NORMAL)) atoms.clear(); else - atoms.append(atom(QXcbAtom::_NET_WM_WINDOW_TYPE_NORMAL)); + atoms.append(atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE_NORMAL)); if (atoms.isEmpty()) { - xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::_NET_WM_WINDOW_TYPE)); + xcb_delete_property(xcb_connection(), m_window, atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE)); } else { xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_WINDOW_TYPE), XCB_ATOM_ATOM, 32, - atoms.count(), atoms.constData()); + atom(QXcbAtom::Atom_NET_WM_WINDOW_TYPE), XCB_ATOM_ATOM, 32, + atoms.size(), atoms.constData()); } xcb_flush(xcb_connection()); } @@ -1584,7 +1674,7 @@ void QXcbWindow::setWindowRole(const QString &role) { QByteArray roleData = role.toLatin1(); xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::WM_WINDOW_ROLE), XCB_ATOM_STRING, 8, + atom(QXcbAtom::AtomWM_WINDOW_ROLE), XCB_ATOM_STRING, 8, roleData.size(), roleData.constData()); } @@ -1643,15 +1733,15 @@ void QXcbWindow::handleClientMessageEvent(const xcb_client_message_event_t *even if (event->format != 32) return; - if (event->type == atom(QXcbAtom::WM_PROTOCOLS)) { + if (event->type == atom(QXcbAtom::AtomWM_PROTOCOLS)) { xcb_atom_t protocolAtom = event->data.data32[0]; - if (protocolAtom == atom(QXcbAtom::WM_DELETE_WINDOW)) { + if (protocolAtom == atom(QXcbAtom::AtomWM_DELETE_WINDOW)) { QWindowSystemInterface::handleCloseEvent(window()); - } else if (protocolAtom == atom(QXcbAtom::WM_TAKE_FOCUS)) { + } else if (protocolAtom == atom(QXcbAtom::AtomWM_TAKE_FOCUS)) { connection()->setTime(event->data.data32[1]); relayFocusToModalWindow(); return; - } else if (protocolAtom == atom(QXcbAtom::_NET_WM_PING)) { + } else if (protocolAtom == atom(QXcbAtom::Atom_NET_WM_PING)) { if (event->window == xcbScreen()->root()) return; @@ -1664,14 +1754,14 @@ void QXcbWindow::handleClientMessageEvent(const xcb_client_message_event_t *even XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (const char *)&reply); xcb_flush(xcb_connection()); - } else if (protocolAtom == atom(QXcbAtom::_NET_WM_SYNC_REQUEST)) { + } else if (protocolAtom == atom(QXcbAtom::Atom_NET_WM_SYNC_REQUEST)) { connection()->setTime(event->data.data32[1]); m_syncValue.lo = event->data.data32[2]; m_syncValue.hi = event->data.data32[3]; if (connection()->hasXSync()) m_syncState = SyncReceived; #ifndef QT_NO_WHATSTHIS - } else if (protocolAtom == atom(QXcbAtom::_NET_WM_CONTEXT_HELP)) { + } else if (protocolAtom == atom(QXcbAtom::Atom_NET_WM_CONTEXT_HELP)) { QWindowSystemInterface::handleEnterWhatsThisEvent(); #endif } else { @@ -1679,29 +1769,29 @@ void QXcbWindow::handleClientMessageEvent(const xcb_client_message_event_t *even connection()->atomName(protocolAtom).constData()); } #if QT_CONFIG(draganddrop) - } else if (event->type == atom(QXcbAtom::XdndEnter)) { + } else if (event->type == atom(QXcbAtom::AtomXdndEnter)) { connection()->drag()->handleEnter(this, event); - } else if (event->type == atom(QXcbAtom::XdndPosition)) { + } else if (event->type == atom(QXcbAtom::AtomXdndPosition)) { connection()->drag()->handlePosition(this, event); - } else if (event->type == atom(QXcbAtom::XdndLeave)) { + } else if (event->type == atom(QXcbAtom::AtomXdndLeave)) { connection()->drag()->handleLeave(this, event); - } else if (event->type == atom(QXcbAtom::XdndDrop)) { + } else if (event->type == atom(QXcbAtom::AtomXdndDrop)) { connection()->drag()->handleDrop(this, event); #endif - } else if (event->type == atom(QXcbAtom::_XEMBED)) { + } else if (event->type == atom(QXcbAtom::Atom_XEMBED)) { handleXEmbedMessage(event); - } else if (event->type == atom(QXcbAtom::_NET_ACTIVE_WINDOW)) { + } else if (event->type == atom(QXcbAtom::Atom_NET_ACTIVE_WINDOW)) { doFocusIn(); - } else if (event->type == atom(QXcbAtom::MANAGER) - || event->type == atom(QXcbAtom::_NET_WM_STATE) - || event->type == atom(QXcbAtom::WM_CHANGE_STATE)) { + } else if (event->type == atom(QXcbAtom::AtomMANAGER) + || event->type == atom(QXcbAtom::Atom_NET_WM_STATE) + || event->type == atom(QXcbAtom::AtomWM_CHANGE_STATE)) { // Ignore _NET_WM_STATE, MANAGER which are relate to tray icons // and other messages. - } else if (event->type == atom(QXcbAtom::_COMPIZ_DECOR_PENDING) - || event->type == atom(QXcbAtom::_COMPIZ_DECOR_REQUEST) - || event->type == atom(QXcbAtom::_COMPIZ_DECOR_DELETE_PIXMAP) - || event->type == atom(QXcbAtom::_COMPIZ_TOOLKIT_ACTION) - || event->type == atom(QXcbAtom::_GTK_LOAD_ICONTHEMES)) { + } else if (event->type == atom(QXcbAtom::Atom_COMPIZ_DECOR_PENDING) + || event->type == atom(QXcbAtom::Atom_COMPIZ_DECOR_REQUEST) + || event->type == atom(QXcbAtom::Atom_COMPIZ_DECOR_DELETE_PIXMAP) + || event->type == atom(QXcbAtom::Atom_COMPIZ_TOOLKIT_ACTION) + || event->type == atom(QXcbAtom::Atom_GTK_LOAD_ICONTHEMES)) { //silence the _COMPIZ and _GTK messages for now } else { qCWarning(lcQpaXcb) << "Unhandled client message: " << connection()->atomName(event->type); @@ -1712,7 +1802,7 @@ void QXcbWindow::handleConfigureNotifyEvent(const xcb_configure_notify_event_t * { bool fromSendEvent = (event->response_type & 0x80); QPoint pos(event->x, event->y); - if (!parent() && !fromSendEvent) { + if (!QPlatformWindow::parent() && !fromSendEvent) { // Do not trust the position, query it instead. auto reply = Q_XCB_REPLY(xcb_translate_coordinates, xcb_connection(), xcb_window(), xcbScreen()->root(), 0, 0); @@ -1723,7 +1813,7 @@ void QXcbWindow::handleConfigureNotifyEvent(const xcb_configure_notify_event_t * } const QRect actualGeometry = QRect(pos, QSize(event->width, event->height)); - QPlatformScreen *newScreen = parent() ? parent()->screen() : screenForGeometry(actualGeometry); + QPlatformScreen *newScreen = QPlatformWindow::parent() ? QPlatformWindow::parent()->screen() : screenForGeometry(actualGeometry); if (!newScreen) return; @@ -1799,8 +1889,11 @@ QPoint QXcbWindow::mapFromGlobal(const QPoint &pos) const void QXcbWindow::handleMapNotifyEvent(const xcb_map_notify_event_t *event) { if (event->window == m_window) { + m_mappedMutex.lock(); m_mapped = true; - if (m_deferredActivation) + const bool deferredActivation = m_deferredActivation; + m_mappedMutex.unlock(); + if (deferredActivation) requestActivateWindow(); QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(), geometry().size())); @@ -1810,7 +1903,9 @@ void QXcbWindow::handleMapNotifyEvent(const xcb_map_notify_event_t *event) void QXcbWindow::handleUnmapNotifyEvent(const xcb_unmap_notify_event_t *event) { if (event->window == m_window) { + m_mappedMutex.lock(); m_mapped = false; + m_mappedMutex.unlock(); QWindowSystemInterface::handleExposeEvent(window(), QRegion()); } } @@ -1833,7 +1928,7 @@ void QXcbWindow::handleButtonPressEvent(int event_x, int event_y, int root_x, in if (m_embedded && !m_trayIconWindow) { if (window() != QGuiApplication::focusWindow()) { - const QXcbWindow *container = static_cast<const QXcbWindow *>(parent()); + const QXcbWindow *container = static_cast<const QXcbWindow *>(QPlatformWindow::parent()); Q_ASSERT(container != nullptr); sendXEmbedMessage(container->xcb_window(), XEMBED_REQUEST_FOCUS); @@ -1877,8 +1972,10 @@ void QXcbWindow::handleButtonReleaseEvent(int event_x, int event_y, int root_x, return; } - if (connection()->buttonState() == Qt::NoButton) + if (connection()->buttonState() == Qt::NoButton) { connection()->setMousePressWindow(nullptr); + m_ignorePressedWindowOnMouseLeave = false; + } handleMouseEvent(timestamp, local, global, modifiers, type, source); } @@ -1898,10 +1995,10 @@ static inline bool doCheckUnGrabAncestor(QXcbConnection *conn) return true; } -static bool ignoreLeaveEvent(quint8 mode, quint8 detail, QXcbConnection *conn = nullptr) +static bool ignoreLeaveEvent(quint8 mode, quint8 detail, QXcbConnection *conn) { return ((doCheckUnGrabAncestor(conn) - && mode == XCB_NOTIFY_MODE_GRAB && detail == XCB_NOTIFY_DETAIL_ANCESTOR) + && mode == XCB_NOTIFY_MODE_GRAB && detail == XCB_NOTIFY_DETAIL_ANCESTOR) || (mode == XCB_NOTIFY_MODE_UNGRAB && detail == XCB_NOTIFY_DETAIL_INFERIOR) || detail == XCB_NOTIFY_DETAIL_VIRTUAL || detail == XCB_NOTIFY_DETAIL_NONLINEAR_VIRTUAL); @@ -1921,14 +2018,18 @@ void QXcbWindow::handleEnterNotifyEvent(int event_x, int event_y, int root_x, in { connection()->setTime(timestamp); - const QPoint global = QPoint(root_x, root_y); - - if (ignoreEnterEvent(mode, detail, connection()) || connection()->mousePressWindow()) + if (ignoreEnterEvent(mode, detail, connection()) + || (connection()->mousePressWindow() && !m_ignorePressedWindowOnMouseLeave)) { return; + } // Updates scroll valuators, as user might have done some scrolling outside our X client. connection()->xi2UpdateScrollingDevices(); + if (mode == XCB_NOTIFY_MODE_UNGRAB && connection()->queryMouseButtons() != Qt::NoButton) + m_ignorePressedWindowOnMouseLeave = true; + + const QPoint global = QPoint(root_x, root_y); const QPoint local(event_x, event_y); QWindowSystemInterface::handleEnterEvent(window(), local, global); } @@ -1938,8 +2039,11 @@ void QXcbWindow::handleLeaveNotifyEvent(int root_x, int root_y, { connection()->setTime(timestamp); - if (ignoreLeaveEvent(mode, detail, connection()) || connection()->mousePressWindow()) + QXcbWindow *mousePressWindow = connection()->mousePressWindow(); + if (ignoreLeaveEvent(mode, detail, connection()) + || (mousePressWindow && !m_ignorePressedWindowOnMouseLeave)) { return; + } // check if enter event is buffered auto event = connection()->eventQueue()->peek([](xcb_generic_event_t *event, int type) { @@ -1957,6 +2061,8 @@ void QXcbWindow::handleLeaveNotifyEvent(int root_x, int root_y, QWindowSystemInterface::handleEnterLeaveEvent(enterWindow->window(), window(), local, global); } else { QWindowSystemInterface::handleLeaveEvent(window()); + if (m_ignorePressedWindowOnMouseLeave) + connection()->setMousePressWindow(nullptr); } free(enter); @@ -2133,17 +2239,17 @@ void QXcbWindow::handlePropertyNotifyEvent(const xcb_property_notify_event_t *ev const bool propertyDeleted = event->state == XCB_PROPERTY_DELETE; - if (event->atom == atom(QXcbAtom::_NET_WM_STATE) || event->atom == atom(QXcbAtom::WM_STATE)) { + if (event->atom == atom(QXcbAtom::Atom_NET_WM_STATE) || event->atom == atom(QXcbAtom::AtomWM_STATE)) { if (propertyDeleted) return; Qt::WindowStates newState = Qt::WindowNoState; - if (event->atom == atom(QXcbAtom::WM_STATE)) { // WM_STATE: Quick check for 'Minimize'. + if (event->atom == atom(QXcbAtom::AtomWM_STATE)) { // WM_STATE: Quick check for 'Minimize'. auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), - 0, m_window, atom(QXcbAtom::WM_STATE), + 0, m_window, atom(QXcbAtom::AtomWM_STATE), XCB_ATOM_ANY, 0, 1024); - if (reply && reply->format == 32 && reply->type == atom(QXcbAtom::WM_STATE)) { + if (reply && reply->format == 32 && reply->type == atom(QXcbAtom::AtomWM_STATE)) { const quint32 *data = (const quint32 *)xcb_get_property_value(reply.get()); if (reply->length != 0) m_minimized = (data[0] == XCB_ICCCM_WM_STATE_ICONIC @@ -2173,7 +2279,7 @@ void QXcbWindow::handlePropertyNotifyEvent(const xcb_property_notify_event_t *ev connection()->setMouseGrabber(nullptr); } return; - } else if (event->atom == atom(QXcbAtom::_NET_FRAME_EXTENTS)) { + } else if (event->atom == atom(QXcbAtom::Atom_NET_FRAME_EXTENTS)) { m_dirtyFrameMargins = true; } } @@ -2282,7 +2388,7 @@ bool QXcbWindow::windowEvent(QEvent *event) case Qt::BacktabFocusReason: { const QXcbWindow *container = - static_cast<const QXcbWindow *>(parent()); + static_cast<const QXcbWindow *>(QPlatformWindow::parent()); sendXEmbedMessage(container->xcb_window(), focusEvent->reason() == Qt::TabFocusReason ? XEMBED_FOCUS_NEXT : XEMBED_FOCUS_PREV); @@ -2312,7 +2418,7 @@ bool QXcbWindow::startSystemMove() bool QXcbWindow::startSystemMoveResize(const QPoint &pos, int edges) { - const xcb_atom_t moveResize = connection()->atom(QXcbAtom::_NET_WM_MOVERESIZE); + const xcb_atom_t moveResize = connection()->atom(QXcbAtom::Atom_NET_WM_MOVERESIZE); if (!connection()->wmSupport()->isSupportedByWM(moveResize)) return false; @@ -2360,7 +2466,7 @@ static uint qtEdgesToXcbMoveResizeDirection(Qt::Edges edges) void QXcbWindow::doStartSystemMoveResize(const QPoint &globalPos, int edges) { qCDebug(lcQpaXInputDevices) << "triggered system move or resize via sending _NET_WM_MOVERESIZE client message"; - const xcb_atom_t moveResize = connection()->atom(QXcbAtom::_NET_WM_MOVERESIZE); + const xcb_atom_t moveResize = connection()->atom(QXcbAtom::Atom_NET_WM_MOVERESIZE); xcb_client_message_event_t xev; xev.response_type = XCB_CLIENT_MESSAGE; xev.type = moveResize; @@ -2393,7 +2499,7 @@ void QXcbWindow::sendXEmbedMessage(xcb_window_t window, quint32 message, event.format = 32; event.sequence = 0; event.window = window; - event.type = atom(QXcbAtom::_XEMBED); + event.type = atom(QXcbAtom::Atom_XEMBED); event.data.data32[0] = connection()->time(); event.data.data32[1] = message; event.data.data32[2] = detail; @@ -2402,15 +2508,15 @@ void QXcbWindow::sendXEmbedMessage(xcb_window_t window, quint32 message, xcb_send_event(xcb_connection(), false, window, XCB_EVENT_MASK_NO_EVENT, (const char *)&event); } -static bool activeWindowChangeQueued(const QWindow *window) +static bool focusWindowChangeQueued(const QWindow *window) { /* Check from window system event queue if the next queued activation * targets a window other than @window. */ - QWindowSystemInterfacePrivate::ActivatedWindowEvent *systemEvent = - static_cast<QWindowSystemInterfacePrivate::ActivatedWindowEvent *> - (QWindowSystemInterfacePrivate::peekWindowSystemEvent(QWindowSystemInterfacePrivate::ActivatedWindow)); - return systemEvent && systemEvent->activated != window; + QWindowSystemInterfacePrivate::FocusWindowEvent *systemEvent = + static_cast<QWindowSystemInterfacePrivate::FocusWindowEvent *> + (QWindowSystemInterfacePrivate::peekWindowSystemEvent(QWindowSystemInterfacePrivate::FocusWindow)); + return systemEvent && systemEvent->focused != window; } void QXcbWindow::handleXEmbedMessage(const xcb_client_message_event_t *event) @@ -2440,13 +2546,13 @@ void QXcbWindow::handleXEmbedMessage(const xcb_client_message_event_t *event) break; } connection()->setFocusWindow(window()); - QWindowSystemInterface::handleWindowActivated(window(), reason); + QWindowSystemInterface::handleFocusWindowChanged(window(), reason); break; case XEMBED_FOCUS_OUT: if (window() == QGuiApplication::focusWindow() - && !activeWindowChangeQueued(window())) { + && !focusWindowChangeQueued(window())) { connection()->setFocusWindow(nullptr); - QWindowSystemInterface::handleWindowActivated(nullptr); + QWindowSystemInterface::handleFocusWindowChanged(nullptr); } break; } @@ -2472,7 +2578,7 @@ void QXcbWindow::setOpacity(qreal level) xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, m_window, - atom(QXcbAtom::_NET_WM_WINDOW_OPACITY), + atom(QXcbAtom::Atom_NET_WM_WINDOW_OPACITY), XCB_ATOM_CARDINAL, 32, 1, @@ -2510,7 +2616,7 @@ void QXcbWindow::setAlertState(bool enabled) m_alertState = enabled; - setNetWmState(enabled, atom(QXcbAtom::_NET_WM_STATE_DEMANDS_ATTENTION)); + setNetWmState(enabled, atom(QXcbAtom::Atom_NET_WM_STATE_DEMANDS_ATTENTION)); } uint QXcbWindow::visualId() const @@ -2544,10 +2650,10 @@ void QXcbWindow::setWindowTitle(const QXcbConnection *conn, xcb_window_t window, xcb_change_property(conn->xcb_connection(), XCB_PROP_MODE_REPLACE, window, - conn->atom(QXcbAtom::_NET_WM_NAME), - conn->atom(QXcbAtom::UTF8_STRING), + conn->atom(QXcbAtom::Atom_NET_WM_NAME), + conn->atom(QXcbAtom::AtomUTF8_STRING), 8, - ba.length(), + ba.size(), ba.constData()); #if QT_CONFIG(xcb_xlib) @@ -2561,9 +2667,9 @@ void QXcbWindow::setWindowTitle(const QXcbConnection *conn, xcb_window_t window, QString QXcbWindow::windowTitle(const QXcbConnection *conn, xcb_window_t window) { - const xcb_atom_t utf8Atom = conn->atom(QXcbAtom::UTF8_STRING); + const xcb_atom_t utf8Atom = conn->atom(QXcbAtom::AtomUTF8_STRING); auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, conn->xcb_connection(), - false, window, conn->atom(QXcbAtom::_NET_WM_NAME), + false, window, conn->atom(QXcbAtom::Atom_NET_WM_NAME), utf8Atom, 0, 1024); if (reply && reply->format == 8 && reply->type == utf8Atom) { const char *name = reinterpret_cast<const char *>(xcb_get_property_value(reply.get())); @@ -2571,7 +2677,7 @@ QString QXcbWindow::windowTitle(const QXcbConnection *conn, xcb_window_t window) } reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, conn->xcb_connection(), - false, window, conn->atom(QXcbAtom::WM_NAME), + false, window, conn->atom(QXcbAtom::AtomWM_NAME), XCB_ATOM_STRING, 0, 1024); if (reply && reply->format == 8 && reply->type == XCB_ATOM_STRING) { const char *name = reinterpret_cast<const char *>(xcb_get_property_value(reply.get())); diff --git a/src/plugins/platforms/xcb/qxcbwindow.h b/src/plugins/platforms/xcb/qxcbwindow.h index 54a96a7a0a..0c047d569b 100644 --- a/src/plugins/platforms/xcb/qxcbwindow.h +++ b/src/plugins/platforms/xcb/qxcbwindow.h @@ -6,6 +6,8 @@ #include <qpa/qplatformwindow.h> #include <qpa/qplatformwindow_p.h> +#include <QtCore/QObject> +#include <QtCore/QPointer> #include <QtGui/QSurfaceFormat> #include <QtGui/QImage> @@ -20,9 +22,10 @@ class QXcbScreen; class QXcbSyncWindowRequest; class QIcon; -class Q_XCB_EXPORT QXcbWindow : public QXcbObject, public QXcbWindowEventListener, public QPlatformWindow +class Q_XCB_EXPORT QXcbWindow : public QObject, public QXcbObject, public QXcbWindowEventListener, public QPlatformWindow , public QNativeInterface::Private::QXcbWindow { + Q_OBJECT public: enum NetWmState { NetWmStateAbove = 0x1, @@ -118,6 +121,8 @@ public: Qt::KeyboardModifiers modifiers, QEvent::Type type, Qt::MouseEventSource source); void updateNetWmUserTime(xcb_timestamp_t timestamp); + void updateWmTransientFor(); + void registerWmTransientForChild(QXcbWindow *); WindowTypes wmWindowTypes() const; void setWmWindowType(WindowTypes types, Qt::WindowFlags flags); @@ -217,6 +222,7 @@ protected: Qt::WindowStates m_windowState = Qt::WindowNoState; + QMutex m_mappedMutex; bool m_mapped = false; bool m_transparent = false; bool m_deferredActivation = false; @@ -224,6 +230,7 @@ protected: bool m_alertState = false; bool m_minimized = false; bool m_trayIconWindow = false; + bool m_ignorePressedWindowOnMouseLeave = false; xcb_window_t m_netWmUserTimeWindow = XCB_NONE; QSurfaceFormat m_format; @@ -253,13 +260,14 @@ protected: qreal m_sizeHintsScaleFactor = 1.0; RecreationReasons m_recreationReasons = RecreationNotNeeded; + + QList<QPointer<QXcbWindow>> m_wmTransientForChildren; }; class QXcbForeignWindow : public QXcbWindow { public: - QXcbForeignWindow(QWindow *window, WId nativeHandle) - : QXcbWindow(window) { m_window = nativeHandle; } + QXcbForeignWindow(QWindow *window, WId nativeHandle); ~QXcbForeignWindow(); bool isForeignWindow() const override { return true; } diff --git a/src/plugins/platforms/xcb/qxcbwmsupport.cpp b/src/plugins/platforms/xcb/qxcbwmsupport.cpp index 50e85c0aa5..0e3c470c89 100644 --- a/src/plugins/platforms/xcb/qxcbwmsupport.cpp +++ b/src/plugins/platforms/xcb/qxcbwmsupport.cpp @@ -30,7 +30,7 @@ void QXcbWMSupport::updateNetWMAtoms() int offset = 0; int remaining = 0; do { - auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, root, atom(QXcbAtom::_NET_SUPPORTED), XCB_ATOM_ATOM, offset, 1024); + auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, root, atom(QXcbAtom::Atom_NET_SUPPORTED), XCB_ATOM_ATOM, offset, 1024); if (!reply) break; @@ -54,7 +54,7 @@ void QXcbWMSupport::updateVirtualRoots() { net_virtual_roots.clear(); - if (!isSupportedByWM(atom(QXcbAtom::_NET_VIRTUAL_ROOTS))) + if (!isSupportedByWM(atom(QXcbAtom::Atom_NET_VIRTUAL_ROOTS))) return; xcb_window_t root = connection()->primaryScreen()->root(); @@ -62,7 +62,7 @@ void QXcbWMSupport::updateVirtualRoots() int remaining = 0; do { auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), - false, root, atom(QXcbAtom::_NET_VIRTUAL_ROOTS), XCB_ATOM_WINDOW, offset, 1024); + false, root, atom(QXcbAtom::Atom_NET_VIRTUAL_ROOTS), XCB_ATOM_WINDOW, offset, 1024); if (!reply) break; diff --git a/src/plugins/platforms/xcb/qxcbxsettings.cpp b/src/plugins/platforms/xcb/qxcbxsettings.cpp index 62fd13e5b9..6b62864add 100644 --- a/src/plugins/platforms/xcb/qxcbxsettings.cpp +++ b/src/plugins/platforms/xcb/qxcbxsettings.cpp @@ -68,7 +68,7 @@ public: int offset = 0; QByteArray settings; - xcb_atom_t _xsettings_atom = screen->connection()->atom(QXcbAtom::_XSETTINGS_SETTINGS); + xcb_atom_t _xsettings_atom = screen->connection()->atom(QXcbAtom::Atom_XSETTINGS_SETTINGS); while (1) { auto reply = Q_XCB_REPLY_UNCHECKED(xcb_get_property, screen->xcb_connection(), @@ -104,7 +104,7 @@ public: void populateSettings(const QByteArray &xSettings) { - if (xSettings.length() < 12) + if (xSettings.size() < 12) return; char byteOrder = xSettings.at(0); if (byteOrder != XCB_IMAGE_ORDER_LSB_FIRST && byteOrder != XCB_IMAGE_ORDER_MSB_FIRST) { @@ -192,7 +192,7 @@ QXcbXSettings::QXcbXSettings(QXcbVirtualDesktop *screen) auto atom_reply = Q_XCB_REPLY(xcb_intern_atom, screen->xcb_connection(), true, - settings_atom_for_screen.length(), + settings_atom_for_screen.size(), settings_atom_for_screen.constData()); if (!atom_reply) return; diff --git a/src/plugins/platformthemes/CMakeLists.txt b/src/plugins/platformthemes/CMakeLists.txt index a3c1f4fa9b..e5abcd1a11 100644 --- a/src/plugins/platformthemes/CMakeLists.txt +++ b/src/plugins/platformthemes/CMakeLists.txt @@ -1,4 +1,6 @@ -# Generated from platformthemes.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + if(QT_FEATURE_dbus AND QT_FEATURE_mimetype AND QT_FEATURE_regularexpression AND UNIX AND NOT APPLE) add_subdirectory(xdgdesktopportal) diff --git a/src/plugins/platformthemes/gtk3/CMakeLists.txt b/src/plugins/platformthemes/gtk3/CMakeLists.txt index 62e752bd92..6d3c7bf3a2 100644 --- a/src/plugins/platformthemes/gtk3/CMakeLists.txt +++ b/src/plugins/platformthemes/gtk3/CMakeLists.txt @@ -1,7 +1,11 @@ -# Generated from gtk3.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -qt_find_package(GTK3) # special case -qt_find_package(X11) # special case +qt_find_package(GTK3) + +if(QT_FEATURE_xlib) + qt_find_package(X11) +endif() ##################################################################### ## QGtk3ThemePlugin Plugin: @@ -16,16 +20,29 @@ qt_internal_add_plugin(QGtk3ThemePlugin qgtk3dialoghelpers.cpp qgtk3dialoghelpers.h qgtk3menu.cpp qgtk3menu.h qgtk3theme.cpp qgtk3theme.h + qgtk3interface.cpp qgtk3interface_p.h + qgtk3storage.cpp qgtk3storage_p.h + qgtk3json.cpp qgtk3json_p.h + NO_PCH_SOURCES + qgtk3dialoghelpers.cpp # undef QT_NO_FOREACH DEFINES GDK_VERSION_MIN_REQUIRED=GDK_VERSION_3_6 - LIBRARIES # special case + LIBRARIES PkgConfig::GTK3 Qt::Core Qt::CorePrivate Qt::Gui Qt::GuiPrivate - X11::X11 # special case ) -#### Keys ignored in scope 1:.:.:gtk3.pro:<TRUE>: -# PLUGIN_EXTENDS = "-" +qt_internal_extend_target(QGtk3ThemePlugin CONDITION QT_FEATURE_dbus + SOURCES + qgtk3portalinterface.cpp + LIBRARIES + Qt::DBus +) + +qt_internal_extend_target(QGtk3ThemePlugin CONDITION QT_FEATURE_xlib + LIBRARIES + X11::X11 +) diff --git a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp index 1fb6a22b31..08419ec7dc 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qgtk3dialoghelpers.h" #include "qgtk3theme.h" @@ -12,14 +14,23 @@ #include <qfileinfo.h> #include <private/qguiapplication_p.h> +#include <private/qgenericunixservices_p.h> +#include <qpa/qplatformintegration.h> #include <qpa/qplatformfontdatabase.h> #undef signals #include <gtk/gtk.h> #include <gdk/gdk.h> -#include <gdk/gdkx.h> #include <pango/pango.h> +#if QT_CONFIG(xlib) && defined(GDK_WINDOWING_X11) +#include <gdk/gdkx.h> +#endif + +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif + // The size of the preview we display for selected image files. We set height // larger than width because generally there is more free space vertically // than horizontally (setting the preview image will always expand the width of @@ -32,12 +43,10 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -class QGtk3Dialog : public QWindow +class QGtk3Dialog { - Q_OBJECT - public: - QGtk3Dialog(GtkWidget *gtkWidget); + QGtk3Dialog(GtkWidget *gtkWidget, QPlatformDialogHelper *helper); ~QGtk3Dialog(); GtkDialog *gtkDialog() const; @@ -46,23 +55,20 @@ public: bool show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent); void hide(); -Q_SIGNALS: - void accept(); - void reject(); - protected: - static void onResponse(QGtk3Dialog *dialog, int response); - -private slots: - void onParentWindowDestroyed(); + static void onResponse(QPlatformDialogHelper *helper, int response); private: GtkWidget *gtkWidget; + QPlatformDialogHelper *helper; + Qt::WindowModality modality; }; -QGtk3Dialog::QGtk3Dialog(GtkWidget *gtkWidget) : gtkWidget(gtkWidget) +QGtk3Dialog::QGtk3Dialog(GtkWidget *gtkWidget, QPlatformDialogHelper *helper) + : gtkWidget(gtkWidget) + , helper(helper) { - g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), this); + g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), helper); g_signal_connect(G_OBJECT(gtkWidget), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); } @@ -79,43 +85,52 @@ GtkDialog *QGtk3Dialog::gtkDialog() const void QGtk3Dialog::exec() { - if (modality() == Qt::ApplicationModal) { + if (modality == Qt::ApplicationModal) { // block input to the whole app, including other GTK dialogs gtk_dialog_run(gtkDialog()); } else { // block input to the window, allow input to other GTK dialogs QEventLoop loop; - connect(this, SIGNAL(accept()), &loop, SLOT(quit())); - connect(this, SIGNAL(reject()), &loop, SLOT(quit())); + loop.connect(helper, SIGNAL(accept()), SLOT(quit())); + loop.connect(helper, SIGNAL(reject()), SLOT(quit())); loop.exec(); } } bool QGtk3Dialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) { - if (parent) { - connect(parent, &QWindow::destroyed, this, &QGtk3Dialog::onParentWindowDestroyed, - Qt::UniqueConnection); - } - setParent(parent); - setFlags(flags); - setModality(modality); + Q_UNUSED(flags); + this->modality = modality; gtk_widget_realize(gtkWidget); // creates X window GdkWindow *gdkWindow = gtk_widget_get_window(gtkWidget); if (parent) { - if (GDK_IS_X11_WINDOW(gdkWindow)) { + if (false) { +#if defined(GDK_WINDOWING_WAYLAND) && GTK_CHECK_VERSION(3, 22, 0) + } else if (GDK_IS_WAYLAND_WINDOW(gdkWindow)) { + const auto unixServices = dynamic_cast<QGenericUnixServices *>( + QGuiApplicationPrivate::platformIntegration()->services()); + if (unixServices) { + const auto handle = unixServices->portalWindowIdentifier(parent); + if (handle.startsWith("wayland:"_L1)) { + auto handleBa = handle.sliced(8).toUtf8(); + gdk_wayland_window_set_transient_for_exported(gdkWindow, handleBa.data()); + } + } +#endif +#if QT_CONFIG(xlib) && defined(GDK_WINDOWING_X11) + } else if (GDK_IS_X11_WINDOW(gdkWindow)) { GdkDisplay *gdkDisplay = gdk_window_get_display(gdkWindow); XSetTransientForHint(gdk_x11_display_get_xdisplay(gdkDisplay), gdk_x11_window_get_xid(gdkWindow), parent->winId()); +#endif } } if (modality != Qt::NonModal) { gdk_window_set_modal_hint(gdkWindow, true); - QGuiApplicationPrivate::showModalWindow(this); } gtk_widget_show(gtkWidget); @@ -125,30 +140,20 @@ bool QGtk3Dialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWind void QGtk3Dialog::hide() { - QGuiApplicationPrivate::hideModalWindow(this); gtk_widget_hide(gtkWidget); } -void QGtk3Dialog::onResponse(QGtk3Dialog *dialog, int response) +void QGtk3Dialog::onResponse(QPlatformDialogHelper *helper, int response) { if (response == GTK_RESPONSE_OK) - emit dialog->accept(); + emit helper->accept(); else - emit dialog->reject(); -} - -void QGtk3Dialog::onParentWindowDestroyed() -{ - // The QGtk3*DialogHelper classes own this object. Make sure the parent doesn't delete it. - setParent(nullptr); + emit helper->reject(); } QGtk3ColorDialogHelper::QGtk3ColorDialogHelper() { - d.reset(new QGtk3Dialog(gtk_color_chooser_dialog_new("", nullptr))); - connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted())); - connect(d.data(), SIGNAL(reject()), this, SIGNAL(reject())); - + d.reset(new QGtk3Dialog(gtk_color_chooser_dialog_new("", nullptr), this)); g_signal_connect_swapped(d->gtkDialog(), "notify::rgba", G_CALLBACK(onColorChanged), this); } @@ -193,11 +198,6 @@ QColor QGtk3ColorDialogHelper::currentColor() const return QColor::fromRgbF(gdkColor.red, gdkColor.green, gdkColor.blue, gdkColor.alpha); } -void QGtk3ColorDialogHelper::onAccepted() -{ - emit accept(); -} - void QGtk3ColorDialogHelper::onColorChanged(QGtk3ColorDialogHelper *dialog) { emit dialog->currentColorChanged(dialog->currentColor()); @@ -217,10 +217,7 @@ QGtk3FileDialogHelper::QGtk3FileDialogHelper() GTK_FILE_CHOOSER_ACTION_OPEN, qUtf8Printable(QGtk3Theme::defaultStandardButtonText(QPlatformDialogHelper::Cancel)), GTK_RESPONSE_CANCEL, qUtf8Printable(QGtk3Theme::defaultStandardButtonText(QPlatformDialogHelper::Ok)), GTK_RESPONSE_OK, - NULL))); - - connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted())); - connect(d.data(), SIGNAL(reject()), this, SIGNAL(reject())); + NULL), this)); g_signal_connect(GTK_FILE_CHOOSER(d->gtkDialog()), "selection-changed", G_CALLBACK(onSelectionChanged), this); g_signal_connect_swapped(GTK_FILE_CHOOSER(d->gtkDialog()), "current-folder-changed", G_CALLBACK(onCurrentFolderChanged), this); @@ -343,11 +340,6 @@ QString QGtk3FileDialogHelper::selectedNameFilter() const return _filterNames.value(gtkFilter); } -void QGtk3FileDialogHelper::onAccepted() -{ - emit accept(); -} - void QGtk3FileDialogHelper::onSelectionChanged(GtkDialog *gtkDialog, QGtk3FileDialogHelper *helper) { QString selection; @@ -503,10 +495,7 @@ void QGtk3FileDialogHelper::setNameFilters(const QStringList &filters) QGtk3FontDialogHelper::QGtk3FontDialogHelper() { - d.reset(new QGtk3Dialog(gtk_font_chooser_dialog_new("", nullptr))); - connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted())); - connect(d.data(), SIGNAL(reject()), this, SIGNAL(reject())); - + d.reset(new QGtk3Dialog(gtk_font_chooser_dialog_new("", nullptr), this)); g_signal_connect_swapped(d->gtkDialog(), "notify::font", G_CALLBACK(onFontChanged), this); } @@ -610,11 +599,6 @@ QFont QGtk3FontDialogHelper::currentFont() const return font; } -void QGtk3FontDialogHelper::onAccepted() -{ - emit accept(); -} - void QGtk3FontDialogHelper::onFontChanged(QGtk3FontDialogHelper *dialog) { emit dialog->currentFontChanged(dialog->currentFont()); @@ -631,5 +615,3 @@ void QGtk3FontDialogHelper::applyOptions() QT_END_NAMESPACE #include "moc_qgtk3dialoghelpers.cpp" - -#include "qgtk3dialoghelpers.moc" diff --git a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h index e5c4c72539..89f48d8b01 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h +++ b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h @@ -35,9 +35,6 @@ public: void setCurrentColor(const QColor &color) override; QColor currentColor() const override; -private Q_SLOTS: - void onAccepted(); - private: static void onColorChanged(QGtk3ColorDialogHelper *helper); void applyOptions(); @@ -66,9 +63,6 @@ public: void selectNameFilter(const QString &filter) override; QString selectedNameFilter() const override; -private Q_SLOTS: - void onAccepted(); - private: static void onSelectionChanged(GtkDialog *dialog, QGtk3FileDialogHelper *helper); static void onCurrentFolderChanged(QGtk3FileDialogHelper *helper); @@ -102,9 +96,6 @@ public: void setCurrentFont(const QFont &font) override; QFont currentFont() const override; -private Q_SLOTS: - void onAccepted(); - private: static void onFontChanged(QGtk3FontDialogHelper *helper); void applyOptions(); diff --git a/src/plugins/platformthemes/gtk3/qgtk3interface.cpp b/src/plugins/platformthemes/gtk3/qgtk3interface.cpp new file mode 100644 index 0000000000..a35e211fbf --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3interface.cpp @@ -0,0 +1,702 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + + +#include "qgtk3interface_p.h" +#include "qgtk3storage_p.h" +#include <QtCore/QMetaEnum> +#include <QtCore/QFileInfo> +#include <QtGui/QFontDatabase> + +QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQGtk3Interface, "qt.qpa.gtk"); + + +// Callback for gnome event loop has to be static +static QGtk3Storage *m_storage = nullptr; + +QGtk3Interface::QGtk3Interface(QGtk3Storage *s) +{ + initColorMap(); + + if (!s) { + qCDebug(lcQGtk3Interface) << "QGtk3Interface instantiated without QGtk3Storage." + << "No reaction to runtime theme changes."; + return; + } + + // Connect to the GTK settings changed signal + auto handleThemeChange = [] { + if (m_storage) + m_storage->handleThemeChange(); + }; + + GtkSettings *settings = gtk_settings_get_default(); + const gboolean success = g_signal_connect(settings, "notify::gtk-theme-name", + G_CALLBACK(handleThemeChange), nullptr); + if (success == FALSE) { + qCDebug(lcQGtk3Interface) << "Connection to theme change signal failed." + << "No reaction to runtime theme changes."; + } else { + m_storage = s; + } +} + +QGtk3Interface::~QGtk3Interface() +{ + // Ignore theme changes when destructor is reached + m_storage = nullptr; + + // QGtkWidgets have to be destroyed manually + for (auto v : cache) + gtk_widget_destroy(v.second); +} + +/*! + \internal + \brief Converts a string into the GtkStateFlags enum. + + Converts a string formatted GTK color \param state into an enum value. + Returns an integer corresponding to GtkStateFlags. + Returns -1 if \param state does not correspond to a valid enum key. + */ +int QGtk3Interface::toGtkState(const QString &state) +{ +#define CASE(x) \ + if (QLatin1String(QByteArray(state.toLatin1())) == #x ##_L1) \ + return GTK_STATE_FLAG_ ##x + +#define CONVERT\ + CASE(NORMAL);\ + CASE(ACTIVE);\ + CASE(PRELIGHT);\ + CASE(SELECTED);\ + CASE(INSENSITIVE);\ + CASE(INCONSISTENT);\ + CASE(FOCUSED);\ + CASE(BACKDROP);\ + CASE(DIR_LTR);\ + CASE(DIR_RTL);\ + CASE(LINK);\ + CASE(VISITED);\ + CASE(CHECKED);\ + CASE(DROP_ACTIVE) + + CONVERT; + return -1; +#undef CASE +} + +/*! + \internal + \brief Returns \param state converted into a string. + */ +const QLatin1String QGtk3Interface::fromGtkState(GtkStateFlags state) +{ +#define CASE(x) case GTK_STATE_FLAG_ ##x: return QLatin1String(#x) + switch (state) { + CONVERT; + } + Q_UNREACHABLE(); +#undef CASE +#undef CONVERT +} + +/*! + \internal + \brief Populates the internal map used to find a GTK color's source and fallback generic color. + */ +void QGtk3Interface::initColorMap() +{ + #define SAVE(src, state, prop, def)\ + {ColorKey({QGtkColorSource::src, GTK_STATE_FLAG_ ##state}), ColorValue({#prop ##_L1, QGtkColorDefault::def})} + + gtkColorMap = ColorMap { + SAVE(Foreground, NORMAL, theme_fg_color, Foreground), + SAVE(Foreground, BACKDROP, theme_unfocused_selected_fg_color, Foreground), + SAVE(Foreground, INSENSITIVE, insensitive_fg_color, Foreground), + SAVE(Foreground, SELECTED, theme_selected_fg_color, Foreground), + SAVE(Foreground, ACTIVE, theme_unfocused_fg_color, Foreground), + SAVE(Text, NORMAL, theme_text_color, Foreground), + SAVE(Text, ACTIVE, theme_unfocused_text_color, Foreground), + SAVE(Base, NORMAL, theme_base_color, Background), + SAVE(Base, INSENSITIVE, insensitive_base_color, Background), + SAVE(Background, NORMAL, theme_bg_color, Background), + SAVE(Background, SELECTED, theme_selected_bg_color, Background), + SAVE(Background, INSENSITIVE, insensitive_bg_color, Background), + SAVE(Background, ACTIVE, theme_unfocused_bg_color, Background), + SAVE(Background, BACKDROP, theme_unfocused_selected_bg_color, Background), + SAVE(Border, NORMAL, borders, Border), + SAVE(Border, ACTIVE, unfocused_borders, Border) + }; +#undef SAVE + + qCDebug(lcQGtk3Interface) << "Color map populated from defaults."; +} + +/*! + \internal + \brief Returns a QImage corresponding to \param standardPixmap. + + A QImage (not a QPixmap) is returned so it can be cached and re-scaled in case the pixmap is + requested multiple times with different resolutions. + + \note Rather than defaulting to a QImage(), all QPlatformTheme::StandardPixmap enum values have + been mentioned explicitly. + That way they can be covered more easily in case additional icons are provided by GTK. + */ +QImage QGtk3Interface::standardPixmap(QPlatformTheme::StandardPixmap standardPixmap) const +{ + switch (standardPixmap) { + case QPlatformTheme::DialogDiscardButton: + return qt_gtk_get_icon(GTK_STOCK_DELETE); + case QPlatformTheme::DialogOkButton: + return qt_gtk_get_icon(GTK_STOCK_OK); + case QPlatformTheme::DialogCancelButton: + return qt_gtk_get_icon(GTK_STOCK_CANCEL); + case QPlatformTheme::DialogYesButton: + return qt_gtk_get_icon(GTK_STOCK_YES); + case QPlatformTheme::DialogNoButton: + return qt_gtk_get_icon(GTK_STOCK_NO); + case QPlatformTheme::DialogOpenButton: + return qt_gtk_get_icon(GTK_STOCK_OPEN); + case QPlatformTheme::DialogCloseButton: + return qt_gtk_get_icon(GTK_STOCK_CLOSE); + case QPlatformTheme::DialogApplyButton: + return qt_gtk_get_icon(GTK_STOCK_APPLY); + case QPlatformTheme::DialogSaveButton: + return qt_gtk_get_icon(GTK_STOCK_SAVE); + case QPlatformTheme::MessageBoxWarning: + return qt_gtk_get_icon(GTK_STOCK_DIALOG_WARNING); + case QPlatformTheme::MessageBoxQuestion: + return qt_gtk_get_icon(GTK_STOCK_DIALOG_QUESTION); + case QPlatformTheme::MessageBoxInformation: + return qt_gtk_get_icon(GTK_STOCK_DIALOG_INFO); + case QPlatformTheme::MessageBoxCritical: + return qt_gtk_get_icon(GTK_STOCK_DIALOG_ERROR); + case QPlatformTheme::CustomBase: + case QPlatformTheme::TitleBarMenuButton: + case QPlatformTheme::TitleBarMinButton: + case QPlatformTheme::TitleBarMaxButton: + case QPlatformTheme::TitleBarCloseButton: + case QPlatformTheme::TitleBarNormalButton: + case QPlatformTheme::TitleBarShadeButton: + case QPlatformTheme::TitleBarUnshadeButton: + case QPlatformTheme::TitleBarContextHelpButton: + case QPlatformTheme::DockWidgetCloseButton: + case QPlatformTheme::DesktopIcon: + case QPlatformTheme::TrashIcon: + case QPlatformTheme::ComputerIcon: + case QPlatformTheme::DriveFDIcon: + case QPlatformTheme::DriveHDIcon: + case QPlatformTheme::DriveCDIcon: + case QPlatformTheme::DriveDVDIcon: + case QPlatformTheme::DriveNetIcon: + case QPlatformTheme::DirOpenIcon: + case QPlatformTheme::DirClosedIcon: + case QPlatformTheme::DirLinkIcon: + case QPlatformTheme::DirLinkOpenIcon: + case QPlatformTheme::FileIcon: + case QPlatformTheme::FileLinkIcon: + case QPlatformTheme::ToolBarHorizontalExtensionButton: + case QPlatformTheme::ToolBarVerticalExtensionButton: + case QPlatformTheme::FileDialogStart: + case QPlatformTheme::FileDialogEnd: + case QPlatformTheme::FileDialogToParent: + case QPlatformTheme::FileDialogNewFolder: + case QPlatformTheme::FileDialogDetailedView: + case QPlatformTheme::FileDialogInfoView: + case QPlatformTheme::FileDialogContentsView: + case QPlatformTheme::FileDialogListView: + case QPlatformTheme::FileDialogBack: + case QPlatformTheme::DirIcon: + case QPlatformTheme::DialogHelpButton: + case QPlatformTheme::DialogResetButton: + case QPlatformTheme::ArrowUp: + case QPlatformTheme::ArrowDown: + case QPlatformTheme::ArrowLeft: + case QPlatformTheme::ArrowRight: + case QPlatformTheme::ArrowBack: + case QPlatformTheme::ArrowForward: + case QPlatformTheme::DirHomeIcon: + case QPlatformTheme::CommandLink: + case QPlatformTheme::VistaShield: + case QPlatformTheme::BrowserReload: + case QPlatformTheme::BrowserStop: + case QPlatformTheme::MediaPlay: + case QPlatformTheme::MediaStop: + case QPlatformTheme::MediaPause: + case QPlatformTheme::MediaSkipForward: + case QPlatformTheme::MediaSkipBackward: + case QPlatformTheme::MediaSeekForward: + case QPlatformTheme::MediaSeekBackward: + case QPlatformTheme::MediaVolume: + case QPlatformTheme::MediaVolumeMuted: + case QPlatformTheme::LineEditClearButton: + case QPlatformTheme::DialogYesToAllButton: + case QPlatformTheme::DialogNoToAllButton: + case QPlatformTheme::DialogSaveAllButton: + case QPlatformTheme::DialogAbortButton: + case QPlatformTheme::DialogRetryButton: + case QPlatformTheme::DialogIgnoreButton: + case QPlatformTheme::RestoreDefaultsButton: + case QPlatformTheme::TabCloseButton: + case QPlatformTheme::NStandardPixmap: + return QImage(); + } + Q_UNREACHABLE(); +} + +/*! + \internal + \brief Returns a QImage for a given GTK \param iconName. + */ +QImage QGtk3Interface::qt_gtk_get_icon(const char* iconName) const +{ + GtkIconSet* iconSet = gtk_icon_factory_lookup_default (iconName); + GdkPixbuf* icon = gtk_icon_set_render_icon_pixbuf(iconSet, context(), GTK_ICON_SIZE_DIALOG); + return qt_convert_gdk_pixbuf(icon); +} + +/*! + \internal + \brief Returns a QImage converted from the GDK pixel buffer \param buf. + + The ability to convert GdkPixbuf to QImage relies on the following assumptions: + \list + \li QImage uses uchar as a data container (unasserted) + \li the types guint8 and uchar are identical (statically asserted) + \li GDK pixel buffer uses 8 bits per sample (assumed at runtime) + \li GDK pixel buffer has 4 channels (assumed at runtime) + \endlist + */ +QImage QGtk3Interface::qt_convert_gdk_pixbuf(GdkPixbuf *buf) const +{ + if (!buf) + return QImage(); + + const guint8 *gdata = gdk_pixbuf_read_pixels(buf); + static_assert(std::is_same<decltype(gdata), const uchar *>::value, + "guint8 has diverted from uchar. Code needs fixing."); + Q_ASSERT(gdk_pixbuf_get_bits_per_sample(buf) == 8); + Q_ASSERT(gdk_pixbuf_get_n_channels(buf) == 4); + const uchar *data = static_cast<const uchar *>(gdata); + + const int width = gdk_pixbuf_get_width(buf); + const int height = gdk_pixbuf_get_height(buf); + const int bpl = gdk_pixbuf_get_rowstride(buf); + QImage converted(data, width, height, bpl, QImage::Format_RGBA8888); + + // convert to more optimal format and detach to survive lifetime of buf + return converted.convertToFormat(QImage::Format_ARGB32_Premultiplied); +} + +/*! + \internal + \brief Instantiate a new GTK widget. + + Returns a pointer to a new GTK widget of \param type, allocated on the heap. + Returns nullptr of gtk_Default has is passed. + */ +GtkWidget *QGtk3Interface::qt_new_gtkWidget(QGtkWidget type) const +{ +#define CASE(Type)\ + case QGtkWidget::Type: return Type ##_new(); +#define CASEN(Type)\ + case QGtkWidget::Type: return Type ##_new(nullptr); + + switch (type) { + CASE(gtk_menu_bar) + CASE(gtk_menu) + CASE(gtk_button) + case QGtkWidget::gtk_button_box: return gtk_button_box_new(GtkOrientation::GTK_ORIENTATION_HORIZONTAL); + CASE(gtk_check_button) + CASEN(gtk_radio_button) + CASEN(gtk_frame) + CASE(gtk_statusbar) + CASE(gtk_entry) + case QGtkWidget::gtk_popup: return gtk_window_new(GTK_WINDOW_POPUP); + CASE(gtk_notebook) + CASE(gtk_toolbar) + CASE(gtk_tree_view) + CASE(gtk_combo_box) + CASE(gtk_combo_box_text) + CASE(gtk_progress_bar) + CASE(gtk_fixed) + CASE(gtk_separator_menu_item) + CASE(gtk_offscreen_window) + case QGtkWidget::gtk_Default: return nullptr; + } +#undef CASE +#undef CASEN + Q_UNREACHABLE(); +} + +/*! + \internal + \brief Read a GTK widget's color from a generic color getter. + + This method returns a generic color of \param con, a given GTK style context. + The requested color is defined by \param def and the GTK color-state \param state. + The return type is GDK color in RGBA format. + */ +GdkRGBA QGtk3Interface::genericColor(GtkStyleContext *con, GtkStateFlags state, QGtkColorDefault def) const +{ + GdkRGBA color; + +#define CASE(def, call)\ + case QGtkColorDefault::def:\ + gtk_style_context_get_ ##call(con, state, &color);\ + break; + + switch (def) { + CASE(Foreground, color) + CASE(Background, background_color) + CASE(Border, border_color) + } + return color; +#undef CASE +} + +/*! + \internal + \brief Read a GTK widget's color from a property. + + Returns a color of GTK-widget \param widget, defined by \param source and \param state. + The return type is GDK color in RGBA format. + + \note If no corresponding property can be found for \param source, the method falls back to a + suitable generic color. + */ +QColor QGtk3Interface::color(GtkWidget *widget, QGtkColorSource source, GtkStateFlags state) const +{ + GdkRGBA col; + GtkStyleContext *con = context(widget); + +#define CASE(src, def)\ + case QGtkColorSource::src: {\ + const ColorKey key = ColorKey({QGtkColorSource::src, state});\ + if (gtkColorMap.contains(key)) {\ + const ColorValue val = gtkColorMap.value(key);\ + if (!gtk_style_context_lookup_color(con, val.propertyName.toUtf8().constData(), &col)) {\ + col = genericColor(con, state, val.genericSource);\ + qCDebug(lcQGtk3Interface) << "Property name" << val.propertyName << "not found.\n"\ + << "Falling back to " << val.genericSource;\ + }\ + } else {\ + col = genericColor(con, state, QGtkColorDefault::def);\ + qCDebug(lcQGtk3Interface) << "No color source found for" << QGtkColorSource::src\ + << fromGtkState(state) << "\n Falling back to"\ + << QGtkColorDefault::def;\ + }\ + }\ + break; + + switch (source) { + CASE(Foreground, Foreground) + CASE(Background, Background) + CASE(Text, Foreground) + CASE(Base, Background) + CASE(Border, Border) + } + + return fromGdkColor(col); +#undef CASE +} + +/*! + \internal + \brief Get pointer to a GTK widget by \param type. + + Returns the pointer to a GTK widget, specified by \param type. + GTK widgets are cached, so that only one instance of each type is created. + \note + The method returns nullptr for the enum value gtk_Default. + */ +GtkWidget *QGtk3Interface::widget(QGtkWidget type) const +{ + if (type == QGtkWidget::gtk_Default) + return nullptr; + + // Return from cache + if (GtkWidget *w = cache.value(type)) + return w; + + // Create new item and cache it + GtkWidget *w = qt_new_gtkWidget(type); + cache.insert(type, w); + return w; +} + +/*! + \internal + \brief Access a GTK widget's style context. + + Returns the pointer to the style context of GTK widget \param w. + + \note If \param w is nullptr, the GTK default style context (entry style) is returned. + */ +GtkStyleContext *QGtk3Interface::context(GtkWidget *w) const +{ + if (w) + return gtk_widget_get_style_context(w); + + return gtk_widget_get_style_context(widget(QGtkWidget::gtk_entry)); +} + +/*! + \internal + \brief Create a QBrush from a GTK widget. + + Returns a QBrush corresponding to GTK widget type \param wtype, \param source and \param state. + + Brush height and width is ignored in GTK3, because brush assets (e.g. 9-patches) + can't be accessed by the GTK3 API. It's therefore unknown, if the brush relates only to colors, + or to a pixmap based style. + + */ +QBrush QGtk3Interface::brush(QGtkWidget wtype, QGtkColorSource source, GtkStateFlags state) const +{ + // FIXME: When a color's pixmap can be accessed via the GTK API, + // read it and set it in the brush. + return QBrush(color(widget(wtype), source, state)); +} + +/*! + \internal + \brief Returns the name of the current GTK theme. + */ +QString QGtk3Interface::themeName() const +{ + QString name; + + if (GtkSettings *settings = gtk_settings_get_default()) { + gchar *theme_name; + g_object_get(settings, "gtk-theme-name", &theme_name, nullptr); + name = QLatin1StringView(theme_name); + g_free(theme_name); + } + + return name; +} + +/*! + \internal + \brief Determine color scheme by colors. + + Returns the color scheme of the current GTK theme, heuristically determined by the + lightness difference between default background and foreground colors. + + \note Returns Unknown in the unlikely case that both colors have the same lightness. + */ +Qt::ColorScheme QGtk3Interface::colorSchemeByColors() const +{ + const QColor background = color(widget(QGtkWidget::gtk_Default), + QGtkColorSource::Background, + GTK_STATE_FLAG_ACTIVE); + const QColor foreground = color(widget(QGtkWidget::gtk_Default), + QGtkColorSource::Foreground, + GTK_STATE_FLAG_ACTIVE); + + if (foreground.lightness() > background.lightness()) + return Qt::ColorScheme::Dark; + if (foreground.lightness() < background.lightness()) + return Qt::ColorScheme::Light; + return Qt::ColorScheme::Unknown; +} + +/*! + \internal + \brief Map font type to GTK widget type. + + Returns the GTK widget type corresponding to the given QPlatformTheme::Font \param type. + */ +inline constexpr QGtk3Interface::QGtkWidget QGtk3Interface::toWidgetType(QPlatformTheme::Font type) +{ + switch (type) { + case QPlatformTheme::SystemFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::MenuFont: return QGtkWidget::gtk_menu; + case QPlatformTheme::MenuBarFont: return QGtkWidget::gtk_menu_bar; + case QPlatformTheme::MenuItemFont: return QGtkWidget::gtk_menu; + case QPlatformTheme::MessageBoxFont: return QGtkWidget::gtk_popup; + case QPlatformTheme::LabelFont: return QGtkWidget::gtk_popup; + case QPlatformTheme::TipLabelFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::StatusBarFont: return QGtkWidget::gtk_statusbar; + case QPlatformTheme::TitleBarFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::MdiSubWindowTitleFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::DockWidgetTitleFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::PushButtonFont: return QGtkWidget::gtk_button; + case QPlatformTheme::CheckBoxFont: return QGtkWidget::gtk_check_button; + case QPlatformTheme::RadioButtonFont: return QGtkWidget::gtk_radio_button; + case QPlatformTheme::ToolButtonFont: return QGtkWidget::gtk_button; + case QPlatformTheme::ItemViewFont: return QGtkWidget::gtk_entry; + case QPlatformTheme::ListViewFont: return QGtkWidget::gtk_tree_view; + case QPlatformTheme::HeaderViewFont: return QGtkWidget::gtk_combo_box; + case QPlatformTheme::ListBoxFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::ComboMenuItemFont: return QGtkWidget::gtk_combo_box; + case QPlatformTheme::ComboLineEditFont: return QGtkWidget::gtk_combo_box_text; + case QPlatformTheme::SmallFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::MiniFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::FixedFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::GroupBoxTitleFont: return QGtkWidget::gtk_Default; + case QPlatformTheme::TabButtonFont: return QGtkWidget::gtk_button; + case QPlatformTheme::EditorFont: return QGtkWidget::gtk_entry; + case QPlatformTheme::NFonts: return QGtkWidget::gtk_Default; + } + Q_UNREACHABLE(); +} + +/*! + \internal + \brief Convert pango \param style to QFont::Style. + */ +inline constexpr QFont::Style QGtk3Interface::toFontStyle(PangoStyle style) +{ + switch (style) { + case PANGO_STYLE_ITALIC: return QFont::StyleItalic; + case PANGO_STYLE_OBLIQUE: return QFont::StyleOblique; + case PANGO_STYLE_NORMAL: return QFont::StyleNormal; + } + // This is reached when GTK has introduced a new font style + Q_UNREACHABLE(); +} + +/*! + \internal + \brief Convert pango font \param weight to an int, representing font weight in Qt. + + Compatibility of PangoWeight is statically asserted. + The minimum (1) and maximum (1000) weight in Qt is respeced. + */ +inline constexpr int QGtk3Interface::toFontWeight(PangoWeight weight) +{ + // GTK PangoWeight can be directly converted to QFont::Weight + // unless one of the enums changes. + static_assert(PANGO_WEIGHT_THIN == 100 && PANGO_WEIGHT_ULTRAHEAVY == 1000, + "Pango font weight enum changed. Fix conversion."); + + static_assert(QFont::Thin == 100 && QFont::Black == 900, + "QFont::Weight enum changed. Fix conversion."); + + return qBound(1, static_cast<int>(weight), 1000); +} + +/*! + \internal + \brief Return a GTK styled font. + + Returns the QFont corresponding to \param type by reading the corresponding + GTK widget type's font. + + \note GTK allows to specify a non fixed font as the system's fixed font. + If a fixed font is requested, the method fixes the pitch and falls back to monospace, + unless a suitable fixed pitch font is found. + */ +QFont QGtk3Interface::font(QPlatformTheme::Font type) const +{ + GtkStyleContext *con = context(widget(toWidgetType(type))); + if (!con) + return QFont(); + + // explicitly add provider for fixed font + GtkCssProvider *cssProvider = nullptr; + if (type == QPlatformTheme::FixedFont) { + cssProvider = gtk_css_provider_new(); + gtk_style_context_add_class (con, GTK_STYLE_CLASS_MONOSPACE); + const char *fontSpec = "* {font-family: monospace;}"; + gtk_css_provider_load_from_data(cssProvider, fontSpec, -1, NULL); + gtk_style_context_add_provider(con, GTK_STYLE_PROVIDER(cssProvider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + } + + // remove monospace provider from style context and unref it + QScopeGuard guard([&](){ + if (cssProvider) { + gtk_style_context_remove_provider(con, GTK_STYLE_PROVIDER(cssProvider)); + g_object_unref(cssProvider); + } + }); + + const PangoFontDescription *gtkFont = gtk_style_context_get_font(con, GTK_STATE_FLAG_NORMAL); + if (!gtkFont) + return QFont(); + + const QString family = QString::fromLatin1(pango_font_description_get_family(gtkFont)); + if (family.isEmpty()) + return QFont(); + + const int weight = toFontWeight(pango_font_description_get_weight(gtkFont)); + + // Creating a QFont() creates a futex lockup on a theme change + // QFont doesn't have a constructor with float point size + // => create a dummy point size and set it later. + QFont font(family, 1, weight); + font.setPointSizeF(static_cast<float>(pango_font_description_get_size(gtkFont)/PANGO_SCALE)); + font.setStyle(toFontStyle(pango_font_description_get_style(gtkFont))); + + if (type == QPlatformTheme::FixedFont) { + font.setFixedPitch(true); + if (!QFontInfo(font).fixedPitch()) { + qCDebug(lcQGtk3Interface) << "No fixed pitch font found in font family" + << font.family() << ". falling back to a default" + << "fixed pitch font"; + font.setFamily("monospace"_L1); + } + } + + return font; +} + +/*! + \internal + \brief Returns a GTK styled file icon for \param fileInfo. + */ +QIcon QGtk3Interface::fileIcon(const QFileInfo &fileInfo) const +{ + GFile *file = g_file_new_for_path(fileInfo.absoluteFilePath().toLatin1().constData()); + if (!file) + return QIcon(); + + GFileInfo *info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_QUERY_INFO_NONE, nullptr, nullptr); + if (!info) { + g_object_unref(file); + return QIcon(); + } + + GIcon *icon = g_file_info_get_icon(info); + if (!icon) { + g_object_unref(file); + g_object_unref(info); + return QIcon(); + } + + GtkIconTheme *theme = gtk_icon_theme_get_default(); + GtkIconInfo *iconInfo = gtk_icon_theme_lookup_by_gicon(theme, icon, 16, + GTK_ICON_LOOKUP_FORCE_SIZE); + if (!iconInfo) { + g_object_unref(file); + g_object_unref(info); + return QIcon(); + } + + GdkPixbuf *buf = gtk_icon_info_load_icon(iconInfo, nullptr); + QImage image = qt_convert_gdk_pixbuf(buf); + g_object_unref(file); + g_object_unref(info); + g_object_unref(buf); + return QIcon(QPixmap::fromImage(image)); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3interface_p.h b/src/plugins/platformthemes/gtk3/qgtk3interface_p.h new file mode 100644 index 0000000000..c43932a4fa --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3interface_p.h @@ -0,0 +1,211 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QGTK3INTERFACE_H +#define QGTK3INTERFACE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QString> +#include <QtCore/QCache> +#include <private/qflatmap_p.h> +#include <QtCore/QObject> +#include <QtGui/QIcon> +#include <QtGui/QPalette> +#include <QtWidgets/QWidget> +#include <QtCore/QLoggingCategory> +#include <QtGui/QPixmap> +#include <qpa/qplatformtheme.h> + +#undef signals // Collides with GTK symbols +#include <gtk/gtk.h> +#include <gdk/gdk.h> +#include <glib.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcQGtk3Interface); + +using namespace Qt::StringLiterals; + +class QGtk3Storage; + +/*! + \internal + \brief The QGtk3Interface class centralizes communication with the GTK3 library. + + By encapsulating all GTK version specific syntax and conversions, it makes Qt's GTK theme + independent from GTK versions. + + \note + Including GTK3 headers requires #undef signals, which disables Qt signal/slot handling. + */ + +class QGtk3Interface +{ + Q_GADGET +public: + QGtk3Interface(QGtk3Storage *); + ~QGtk3Interface(); + + /*! + * \internal + \enum QGtk3Interface::QGtkWidget + \brief Represents GTK widget types used to obtain color information. + + \note The enum value gtk_Default refers to the GTK default style, rather than to a specific widget. + */ + enum class QGtkWidget { + gtk_menu_bar, + gtk_menu, + gtk_button, + gtk_button_box, + gtk_check_button, + gtk_radio_button, + gtk_frame, + gtk_statusbar, + gtk_entry, + gtk_popup, + gtk_notebook, + gtk_toolbar, + gtk_tree_view, + gtk_combo_box, + gtk_combo_box_text, + gtk_progress_bar, + gtk_fixed, + gtk_separator_menu_item, + gtk_Default, + gtk_offscreen_window + }; + Q_ENUM(QGtkWidget) + + /*! + \internal + \enum QGtk3Interface::QGtkColorSource + \brief The QGtkColorSource enum represents the source of a color within a GTK widgets style context. + + If the current GTK theme provides such a color for a given widget, the color can be read + from the style context by passing the enum's key as a property name to the GTK method + gtk_style_context_lookup_color. The method will return false, if no color has been found. + */ + enum class QGtkColorSource { + Foreground, + Background, + Text, + Base, + Border + }; + Q_ENUM(QGtkColorSource) + + /*! + \internal + \enum QGtk3Interface::QGtkColorDefault + \brief The QGtkColorDefault enum represents generic GTK colors. + + The GTK3 methods gtk_style_context_get_color, gtk_style_context_get_background_color, and + gtk_style_context_get_foreground_color always return the respective colors with a widget's + style context. Unless set as a property by the current GTK theme, GTK's default colors will + be returned. + These generic default colors, represented by the GtkColorDefault enum, are used as a + back, if a specific color property is requested but not defined in the current GTK theme. + */ + enum class QGtkColorDefault { + Foreground, + Background, + Border + }; + Q_ENUM(QGtkColorDefault) + + // Create a brush from GTK widget type, color source and color state + QBrush brush(QGtkWidget wtype, QGtkColorSource source, GtkStateFlags state) const; + + // Font & icon getters + QImage standardPixmap(QPlatformTheme::StandardPixmap standardPixmap) const; + QFont font(QPlatformTheme::Font type) const; + QIcon fileIcon(const QFileInfo &fileInfo) const; + + // Return current GTK theme name + QString themeName() const; + + // Derive color scheme from default colors + Qt::ColorScheme colorSchemeByColors() const; + + // Convert GTK state to/from string + static int toGtkState(const QString &state); + static const QLatin1String fromGtkState(GtkStateFlags state); + +private: + + // Map colors to GTK property names and default to generic color getters + struct ColorKey { + QGtkColorSource colorSource = QGtkColorSource::Background; + GtkStateFlags state = GTK_STATE_FLAG_NORMAL; + + // struct becomes key of a map, so operator< is needed + bool operator<(const ColorKey& other) const { + return std::tie(colorSource, state) < + std::tie(other.colorSource, other.state); + } + + QDebug operator<<(QDebug dbg) + { + return dbg << "QGtk3Interface::ColorKey(colorSource=" << colorSource << ", GTK state=" << fromGtkState(state) << ")"; + } + }; + + struct ColorValue { + QString propertyName = QString(); + QGtkColorDefault genericSource = QGtkColorDefault::Background; + + QDebug operator<<(QDebug dbg) + { + return dbg << "QGtk3Interface::ColorValue(propertyName=" << propertyName << ", genericSource=" << genericSource << ")"; + } + }; + + typedef QFlatMap<ColorKey, ColorValue> ColorMap; + ColorMap gtkColorMap; + void initColorMap(); + + GdkRGBA genericColor(GtkStyleContext *con, GtkStateFlags state, QGtkColorDefault def) const; + + // Cache for GTK widgets + mutable QFlatMap<QGtkWidget, GtkWidget *> cache; + + // Converters for GTK icon and GDK pixbuf + QImage qt_gtk_get_icon(const char *iconName) const; + QImage qt_convert_gdk_pixbuf(GdkPixbuf *buf) const; + + // Create new GTK widget object + GtkWidget *qt_new_gtkWidget(QGtkWidget type) const; + + // Deliver GTK Widget from cache or create new + GtkWidget *widget(QGtkWidget type) const; + + // Get a GTK widget's style context. Default settings style context if nullptr + GtkStyleContext *context(GtkWidget *widget = nullptr) const; + + // Convert GTK color into QColor + static inline QColor fromGdkColor (const GdkRGBA &c) + { return QColor::fromRgbF(c.red, c.green, c.blue, c.alpha); } + + // get a QColor of a GTK widget (default settings style if nullptr) + QColor color (GtkWidget *widget, QGtkColorSource source, GtkStateFlags state) const; + + // Mappings for GTK fonts + inline static constexpr QGtkWidget toWidgetType(QPlatformTheme::Font); + inline static constexpr QFont::Style toFontStyle(PangoStyle style); + inline static constexpr int toFontWeight(PangoWeight weight); + +}; +QT_END_NAMESPACE +#endif // QGTK3INTERFACE_H diff --git a/src/plugins/platformthemes/gtk3/qgtk3json.cpp b/src/plugins/platformthemes/gtk3/qgtk3json.cpp new file mode 100644 index 0000000000..eb81e563be --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3json.cpp @@ -0,0 +1,404 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qgtk3json_p.h" +#include <QtCore/QFile> +#include <QMetaEnum> + +QT_BEGIN_NAMESPACE + +QLatin1String QGtk3Json::fromPalette(QPlatformTheme::Palette palette) +{ + return QLatin1String(QMetaEnum::fromType<QPlatformTheme::Palette>().valueToKey(static_cast<int>(palette))); +} + +QLatin1String QGtk3Json::fromGtkState(GtkStateFlags state) +{ + return QGtk3Interface::fromGtkState(state); +} + +QLatin1String fromColor(const QColor &color) +{ + return QLatin1String(QByteArray(color.name(QColor::HexRgb).toLatin1())); +} + +QLatin1String QGtk3Json::fromColorRole(QPalette::ColorRole role) +{ + return QLatin1String(QMetaEnum::fromType<QPalette::ColorRole>().valueToKey(static_cast<int>(role))); +} + +QLatin1String QGtk3Json::fromColorGroup(QPalette::ColorGroup group) +{ + return QLatin1String(QMetaEnum::fromType<QPalette::ColorGroup>().valueToKey(static_cast<int>(group))); +} + +QLatin1String QGtk3Json::fromGdkSource(QGtk3Interface::QGtkColorSource source) +{ + return QLatin1String(QMetaEnum::fromType<QGtk3Interface::QGtkColorSource>().valueToKey(static_cast<int>(source))); +} + +QLatin1String QGtk3Json::fromWidgetType(QGtk3Interface::QGtkWidget widgetType) +{ + return QLatin1String(QMetaEnum::fromType<QGtk3Interface::QGtkWidget>().valueToKey(static_cast<int>(widgetType))); +} + +QLatin1String QGtk3Json::fromColorScheme(Qt::ColorScheme app) +{ + return QLatin1String(QMetaEnum::fromType<Qt::ColorScheme>().valueToKey(static_cast<int>(app))); +} + +#define CONVERT(type, key, def)\ + bool ok;\ + const int intVal = QMetaEnum::fromType<type>().keyToValue(key.toLatin1().constData(), &ok);\ + return ok ? static_cast<type>(intVal) : type::def + +Qt::ColorScheme QGtk3Json::toColorScheme(const QString &colorScheme) +{ + CONVERT(Qt::ColorScheme, colorScheme, Unknown); +} + +QPlatformTheme::Palette QGtk3Json::toPalette(const QString &palette) +{ + CONVERT(QPlatformTheme::Palette, palette, NPalettes); +} + +GtkStateFlags QGtk3Json::toGtkState(const QString &type) +{ + int i = QGtk3Interface::toGtkState(type); + if (i < 0) + return GTK_STATE_FLAG_NORMAL; + return static_cast<GtkStateFlags>(i); +} + +QColor toColor(const QStringView &color) +{ + return QColor::fromString(color); +} + +QPalette::ColorRole QGtk3Json::toColorRole(const QString &role) +{ + CONVERT(QPalette::ColorRole, role, NColorRoles); +} + +QPalette::ColorGroup QGtk3Json::toColorGroup(const QString &group) +{ + CONVERT(QPalette::ColorGroup, group, NColorGroups); +} + +QGtk3Interface::QGtkColorSource QGtk3Json::toGdkSource(const QString &source) +{ + CONVERT(QGtk3Interface::QGtkColorSource, source, Background); +} + +QLatin1String QGtk3Json::fromSourceType(QGtk3Storage::SourceType sourceType) +{ + return QLatin1String(QMetaEnum::fromType<QGtk3Storage::SourceType>().valueToKey(static_cast<int>(sourceType))); +} + +QGtk3Storage::SourceType QGtk3Json::toSourceType(const QString &sourceType) +{ + CONVERT(QGtk3Storage::SourceType, sourceType, Invalid); +} + +QGtk3Interface::QGtkWidget QGtk3Json::toWidgetType(const QString &widgetType) +{ + CONVERT(QGtk3Interface::QGtkWidget, widgetType, gtk_offscreen_window); +} + +#undef CONVERT + +bool QGtk3Json::save(const QGtk3Storage::PaletteMap &map, const QString &fileName, + QJsonDocument::JsonFormat format) +{ + QJsonDocument doc = save(map); + if (doc.isEmpty()) { + qWarning() << "Nothing to save to" << fileName; + return false; + } + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "Unable to open file" << fileName << "for writing."; + return false; + } + + if (!file.write(doc.toJson(format))) { + qWarning() << "Unable to serialize Json document."; + return false; + } + + file.close(); + qInfo() << "Saved mapping data to" << fileName; + return true; +} + +const QJsonDocument QGtk3Json::save(const QGtk3Storage::PaletteMap &map) +{ + QJsonObject paletteObject; + for (auto paletteIterator = map.constBegin(); paletteIterator != map.constEnd(); + ++paletteIterator) { + const QGtk3Storage::BrushMap &bm = paletteIterator.value(); + QFlatMap<QPalette::ColorRole, QGtk3Storage::BrushMap> brushMaps; + for (auto brushIterator = bm.constBegin(); brushIterator != bm.constEnd(); + ++brushIterator) { + const QPalette::ColorRole role = brushIterator.key().colorRole; + if (brushMaps.contains(role)) { + brushMaps.value(role).insert(brushIterator.key(), brushIterator.value()); + } else { + QGtk3Storage::BrushMap newMap; + newMap.insert(brushIterator.key(), brushIterator.value()); + brushMaps.insert(role, newMap); + } + } + + QJsonObject brushArrayObject; + for (auto brushMapIterator = brushMaps.constBegin(); + brushMapIterator != brushMaps.constEnd(); ++brushMapIterator) { + + QJsonArray brushArray; + int brushIndex = 0; + const QGtk3Storage::BrushMap &bm = brushMapIterator.value(); + for (auto brushIterator = bm.constBegin(); brushIterator != bm.constEnd(); + ++brushIterator) { + QJsonObject brushObject; + const QGtk3Storage::TargetBrush tb = brushIterator.key(); + QGtk3Storage::Source s = brushIterator.value(); + brushObject.insert(ceColorGroup, fromColorGroup(tb.colorGroup)); + brushObject.insert(ceColorScheme, fromColorScheme(tb.colorScheme)); + brushObject.insert(ceSourceType, fromSourceType(s.sourceType)); + + QJsonObject sourceObject; + switch (s.sourceType) { + case QGtk3Storage::SourceType::Gtk: { + sourceObject.insert(ceGtkWidget, fromWidgetType(s.gtk3.gtkWidgetType)); + sourceObject.insert(ceGdkSource, fromGdkSource(s.gtk3.source)); + sourceObject.insert(ceGtkState, fromGtkState(s.gtk3.state)); + sourceObject.insert(ceWidth, s.gtk3.width); + sourceObject.insert(ceHeight, s.gtk3.height); + } + break; + + case QGtk3Storage::SourceType::Fixed: { + QJsonObject fixedObject; + fixedObject.insert(ceColor, s.fix.fixedBrush.color().name()); + fixedObject.insert(ceWidth, s.fix.fixedBrush.texture().width()); + fixedObject.insert(ceHeight, s.fix.fixedBrush.texture().height()); + sourceObject.insert(ceBrush, fixedObject); + } + break; + + case QGtk3Storage::SourceType::Modified:{ + sourceObject.insert(ceColorGroup, fromColorGroup(s.rec.colorGroup)); + sourceObject.insert(ceColorRole, fromColorRole(s.rec.colorRole)); + sourceObject.insert(ceColorScheme, fromColorScheme(s.rec.colorScheme)); + sourceObject.insert(ceRed, s.rec.deltaRed); + sourceObject.insert(ceGreen, s.rec.deltaGreen); + sourceObject.insert(ceBlue, s.rec.deltaBlue); + sourceObject.insert(ceWidth, s.rec.width); + sourceObject.insert(ceHeight, s.rec.height); + sourceObject.insert(ceLighter, s.rec.lighter); + } + break; + + case QGtk3Storage::SourceType::Invalid: + break; + } + + brushObject.insert(ceData, sourceObject); + brushArray.insert(brushIndex, brushObject); + ++brushIndex; + } + brushArrayObject.insert(fromColorRole(brushMapIterator.key()), brushArray); + } + paletteObject.insert(fromPalette(paletteIterator.key()), brushArrayObject); + } + + QJsonObject top; + top.insert(cePalettes, paletteObject); + return paletteObject.keys().isEmpty() ? QJsonDocument() : QJsonDocument(top); +} + +bool QGtk3Json::load(QGtk3Storage::PaletteMap &map, const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(lcQGtk3Interface) << "Unable to open file:" << fileName; + return false; + } + + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err); + if (err.error != QJsonParseError::NoError) { + qWarning(lcQGtk3Interface) << "Unable to parse Json document from" << fileName + << err.error << err.errorString(); + return false; + } + + if (Q_LIKELY(load(map, doc))) { + qInfo() << "GTK mapping successfully imported from" << fileName; + return true; + } + + qWarning() << "File" << fileName << "could not be loaded."; + return false; +} + +bool QGtk3Json::load(QGtk3Storage::PaletteMap &map, const QJsonDocument &doc) +{ +#define GETSTR(obj, key)\ + if (!obj.contains(key)) {\ + qCInfo(lcQGtk3Interface) << key << "missing for palette" << paletteName\ + << ", Brush" << colorRoleName;\ + return false;\ + }\ + value = obj[key].toString() + +#define GETINT(obj, key, var) GETSTR(obj, key);\ + if (!obj[key].isDouble()) {\ + qCInfo(lcQGtk3Interface) << key << "type mismatch" << value\ + << "is not an integer!"\ + << "(Palette" << paletteName << "), Brush" << colorRoleName;\ + return false;\ + }\ + const int var = obj[key].toInt() + + map.clear(); + const QJsonObject top(doc.object()); + if (doc.isEmpty() || top.isEmpty() || !top.contains(cePalettes)) { + qCInfo(lcQGtk3Interface) << "Document does not contain Palettes."; + return false; + } + + const QStringList &paletteList = top[cePalettes].toObject().keys(); + for (const QString &paletteName : paletteList) { + bool ok; + const int intVal = QMetaEnum::fromType<QPlatformTheme::Palette>().keyToValue(paletteName + .toLatin1().constData(), &ok); + if (!ok) { + qCInfo(lcQGtk3Interface) << "Invalid Palette name:" << paletteName; + return false; + } + const QJsonObject &paletteObject = top[cePalettes][paletteName].toObject(); + const QStringList &brushList = paletteObject.keys(); + if (brushList.isEmpty()) { + qCInfo(lcQGtk3Interface) << "Palette" << paletteName << "does not contain brushes"; + return false; + } + + const QPlatformTheme::Palette paletteType = static_cast<QPlatformTheme::Palette>(intVal); + QGtk3Storage::BrushMap brushes; + const QStringList &colorRoles = paletteObject.keys(); + for (const QString &colorRoleName : colorRoles) { + const int intVal = QMetaEnum::fromType<QPalette::ColorRole>().keyToValue(colorRoleName + .toLatin1().constData(), &ok); + if (!ok) { + qCInfo(lcQGtk3Interface) << "Palette" << paletteName + << "contains invalid color role" << colorRoleName; + return false; + } + const QPalette::ColorRole colorRole = static_cast<QPalette::ColorRole>(intVal); + const QJsonArray &brushArray = paletteObject[colorRoleName].toArray(); + for (int brushIndex = 0; brushIndex < brushArray.size(); ++brushIndex) { + const QJsonObject brushObject = brushArray.at(brushIndex).toObject(); + if (brushObject.isEmpty()) { + qCInfo(lcQGtk3Interface) << "Brush specification missing at for palette" + << paletteName << ", Brush" << colorRoleName; + return false; + } + + QString value; + GETSTR(brushObject, ceSourceType); + const QGtk3Storage::SourceType sourceType = toSourceType(value); + GETSTR(brushObject, ceColorGroup); + const QPalette::ColorGroup colorGroup = toColorGroup(value); + GETSTR(brushObject, ceColorScheme); + const Qt::ColorScheme colorScheme = toColorScheme(value); + QGtk3Storage::TargetBrush tb(colorGroup, colorRole, colorScheme); + QGtk3Storage::Source s; + + if (!brushObject.contains(ceData) || !brushObject[ceData].isObject()) { + qCInfo(lcQGtk3Interface) << "Source specification missing for palette" << paletteName + << "Brush" << colorRoleName; + return false; + } + const QJsonObject &sourceObject = brushObject[ceData].toObject(); + + switch (sourceType) { + case QGtk3Storage::SourceType::Gtk: { + GETSTR(sourceObject, ceGdkSource); + const QGtk3Interface::QGtkColorSource gtkSource = toGdkSource(value); + GETSTR(sourceObject, ceGtkState); + const GtkStateFlags gtkState = toGtkState(value); + GETSTR(sourceObject, ceGtkWidget); + const QGtk3Interface::QGtkWidget widgetType = toWidgetType(value); + GETINT(sourceObject, ceHeight, height); + GETINT(sourceObject, ceWidth, width); + s = QGtk3Storage::Source(widgetType, gtkSource, gtkState, width, height); + } + break; + + case QGtk3Storage::SourceType::Fixed: { + if (!sourceObject.contains(ceBrush)) { + qCInfo(lcQGtk3Interface) << "Fixed brush specification missing for palette" << paletteName + << "Brush" << colorRoleName; + return false; + } + const QJsonObject &fixedSource = sourceObject[ceBrush].toObject(); + GETINT(fixedSource, ceWidth, width); + GETINT(fixedSource, ceHeight, height); + GETSTR(fixedSource, ceColor); + const QColor color(value); + if (!color.isValid()) { + qCInfo(lcQGtk3Interface) << "Color" << value << "can't be parsed for:" << paletteName + << "Brush" << colorRoleName; + return false; + } + const QBrush fixedBrush = (width < 0 && height < 0) + ? QBrush(color, QPixmap(width, height)) + : QBrush(color); + s = QGtk3Storage::Source(fixedBrush); + } + break; + + case QGtk3Storage::SourceType::Modified: { + GETSTR(sourceObject, ceColorGroup); + const QPalette::ColorGroup colorGroup = toColorGroup(value); + GETSTR(sourceObject, ceColorRole); + const QPalette::ColorRole colorRole = toColorRole(value); + GETSTR(sourceObject, ceColorScheme); + const Qt::ColorScheme colorScheme = toColorScheme(value); + GETINT(sourceObject, ceLighter, lighter); + GETINT(sourceObject, ceRed, red); + GETINT(sourceObject, ceBlue, blue); + GETINT(sourceObject, ceGreen, green); + s = QGtk3Storage::Source(colorGroup, colorRole, colorScheme, + lighter, red, green, blue); + } + break; + + case QGtk3Storage::SourceType::Invalid: + qInfo(lcQGtk3Interface) << "Invalid source type for palette" << paletteName + << "Brush." << colorRoleName; + return false; + } + brushes.insert(tb, s); + } + } + map.insert(paletteType, brushes); + } + return true; +} + +QT_END_NAMESPACE + diff --git a/src/plugins/platformthemes/gtk3/qgtk3json_p.h b/src/plugins/platformthemes/gtk3/qgtk3json_p.h new file mode 100644 index 0000000000..daf280612c --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3json_p.h @@ -0,0 +1,102 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QGTK3JSON_P_H +#define QGTK3JSON_P_H + +#include <QtCore/QCache> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonObject> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtGui/QGuiApplication> +#include <QtGui/QPalette> + +#include <qpa/qplatformtheme.h> +#include "qgtk3interface_p.h" +#include "qgtk3storage_p.h" + +#undef signals // Collides with GTK symbols +#include <gtk/gtk.h> +#include <gdk/gdk.h> +#include <glib.h> + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class QGtk3Json +{ + Q_GADGET +private: + QGtk3Json(){}; + +public: + // Convert enums to strings + static QLatin1String fromPalette(QPlatformTheme::Palette palette); + static QLatin1String fromGtkState(GtkStateFlags type); + static QLatin1String fromColor(const QColor &Color); + static QLatin1String fromColorRole(QPalette::ColorRole role); + static QLatin1String fromColorGroup(QPalette::ColorGroup group); + static QLatin1String fromGdkSource(QGtk3Interface::QGtkColorSource source); + static QLatin1String fromSourceType(QGtk3Storage::SourceType sourceType); + static QLatin1String fromWidgetType(QGtk3Interface::QGtkWidget widgetType); + static QLatin1String fromColorScheme(Qt::ColorScheme colorScheme); + + // Convert strings to enums + static QPlatformTheme::Palette toPalette(const QString &palette); + static GtkStateFlags toGtkState(const QString &type); + static QColor toColor(const QString &Color); + static QPalette::ColorRole toColorRole(const QString &role); + static QPalette::ColorGroup toColorGroup(const QString &group); + static QGtk3Interface::QGtkColorSource toGdkSource(const QString &source); + static QGtk3Storage::SourceType toSourceType(const QString &sourceType); + static QGtk3Interface::QGtkWidget toWidgetType(const QString &widgetType); + static Qt::ColorScheme toColorScheme(const QString &colorScheme); + + // Json keys + static constexpr QLatin1StringView cePalettes = "QtGtk3Palettes"_L1; + static constexpr QLatin1StringView cePalette = "PaletteType"_L1; + static constexpr QLatin1StringView ceGtkState = "GtkStateType"_L1; + static constexpr QLatin1StringView ceGtkWidget = "GtkWidgetType"_L1; + static constexpr QLatin1StringView ceColor = "Color"_L1; + static constexpr QLatin1StringView ceColorRole = "ColorRole"_L1; + static constexpr QLatin1StringView ceColorGroup = "ColorGroup"_L1; + static constexpr QLatin1StringView ceGdkSource = "GdkSource"_L1; + static constexpr QLatin1StringView ceSourceType = "SourceType"_L1; + static constexpr QLatin1StringView ceLighter = "Lighter"_L1; + static constexpr QLatin1StringView ceRed = "DeltaRed"_L1; + static constexpr QLatin1StringView ceGreen = "DeltaGreen"_L1; + static constexpr QLatin1StringView ceBlue = "DeltaBlue"_L1; + static constexpr QLatin1StringView ceWidth = "Width"_L1; + static constexpr QLatin1StringView ceHeight = "Height"_L1; + static constexpr QLatin1StringView ceBrush = "FixedBrush"_L1; + static constexpr QLatin1StringView ceData = "SourceData"_L1; + static constexpr QLatin1StringView ceBrushes = "Brushes"_L1; + static constexpr QLatin1StringView ceColorScheme = "ColorScheme"_L1; + + // Save to a file + static bool save(const QGtk3Storage::PaletteMap &map, const QString &fileName, + QJsonDocument::JsonFormat format = QJsonDocument::Indented); + + // Save to a Json document + static const QJsonDocument save(const QGtk3Storage::PaletteMap &map); + + // Load from a file + static bool load(QGtk3Storage::PaletteMap &map, const QString &fileName); + + // Load from a Json document + static bool load(QGtk3Storage::PaletteMap &map, const QJsonDocument &doc); +}; + +QT_END_NAMESPACE +#endif // QGTK3JSON_P_H diff --git a/src/plugins/platformthemes/gtk3/qgtk3menu.cpp b/src/plugins/platformthemes/gtk3/qgtk3menu.cpp index 5589ff8836..c4ea0e5e33 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3menu.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3menu.cpp @@ -123,13 +123,13 @@ static QString convertMnemonics(QString text, bool *found) { *found = false; - qsizetype i = text.length() - 1; + qsizetype i = text.size() - 1; while (i >= 0) { const QChar c = text.at(i); if (c == u'&') { if (i == 0 || text.at(i - 1) != u'&') { // convert Qt to GTK mnemonic - if (i < text.length() - 1 && !text.at(i + 1).isSpace()) { + if (i < text.size() - 1 && !text.at(i + 1).isSpace()) { text.replace(i, 1, u'_'); *found = true; } @@ -329,7 +329,7 @@ void QGtk3Menu::insertMenuItem(QPlatformMenuItem *item, QPlatformMenuItem *befor GtkWidget *handle = gitem->create(); int index = m_items.indexOf(static_cast<QGtk3MenuItem *>(before)); if (index < 0) - index = m_items.count(); + index = m_items.size(); m_items.insert(index, gitem); gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), handle, index); } diff --git a/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp b/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp new file mode 100644 index 0000000000..1ffdda74fa --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp @@ -0,0 +1,123 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qgtk3portalinterface_p.h" +#include "qgtk3storage_p.h" + +#include <QtDBus/QDBusArgument> +#include <QtDBus/QDBusConnection> +#include <QtDBus/QDBusMessage> +#include <QtDBus/QDBusPendingCall> +#include <QtDBus/QDBusPendingCallWatcher> +#include <QtDBus/QDBusPendingReply> +#include <QtDBus/QDBusVariant> +#include <QtDBus/QtDBus> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQGtk3PortalInterface, "qt.qpa.gtk"); + +using namespace Qt::StringLiterals; + +static constexpr QLatin1StringView appearanceInterface("org.freedesktop.appearance"); +static constexpr QLatin1StringView colorSchemeKey("color-scheme"); + +const QDBusArgument &operator>>(const QDBusArgument &argument, QMap<QString, QVariantMap> &map) +{ + argument.beginMap(); + map.clear(); + + while (!argument.atEnd()) { + QString key; + QVariantMap value; + argument.beginMapEntry(); + argument >> key >> value; + argument.endMapEntry(); + map.insert(key, value); + } + + argument.endMap(); + return argument; +} + +QGtk3PortalInterface::QGtk3PortalInterface(QGtk3Storage *s) + : m_storage(s) { + qRegisterMetaType<QDBusVariant>(); + qDBusRegisterMetaType<QMap<QString, QVariantMap>>(); + + queryColorScheme(); + + if (!s) { + qCDebug(lcQGtk3PortalInterface) << "QGtk3PortalInterface instantiated without QGtk3Storage." + << "No reaction to runtime theme changes."; + } +} + +Qt::ColorScheme QGtk3PortalInterface::colorScheme() const +{ + return m_colorScheme; +} + +void QGtk3PortalInterface::queryColorScheme() { + QDBusConnection connection = QDBusConnection::sessionBus(); + QDBusMessage message = QDBusMessage::createMethodCall( + "org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Settings"_L1, "ReadAll"_L1); + message << QStringList{ appearanceInterface }; + + QDBusPendingCall pendingCall = connection.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + QObject::connect( + watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply<QMap<QString, QVariantMap>> reply = *watcher; + if (reply.isValid()) { + QMap<QString, QVariantMap> settings = reply.value(); + if (!settings.isEmpty()) { + settingChanged(appearanceInterface, colorSchemeKey, + QDBusVariant(settings.value(appearanceInterface).value(colorSchemeKey))); + } + } else { + qCDebug(lcQGtk3PortalInterface) << "Failed to query org.freedesktop.portal.Settings: " + << reply.error().message(); + } + watcher->deleteLater(); + }); + + QDBusConnection::sessionBus().connect( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Settings"_L1, "SettingChanged"_L1, this, + SLOT(settingChanged(QString, QString, QDBusVariant))); +} + +void QGtk3PortalInterface::settingChanged(const QString &group, const QString &key, + const QDBusVariant &value) +{ + if (group == appearanceInterface && key == colorSchemeKey) { + const uint colorScheme = value.variant().toUInt(); + // From org.freedesktop.portal.Settings.xml + // "1" - Prefer dark appearance + Qt::ColorScheme newColorScheme = colorScheme == 1 ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; + if (m_colorScheme != newColorScheme) { + m_colorScheme = newColorScheme; + if (m_storage) + m_storage->handleThemeChange(); + } + } +} + +QT_END_NAMESPACE + +#include "moc_qgtk3portalinterface_p.cpp" diff --git a/src/plugins/platformthemes/gtk3/qgtk3portalinterface_p.h b/src/plugins/platformthemes/gtk3/qgtk3portalinterface_p.h new file mode 100644 index 0000000000..25a5f58ab1 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3portalinterface_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QGTK3PORTALINTERFACE_H +#define QGTK3PORTALINTERFACE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QObject> +#include <QtCore/QLoggingCategory> + +QT_BEGIN_NAMESPACE + +class QDBusVariant; +class QGtk3Storage; + +Q_DECLARE_LOGGING_CATEGORY(lcQGtk3PortalInterface); + +class QGtk3PortalInterface : public QObject +{ + Q_OBJECT +public: + QGtk3PortalInterface(QGtk3Storage *s); + ~QGtk3PortalInterface() = default; + + Qt::ColorScheme colorScheme() const; + +private Q_SLOTS: + void settingChanged(const QString &group, const QString &key, + const QDBusVariant &value); +private: + void queryColorScheme(); + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + QGtk3Storage *m_storage = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QGTK3PORTALINTERFACE_H diff --git a/src/plugins/platformthemes/gtk3/qgtk3storage.cpp b/src/plugins/platformthemes/gtk3/qgtk3storage.cpp new file mode 100644 index 0000000000..2877b28590 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3storage.cpp @@ -0,0 +1,686 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qgtk3json_p.h" +#include "qgtk3storage_p.h" +#include <qpa/qwindowsysteminterface.h> + +QT_BEGIN_NAMESPACE + +QGtk3Storage::QGtk3Storage() +{ + m_interface.reset(new QGtk3Interface(this)); +#if QT_CONFIG(dbus) + m_portalInterface.reset(new QGtk3PortalInterface(this)); +#endif + populateMap(); +} + +/*! + \internal + \enum QGtk3Storage::SourceType + \brief This enum represents the type of a color source. + + \value Gtk Color is read from a GTK widget + \value Fixed A fixed brush is specified + \value Modified The color is a modification of another color (fixed or read from GTK) + \omitvalue Invalid + */ + +/*! + \internal + \brief Find a brush from a source. + + Returns a QBrush from a given \param source and a \param map of available brushes + to search from. + + A null QBrush is returned, if no brush corresponding to the source has been found. + */ +QBrush QGtk3Storage::brush(const Source &source, const BrushMap &map) const +{ + switch (source.sourceType) { + case SourceType::Gtk: + return m_interface ? QBrush(m_interface->brush(source.gtk3.gtkWidgetType, + source.gtk3.source, source.gtk3.state)) + : QBrush(); + + case SourceType::Modified: { + // don't loop through modified sources, break if modified source not found + Source recSource = brush(TargetBrush(source.rec.colorGroup, source.rec.colorRole, + source.rec.colorScheme), map); + + if (!recSource.isValid() || (recSource.sourceType == SourceType::Modified)) + return QBrush(); + + // Set brush and alter color + QBrush b = brush(recSource, map); + if (source.rec.width > 0 && source.rec.height > 0) + b.setTexture(QPixmap(source.rec.width, source.rec.height)); + QColor c = b.color().lighter(source.rec.lighter); + c = QColor((c.red() + source.rec.deltaRed), + (c.green() + source.rec.deltaGreen), + (c.blue() + source.rec.deltaBlue)); + b.setColor(c); + return b; + } + + case SourceType::Fixed: + return source.fix.fixedBrush; + + case SourceType::Invalid: + return QBrush(); + } + + // needed because of the scope after recursive + Q_UNREACHABLE(); +} + +/*! + \internal + \brief Recurse to find a source brush for modification. + + Returns the source specified by the target brush \param b in the \param map of brushes. + Takes dark/light/unknown into consideration. + Returns an empty brush if no suitable one can be found. + */ +QGtk3Storage::Source QGtk3Storage::brush(const TargetBrush &b, const BrushMap &map) const +{ +#define FIND(brush) if (map.contains(brush))\ + return map.value(brush) + + // Return exact match + FIND(b); + + // unknown color scheme can find anything + if (b.colorScheme == Qt::ColorScheme::Unknown) { + FIND(TargetBrush(b, Qt::ColorScheme::Dark)); + FIND(TargetBrush(b, Qt::ColorScheme::Light)); + } + + // Color group All can always be found + if (b.colorGroup != QPalette::All) + return brush(TargetBrush(QPalette::All, b.colorRole, b.colorScheme), map); + + // Brush not found + return Source(); +#undef FIND +} + +/*! + \internal + \brief Returns a simple, hard coded base palette. + + Create a hard coded palette with default colors as a fallback for any color that can't be + obtained from GTK. + + \note This palette will be used as a default baseline for the system palette, which then + will be used as a default baseline for any other palette type. + */ +QPalette QGtk3Storage::standardPalette() +{ + QColor backgroundColor(0xd4, 0xd0, 0xc8); + QColor lightColor(backgroundColor.lighter()); + QColor darkColor(backgroundColor.darker()); + const QBrush darkBrush(darkColor); + QColor midColor(Qt::gray); + QPalette palette(Qt::black, backgroundColor, lightColor, darkColor, + midColor, Qt::black, Qt::white); + palette.setBrush(QPalette::Disabled, QPalette::WindowText, darkBrush); + palette.setBrush(QPalette::Disabled, QPalette::Text, darkBrush); + palette.setBrush(QPalette::Disabled, QPalette::ButtonText, darkBrush); + palette.setBrush(QPalette::Disabled, QPalette::Base, QBrush(backgroundColor)); + return palette; +} + +/*! + \internal + \brief Return a GTK styled QPalette. + + Returns the pointer to a (cached) QPalette for \param type, with its brushes + populated according to the current GTK theme. + */ +const QPalette *QGtk3Storage::palette(QPlatformTheme::Palette type) const +{ + if (type >= QPlatformTheme::NPalettes) + return nullptr; + + if (m_paletteCache[type].has_value()) { + qCDebug(lcQGtk3Interface) << "Returning palette from cache:" + << QGtk3Json::fromPalette(type); + + return &m_paletteCache[type].value(); + } + + // Read system palette as a baseline first + if (!m_paletteCache[QPlatformTheme::SystemPalette].has_value() && type != QPlatformTheme::SystemPalette) + palette(); + + // Fall back to system palette for unknown types + if (!m_palettes.contains(type) && type != QPlatformTheme::SystemPalette) { + qCDebug(lcQGtk3Interface) << "Returning system palette for unknown type" + << QGtk3Json::fromPalette(type); + return palette(); + } + + BrushMap brushes = m_palettes.value(type); + + // Standard palette is base for system palette. System palette is base for all others. + QPalette p = QPalette( type == QPlatformTheme::SystemPalette ? standardPalette() + : m_paletteCache[QPlatformTheme::SystemPalette].value()); + + qCDebug(lcQGtk3Interface) << "Creating palette:" << QGtk3Json::fromPalette(type); + for (auto i = brushes.begin(); i != brushes.end(); ++i) { + Source source = i.value(); + + // Brush is set if + // - theme and source color scheme match + // - or either of them is unknown + const auto appSource = i.key().colorScheme; + const auto appTheme = colorScheme(); + const bool setBrush = (appSource == appTheme) || + (appSource == Qt::ColorScheme::Unknown) || + (appTheme == Qt::ColorScheme::Unknown); + + if (setBrush) { + p.setBrush(i.key().colorGroup, i.key().colorRole, brush(source, brushes)); + } + } + + m_paletteCache[type].emplace(p); + if (type == QPlatformTheme::SystemPalette) + qCDebug(lcQGtk3Interface) << "System Palette defined" << themeName() << colorScheme() << p; + + return &m_paletteCache[type].value(); +} + +/*! + \internal + \brief Return a GTK styled font. + + Returns a QFont of \param type, styled according to the current GTK theme. +*/ +const QFont *QGtk3Storage::font(QPlatformTheme::Font type) const +{ + if (m_fontCache[type].has_value()) + return &m_fontCache[type].value(); + + m_fontCache[type].emplace(m_interface->font(type)); + return &m_fontCache[type].value(); +} + +/*! + \internal + \brief Return a GTK styled standard pixmap if available. + + Returns a pixmap specified by \param standardPixmap and \param size. + Returns an empty pixmap if GTK doesn't support the requested one. + */ +QPixmap QGtk3Storage::standardPixmap(QPlatformTheme::StandardPixmap standardPixmap, + const QSizeF &size) const +{ + if (m_pixmapCache.contains(standardPixmap)) + return QPixmap::fromImage(m_pixmapCache.object(standardPixmap)->scaled(size.toSize())); + + if (!m_interface) + return QPixmap(); + + QImage image = m_interface->standardPixmap(standardPixmap); + if (image.isNull()) + return QPixmap(); + + m_pixmapCache.insert(standardPixmap, new QImage(image)); + return QPixmap::fromImage(image.scaled(size.toSize())); +} + +/*! + \internal + \brief Returns a GTK styled file icon corresponding to \param fileInfo. + */ +QIcon QGtk3Storage::fileIcon(const QFileInfo &fileInfo) const +{ + return m_interface ? m_interface->fileIcon(fileInfo) : QIcon(); +} + +/*! + \internal + \brief Clears all caches. + */ +void QGtk3Storage::clear() +{ + m_colorScheme = Qt::ColorScheme::Unknown; + m_palettes.clear(); + for (auto &cache : m_paletteCache) + cache.reset(); + + for (auto &cache : m_fontCache) + cache.reset(); +} + +/*! + \internal + \brief Handles a theme change at runtime. + + Clear all caches, re-populate with current GTK theme and notify the window system interface. + This method is a callback for the theme change signal sent from GTK. + */ +void QGtk3Storage::handleThemeChange() +{ + populateMap(); + QWindowSystemInterface::handleThemeChange(); +} + +/*! + \internal + \brief Populates a map with information about how to locate colors in GTK. + + This method creates a data structure to locate color information for each brush of a QPalette + within GTK. The structure can hold mapping information for each QPlatformTheme::Palette + enum value. If no specific mapping is stored for an enum value, the system palette is returned + instead of a specific one. If no mapping is stored for the system palette, it will fall back to + QGtk3Storage::standardPalette. + + The method will populate the data structure with a standard mapping, covering the following + palette types: + \list + \li QPlatformTheme::SystemPalette + \li QPlatformTheme::CheckBoxPalette + \li QPlatformTheme::RadioButtonPalette + \li QPlatformTheme::ComboBoxPalette + \li QPlatformTheme::GroupBoxPalette + \li QPlatformTheme::MenuPalette + \li QPlatformTheme::TextLineEditPalette + \endlist + + The method will check the environment variable {{QT_GUI_GTK_JSON_SAVE}}. If it points to a + valid path with write access, it will write the standard mapping into a Json file. + That Json file can be modified and/or extended. + The Json syntax is + - "QGtk3Palettes" (top level value) + - QPlatformTheme::Palette + - QPalette::ColorRole + - Qt::ColorScheme + - Qt::ColorGroup + - Source data + - Source Type + - [source data] + + If the environment variable {{QT_GUI_GTK_JSON_HARDCODED}} contains the keyword \c true, + all sources are converted to fixed sources. In that case, they contain the hard coded HexRGBA + values read from GTK. + + The method will also check the environment variable {{QT_GUI_GTK_JSON}}. If it points to a valid + Json file with read access, it will be parsed instead of creating a standard mapping. + Parsing errors will be printed out with qCInfo if the logging category {{qt.qpa.gtk}} is activated. + In case of a parsing error, the method will fall back to creating a standard mapping. + + \note + If a Json file contains only fixed brushes (e.g. exported with {{QT_GUI_GTK_JSON_HARDCODED=true}}), + no colors will be imported from GTK. + */ +void QGtk3Storage::populateMap() +{ + static QString m_themeName; + + // Distiguish initialization, theme change or call without theme change + Qt::ColorScheme newColorScheme = Qt::ColorScheme::Unknown; + const QString newThemeName = themeName(); + +#if QT_CONFIG(dbus) + // Prefer color scheme we get from xdg-desktop-portal as this is what GNOME + // relies on these days + newColorScheme = m_portalInterface->colorScheme(); +#endif + + if (newColorScheme == Qt::ColorScheme::Unknown) { + // Derive color scheme from theme name + newColorScheme = newThemeName.contains("dark"_L1, Qt::CaseInsensitive) + ? Qt::ColorScheme::Dark : m_interface->colorSchemeByColors(); + } + + if (m_themeName == newThemeName && m_colorScheme == newColorScheme) + return; + + clear(); + + if (m_themeName.isEmpty()) { + qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << newColorScheme; + } else { + qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << newColorScheme; + } + m_colorScheme = newColorScheme; + m_themeName = newThemeName; + + // create standard mapping or load from Json file? + const QString jsonInput = qEnvironmentVariable("QT_GUI_GTK_JSON"); + if (!jsonInput.isEmpty()) { + if (load(jsonInput)) { + return; + } else { + qWarning() << "Falling back to standard GTK mapping."; + } + } + + createMapping(); + + const QString jsonOutput = qEnvironmentVariable("QT_GUI_GTK_JSON_SAVE"); + if (!jsonOutput.isEmpty() && !save(jsonOutput)) + qWarning() << "File" << jsonOutput << "could not be saved.\n"; +} + +/*! + \internal + \brief Return a palette map for saving. + + This method returns the existing palette map, if the environment variable + {{QT_GUI_GTK_JSON_HARDCODED}} is not set or does not contain the keyword \c true. + If it contains the keyword \c true, it returns a palette map with all brush + sources converted to fixed sources. + */ +const QGtk3Storage::PaletteMap QGtk3Storage::savePalettes() const +{ + const QString hard = qEnvironmentVariable("QT_GUI_GTK_JSON_HARDCODED"); + if (!hard.contains("true"_L1, Qt::CaseInsensitive)) + return m_palettes; + + // Json output is supposed to be readable without GTK connection + // convert palette map into hard coded brushes + PaletteMap map = m_palettes; + for (auto paletteIterator = map.begin(); paletteIterator != map.end(); + ++paletteIterator) { + QGtk3Storage::BrushMap &bm = paletteIterator.value(); + for (auto brushIterator = bm.begin(); brushIterator != bm.end(); + ++brushIterator) { + QGtk3Storage::Source &s = brushIterator.value(); + switch (s.sourceType) { + + // Read the brush and convert it into a fixed brush + case SourceType::Gtk: { + const QBrush fixedBrush = brush(s, bm); + s.fix.fixedBrush = fixedBrush; + s.sourceType = SourceType::Fixed; + } + break; + case SourceType::Fixed: + case SourceType::Modified: + case SourceType::Invalid: + break; + } + } + } + return map; +} + +/*! + \internal + \brief Saves current palette mapping to a \param filename with Json format \param f. + + Saves the current palette mapping into a QJson file, + taking {{QT_GUI_GTK_JSON_HARDCODED}} into consideration. + Returns \c true if saving was successful and \c false otherwise. + */ +bool QGtk3Storage::save(const QString &filename, QJsonDocument::JsonFormat f) const +{ + return QGtk3Json::save(savePalettes(), filename, f); +} + +/*! + \internal + \brief Returns a QJsonDocument with current palette mapping. + + Saves the current palette mapping into a QJsonDocument, + taking {{QT_GUI_GTK_JSON_HARDCODED}} into consideration. + Returns \c true if saving was successful and \c false otherwise. + */ +QJsonDocument QGtk3Storage::save() const +{ + return QGtk3Json::save(savePalettes()); +} + +/*! + \internal + \brief Loads palette mapping from Json file \param filename. + + Returns \c true if the file was successfully parsed and \c false otherwise. + */ +bool QGtk3Storage::load(const QString &filename) +{ + return QGtk3Json::load(m_palettes, filename); +} + +/*! + \internal + \brief Creates a standard palette mapping. + + The method creates a hard coded standard mapping, used if no external Json file + containing a valid mapping has been specified in the environment variable {{QT_GUI_GTK_JSON}}. + */ +void QGtk3Storage::createMapping() +{ + // Hard code standard mapping + BrushMap map; + Source source; + + // Define a GTK source +#define GTK(wtype, colorSource, state)\ + source = Source(QGtk3Interface::QGtkWidget::gtk_ ##wtype,\ + QGtk3Interface::QGtkColorSource::colorSource, GTK_STATE_FLAG_ ##state) + + // Define a modified source +#define LIGHTER(group, role, lighter)\ + source = Source(QPalette::group, QPalette::role,\ + Qt::ColorScheme::Unknown, lighter) +#define MODIFY(group, role, red, green, blue)\ + source = Source(QPalette::group, QPalette::role,\ + Qt::ColorScheme::Unknown, red, green, blue) + + // Define fixed source +#define FIX(color) source = FixedSource(color); + + // Add the source to a target brush + // Use default Qt::ColorScheme::Unknown, if no color scheme was specified +#define ADD_2(group, role) map.insert(TargetBrush(QPalette::group, QPalette::role), source); +#define ADD_3(group, role, app) map.insert(TargetBrush(QPalette::group, QPalette::role,\ + Qt::ColorScheme::app), source); +#define ADD_X(x, group, role, app, FUNC, ...) FUNC +#define ADD(...) ADD_X(,##__VA_ARGS__, ADD_3(__VA_ARGS__), ADD_2(__VA_ARGS__)) + // Save target brushes to a palette type +#define SAVE(palette) m_palettes.insert(QPlatformTheme::palette, map) + // Clear brushes to start next palette +#define CLEAR map.clear() + + /* + Macro usage: + + 1. Define a source + GTK(QGtkWidget, QGtkColorSource, GTK_STATE_FLAG) + Fetch the color from a GtkWidget, related to a source and a state. + + LIGHTER(ColorGroup, ColorROle, lighter) + Use a color of the same QPalette related to ColorGroup and ColorRole. + Make the color lighter (if lighter >100) or darker (if lighter < 100) + + MODIFY(ColorGroup, ColorRole, red, green, blue) + Use a color of the same QPalette related to ColorGroup and ColorRole. + Modify it by adding red, green, blue. + + FIX(const QBrush &) + Use a fixed brush without querying GTK + + 2. Define the target + Use ADD(ColorGroup, ColorRole) to use the defined source for the + color group / role in the current palette. + + Use ADD(ColorGroup, ColorRole, ColorScheme) to use the defined source + only for a specific color scheme + + 3. Save mapping + Save the defined mappings for a specific palette. + If a mapping entry does not cover all color groups and roles of a palette, + the system palette will be used for the remaining values. + If the system palette does not have all combination of color groups and roles, + the remaining ones will be populated by a hard coded fusion-style like palette. + + 4. Clear mapping + Use CLEAR to clear the mapping and begin a new one. + */ + + + // System palette + { + // background color and calculate derivates + GTK(Default, Background, INSENSITIVE); + ADD(All, Window); + ADD(All, Button); + ADD(All, Base); + LIGHTER(Normal, Window, 125); + ADD(Normal, Light); + ADD(Inactive, Light); + LIGHTER(Normal, Window, 70); + ADD(Normal, Shadow); + LIGHTER(Normal, Window, 80); + ADD(Normal, Dark); + ADD(Inactive, Dark) + + GTK(button, Foreground, ACTIVE); + ADD(Inactive, WindowText); + LIGHTER(Normal, WindowText, 50); + ADD(Disabled, Text); + ADD(Disabled, WindowText); + ADD(Disabled, ButtonText); + + GTK(button, Text, NORMAL); + ADD(Inactive, ButtonText); + + // special background colors + GTK(Default, Background, SELECTED); + ADD(Disabled, Highlight); + ADD(Normal, Highlight); + ADD(Inactive, Highlight); + + GTK(entry, Foreground, SELECTED); + ADD(Normal, HighlightedText); + ADD(Inactive, HighlightedText); + + // text color and friends + GTK(entry, Text, NORMAL); + ADD(Normal, ButtonText); + ADD(Normal, WindowText); + ADD(Disabled, HighlightedText); + + GTK(Default, Text, NORMAL); + ADD(Normal, Text); + ADD(Inactive, Text); + ADD(Normal, HighlightedText); + LIGHTER(Normal, Base, 93); + ADD(All, AlternateBase); + + GTK(Default, Foreground, NORMAL); + MODIFY(Normal, Text, 100, 100, 100); + ADD(All, PlaceholderText, Light); + MODIFY(Normal, Text, -100, -100, -100); + ADD(All, PlaceholderText, Dark); + + // Light, midlight, dark, mid, shadow colors + LIGHTER(Normal, Button, 125); + ADD(All, Light) + LIGHTER(Normal, Button, 113); + ADD(All, Midlight) + LIGHTER(Normal, Button, 113); + ADD(All, Mid) + LIGHTER(Normal, Button, 87); + ADD(All, Dark) + LIGHTER(Normal, Button, 5); + ADD(All, Shadow) + + SAVE(SystemPalette); + CLEAR; + } + + // Label and TabBar Palette + { + GTK(entry, Text, NORMAL); + ADD(Normal, WindowText); + ADD(Inactive, WindowText); + + SAVE(LabelPalette); + SAVE(TabBarPalette); + CLEAR; + } + + // Checkbox and RadioButton Palette + { + GTK(button, Text, ACTIVE); + ADD(Normal, Base, Dark); + ADD(Inactive, WindowText, Dark); + + GTK(Default, Foreground, NORMAL); + ADD(All, Text); + + GTK(Default, Background, NORMAL); + ADD(All, Base); + + GTK(button, Text, NORMAL); + ADD(Normal, Base, Light); + ADD(Inactive, WindowText, Light); + + SAVE(CheckBoxPalette); + SAVE(RadioButtonPalette); + CLEAR; + } + + // ComboBox, GroupBox & Frame Palette + { + GTK(combo_box, Text, NORMAL); + ADD(Normal, ButtonText, Dark); + ADD(Normal, Text, Dark); + ADD(Inactive, WindowText, Dark); + + GTK(combo_box, Text, ACTIVE); + ADD(Normal, ButtonText, Light); + ADD(Normal, Text, Light); + ADD(Inactive, WindowText, Light); + + SAVE(ComboBoxPalette); + SAVE(GroupBoxPalette); + CLEAR; + } + + // MenuBar Palette + { + GTK(Default, Text, ACTIVE); + ADD(Normal, ButtonText); + SAVE(MenuPalette); + CLEAR; + } + + // LineEdit Palette + { + GTK(Default, Background, NORMAL); + ADD(All, Base); + SAVE(TextLineEditPalette); + CLEAR; + } + +#undef GTK +#undef REC +#undef FIX +#undef ADD +#undef ADD_2 +#undef ADD_3 +#undef ADD_X +#undef SAVE +#undef LOAD +} + +QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3storage_p.h b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h new file mode 100644 index 0000000000..45192263a9 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h @@ -0,0 +1,240 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QGTK3STORAGE_P_H +#define QGTK3STORAGE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qgtk3interface_p.h" +#if QT_CONFIG(dbus) +#include "qgtk3portalinterface_p.h" +#endif + +#include <QtCore/QJsonDocument> +#include <QtCore/QCache> +#include <QtCore/QString> +#include <QtGui/QGuiApplication> +#include <QtGui/QPalette> + +#include <qpa/qplatformtheme.h> +#include <private/qflatmap_p.h> + +QT_BEGIN_NAMESPACE +class QGtk3Storage +{ + Q_GADGET +public: + QGtk3Storage(); + + // Enum documented in cpp file. Please keep it in line with updates made here. + enum class SourceType { + Gtk, + Fixed, + Modified, + Invalid + }; + Q_ENUM(SourceType) + + // Standard GTK source: Populate a brush from GTK + struct Gtk3Source { + QGtk3Interface::QGtkWidget gtkWidgetType; + QGtk3Interface::QGtkColorSource source; + GtkStateFlags state; + int width = -1; + int height = -1; + QDebug operator<<(QDebug dbg) + { + return dbg << "QGtkStorage::Gtk3Source(gtkwidgetType=" << gtkWidgetType << ", source=" + << source << ", state=" << state << ", width=" << width << ", height=" + << height << ")"; + } + }; + + // Recursive source: Populate a brush by altering another source + struct RecursiveSource { + QPalette::ColorGroup colorGroup; + QPalette::ColorRole colorRole; + Qt::ColorScheme colorScheme; + int lighter = 100; + int deltaRed = 0; + int deltaGreen = 0; + int deltaBlue = 0; + int width = -1; + int height = -1; + QDebug operator<<(QDebug dbg) + { + return dbg << "QGtkStorage::RecursiceSource(colorGroup=" << colorGroup << ", colorRole=" + << colorRole << ", colorScheme=" << colorScheme << ", lighter=" << lighter + << ", deltaRed="<< deltaRed << "deltaBlue =" << deltaBlue << "deltaGreen=" + << deltaGreen << ", width=" << width << ", height=" << height << ")"; + } + }; + + // Fixed source: Populate a brush with fixed values rather than reading GTK + struct FixedSource { + QBrush fixedBrush; + QDebug operator<<(QDebug dbg) + { + return dbg << "QGtkStorage::FixedSource(" << fixedBrush << ")"; + } + }; + + // Data source for brushes + struct Source { + SourceType sourceType = SourceType::Invalid; + Gtk3Source gtk3; + RecursiveSource rec; + FixedSource fix; + + // GTK constructor + Source(QGtk3Interface::QGtkWidget wtype, QGtk3Interface::QGtkColorSource csource, + GtkStateFlags cstate, int bwidth = -1, int bheight = -1) : sourceType(SourceType::Gtk) + { + gtk3.gtkWidgetType = wtype; + gtk3.source = csource; + gtk3.state = cstate; + gtk3.width = bwidth; + gtk3.height = bheight; + } + + // Recursive constructor for darker/lighter colors + Source(QPalette::ColorGroup group, QPalette::ColorRole role, + Qt::ColorScheme scheme, int p_lighter = 100) + : sourceType(SourceType::Modified) + { + rec.colorGroup = group; + rec.colorRole = role; + rec.colorScheme = scheme; + rec.lighter = p_lighter; + } + + // Recursive ocnstructor for color modification + Source(QPalette::ColorGroup group, QPalette::ColorRole role, + Qt::ColorScheme scheme, int p_red, int p_green, int p_blue) + : sourceType(SourceType::Modified) + { + rec.colorGroup = group; + rec.colorRole = role; + rec.colorScheme = scheme; + rec.deltaRed = p_red; + rec.deltaGreen = p_green; + rec.deltaBlue = p_blue; + } + + // Recursive constructor for all: color modification and darker/lighter + Source(QPalette::ColorGroup group, QPalette::ColorRole role, + Qt::ColorScheme scheme, int p_lighter, + int p_red, int p_green, int p_blue) : sourceType(SourceType::Modified) + { + rec.colorGroup = group; + rec.colorRole = role; + rec.colorScheme = scheme; + rec.lighter = p_lighter; + rec.deltaRed = p_red; + rec.deltaGreen = p_green; + rec.deltaBlue = p_blue; + } + + // Fixed Source constructor + Source(const QBrush &brush) : sourceType(SourceType::Fixed) + { + fix.fixedBrush = brush; + }; + + // Invalid constructor and getter + Source() : sourceType(SourceType::Invalid) {}; + bool isValid() const { return sourceType != SourceType::Invalid; } + + // Debug + QDebug operator<<(QDebug dbg) + { + return dbg << "QGtk3Storage::Source(sourceType=" << sourceType << ")"; + } + }; + + // Struct with key attributes to identify a brush: color group, color role and color scheme + struct TargetBrush { + QPalette::ColorGroup colorGroup; + QPalette::ColorRole colorRole; + Qt::ColorScheme colorScheme; + + // Generic constructor + TargetBrush(QPalette::ColorGroup group, QPalette::ColorRole role, + Qt::ColorScheme scheme = Qt::ColorScheme::Unknown) : + colorGroup(group), colorRole(role), colorScheme(scheme) {}; + + // Copy constructor with color scheme modifier for dark/light aware search + TargetBrush(const TargetBrush &other, Qt::ColorScheme scheme) : + colorGroup(other.colorGroup), colorRole(other.colorRole), colorScheme(scheme) {}; + + // struct becomes key of a map, so operator< is needed + bool operator<(const TargetBrush& other) const { + return std::tie(colorGroup, colorRole, colorScheme) < + std::tie(other.colorGroup, other.colorRole, other.colorScheme); + } + }; + + // Mapping a palette's brushes to their GTK sources + typedef QFlatMap<TargetBrush, Source> BrushMap; + + // Storage of palettes and their GTK sources + typedef QFlatMap<QPlatformTheme::Palette, BrushMap> PaletteMap; + + // Public getters + const QPalette *palette(QPlatformTheme::Palette = QPlatformTheme::SystemPalette) const; + QPixmap standardPixmap(QPlatformTheme::StandardPixmap standardPixmap, const QSizeF &size) const; + Qt::ColorScheme colorScheme() const { return m_colorScheme; }; + static QPalette standardPalette(); + const QString themeName() const { return m_interface ? m_interface->themeName() : QString(); }; + const QFont *font(QPlatformTheme::Font type) const; + QIcon fileIcon(const QFileInfo &fileInfo) const; + + // Initialization + void populateMap(); + void handleThemeChange(); + +private: + // Storage for palettes and their brushes + PaletteMap m_palettes; + + std::unique_ptr<QGtk3Interface> m_interface; +#if QT_CONFIG(dbus) + std::unique_ptr<QGtk3PortalInterface> m_portalInterface; +#endif + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + + // Caches for Pixmaps, fonts and palettes + mutable QCache<QPlatformTheme::StandardPixmap, QImage> m_pixmapCache; + mutable std::array<std::optional<QPalette>, QPlatformTheme::Palette::NPalettes> m_paletteCache; + mutable std::array<std::optional<QFont>, QPlatformTheme::NFonts> m_fontCache; + + // Search brush with a given GTK3 source + QBrush brush(const Source &source, const BrushMap &map) const; + + // Get GTK3 source for a target brush + Source brush (const TargetBrush &brush, const BrushMap &map) const; + + // clear cache, palettes and color scheme + void clear(); + + // Data creation, import & export + void createMapping (); + const PaletteMap savePalettes() const; + bool save(const QString &filename, const QJsonDocument::JsonFormat f = QJsonDocument::Indented) const; + QJsonDocument save() const; + bool load(const QString &filename); +}; + +QT_END_NAMESPACE +#endif // QGTK3STORAGE_H diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp index 22a079732a..9d23ba7e48 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp @@ -5,15 +5,20 @@ #include "qgtk3dialoghelpers.h" #include "qgtk3menu.h" #include <QVariant> -#include <QtCore/qregularexpression.h> +#include <QGuiApplication> +#include <qpa/qwindowsysteminterface.h> #undef signals #include <gtk/gtk.h> +#if QT_CONFIG(xcb_xlib) #include <X11/Xlib.h> +#endif QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + const char *QGtk3Theme::name = "gtk3"; template <typename T> @@ -49,13 +54,25 @@ void gtkMessageHandler(const gchar *log_domain, QGtk3Theme::QGtk3Theme() { + // Ensure gtk uses the same windowing system, but let it + // fallback in case GDK_BACKEND environment variable + // filters the preferred one out + if (QGuiApplication::platformName().startsWith("wayland"_L1)) + gdk_set_allowed_backends("wayland,x11"); + else if (QGuiApplication::platformName() == "xcb"_L1) + gdk_set_allowed_backends("x11,wayland"); + +#if QT_CONFIG(xcb_xlib) // gtk_init will reset the Xlib error handler, and that causes // Qt applications to quit on X errors. Therefore, we need to manually restore it. int (*oldErrorHandler)(Display *, XErrorEvent *) = XSetErrorHandler(nullptr); +#endif gtk_init(nullptr, nullptr); +#if QT_CONFIG(xcb_xlib) XSetErrorHandler(oldErrorHandler); +#endif /* Initialize some types here so that Gtk+ does not crash when reading * the treemodel for GtkFontChooser. @@ -65,6 +82,29 @@ QGtk3Theme::QGtk3Theme() /* Use our custom log handler. */ g_log_set_handler("Gtk", G_LOG_LEVEL_MESSAGE, gtkMessageHandler, nullptr); + +#define SETTING_CONNECT(setting) g_signal_connect(settings, "notify::" setting, G_CALLBACK(notifyThemeChanged), nullptr) + auto notifyThemeChanged = [] { + QWindowSystemInterface::handleThemeChange(); + }; + + GtkSettings *settings = gtk_settings_get_default(); + SETTING_CONNECT("gtk-cursor-blink-time"); + SETTING_CONNECT("gtk-double-click-distance"); + SETTING_CONNECT("gtk-double-click-time"); + SETTING_CONNECT("gtk-long-press-time"); + SETTING_CONNECT("gtk-entry-password-hint-timeout"); + SETTING_CONNECT("gtk-dnd-drag-threshold"); + SETTING_CONNECT("gtk-icon-theme-name"); + SETTING_CONNECT("gtk-fallback-icon-theme"); + SETTING_CONNECT("gtk-font-name"); + SETTING_CONNECT("gtk-application-prefer-dark-theme"); + SETTING_CONNECT("gtk-theme-name"); + SETTING_CONNECT("gtk-cursor-theme-name"); + SETTING_CONNECT("gtk-cursor-theme-size"); +#undef SETTING_CONNECT + + m_storage.reset(new QGtk3Storage); } static inline QVariant gtkGetLongPressTime() @@ -99,8 +139,14 @@ QVariant QGtk3Theme::themeHint(QPlatformTheme::ThemeHint hint) const return QVariant(gtkSetting("gtk-icon-theme-name")); case QPlatformTheme::SystemIconFallbackThemeName: return QVariant(gtkSetting("gtk-fallback-icon-theme")); - case QPlatformTheme::PreselectFirstFileInDirectory: - return true; + case QPlatformTheme::MouseCursorTheme: + return QVariant(gtkSetting("gtk-cursor-theme-name")); + case QPlatformTheme::MouseCursorSize: { + int s = gtkSetting<gint>("gtk-cursor-theme-size"); + if (s > 0) + return QVariant(QSize(s, s)); + return QGnomeTheme::themeHint(hint); + } default: return QGnomeTheme::themeHint(hint); } @@ -114,45 +160,10 @@ QString QGtk3Theme::gtkFontName() const return QGnomeTheme::gtkFontName(); } -QPlatformTheme::Appearance QGtk3Theme::appearance() const +Qt::ColorScheme QGtk3Theme::colorScheme() const { - /* - https://docs.gtk.org/gtk3/running.html - - It's possible to set a theme variant after the theme name when using GTK_THEME: - - GTK_THEME=Adwaita:dark - - Some themes also have "-dark" as part of their name. - - We test this environment variable first because the documentation says - it's mainly used for easy debugging, so it should be possible to use it - to override any other settings. - */ - QString themeName = qEnvironmentVariable("GTK_THEME"); - const QRegularExpression darkRegex(QStringLiteral("[:-]dark"), QRegularExpression::CaseInsensitiveOption); - if (!themeName.isEmpty()) - return darkRegex.match(themeName).hasMatch() ? Appearance::Dark : Appearance::Light; - - /* - https://docs.gtk.org/gtk3/property.Settings.gtk-application-prefer-dark-theme.html - - This setting controls which theme is used when the theme specified by - gtk-theme-name provides both light and dark variants. We can save a - regex check by testing this property first. - */ - const auto preferDark = gtkSetting<bool>("gtk-application-prefer-dark-theme"); - if (preferDark) - return Appearance::Dark; - - /* - https://docs.gtk.org/gtk3/property.Settings.gtk-theme-name.html - */ - themeName = gtkSetting("gtk-theme-name"); - if (!themeName.isEmpty()) - return darkRegex.match(themeName).hasMatch() ? Appearance::Dark : Appearance::Light; - - return Appearance::Unknown; + Q_ASSERT(m_storage); + return m_storage->colorScheme(); } bool QGtk3Theme::usePlatformNativeDialog(DialogType type) const @@ -208,4 +219,30 @@ bool QGtk3Theme::useNativeFileDialog() return gtk_check_version(3, 15, 5) == nullptr; } +const QPalette *QGtk3Theme::palette(Palette type) const +{ + Q_ASSERT(m_storage); + return m_storage->palette(type); +} + +QPixmap QGtk3Theme::standardPixmap(StandardPixmap sp, const QSizeF &size) const +{ + Q_ASSERT(m_storage); + return m_storage->standardPixmap(sp, size); +} + +const QFont *QGtk3Theme::font(Font type) const +{ + Q_ASSERT(m_storage); + return m_storage->font(type); +} + +QIcon QGtk3Theme::fileIcon(const QFileInfo &fileInfo, + QPlatformTheme::IconOptions iconOptions) const +{ + Q_UNUSED(iconOptions); + Q_ASSERT(m_storage); + return m_storage->fileIcon(fileInfo); +} + QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.h b/src/plugins/platformthemes/gtk3/qgtk3theme.h index 0f274234d5..2828cc56e6 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.h +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.h @@ -4,7 +4,9 @@ #ifndef QGTK3THEME_H #define QGTK3THEME_H +#include <private/qtguiglobal_p.h> #include <private/qgenericunixthemes_p.h> +#include "qgtk3storage_p.h" QT_BEGIN_NAMESPACE @@ -16,7 +18,7 @@ public: virtual QVariant themeHint(ThemeHint hint) const override; virtual QString gtkFontName() const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; @@ -24,9 +26,16 @@ public: QPlatformMenu* createPlatformMenu() const override; QPlatformMenuItem* createPlatformMenuItem() const override; + const QPalette *palette(Palette type = SystemPalette) const override; + const QFont *font(Font type = SystemFont) const override; + QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; + QIcon fileIcon(const QFileInfo &fileInfo, + QPlatformTheme::IconOptions iconOptions = { }) const override; + static const char *name; private: static bool useNativeFileDialog(); + std::unique_ptr<QGtk3Storage> m_storage; }; QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt b/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt index 82fb94e31d..6228e83ec7 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt +++ b/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from xdgdesktopportal.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QXdgDesktopPortalThemePlugin Plugin: @@ -19,6 +20,3 @@ qt_internal_add_plugin(QXdgDesktopPortalThemePlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:xdgdesktopportal.pro:<TRUE>: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp index ecc5545e76..1c162be8fc 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp @@ -3,6 +3,10 @@ #include "qxdgdesktopportalfiledialog_p.h" +#include <private/qgenericunixservices_p.h> +#include <private/qguiapplication_p.h> +#include <qpa/qplatformintegration.h> + #include <QDBusConnection> #include <QDBusMessage> #include <QDBusPendingCall> @@ -157,8 +161,6 @@ void QXdgDesktopPortalFileDialog::openPortal(Qt::WindowFlags windowFlags, Qt::Wi "/org/freedesktop/portal/desktop"_L1, "org.freedesktop.portal.FileChooser"_L1, d->saveFile ? "SaveFile"_L1 : "OpenFile"_L1); - QString parentWindowId = "x11:"_L1 + QString::number(parent ? parent->winId() : 0, 16); - QVariantMap options; if (!d->acceptLabel.isEmpty()) options.insert("accept_label"_L1, d->acceptLabel); @@ -167,16 +169,18 @@ void QXdgDesktopPortalFileDialog::openPortal(Qt::WindowFlags windowFlags, Qt::Wi options.insert("multiple"_L1, d->multipleFiles); options.insert("directory"_L1, d->directoryMode); - if (d->saveFile) { - if (!d->directory.isEmpty()) - options.insert("current_folder"_L1, QFile::encodeName(d->directory).append('\0')); - - if (!d->selectedFiles.isEmpty()) { - // current_file for the file to be pre-selected, current_name for the file name to be pre-filled - // current_file accepts absolute path while current_name accepts just file name - options.insert("current_file"_L1, QFile::encodeName(d->selectedFiles.first()).append('\0')); - options.insert("current_name"_L1, QFileInfo(d->selectedFiles.first()).fileName()); - } + if (!d->directory.isEmpty()) + options.insert("current_folder"_L1, QFile::encodeName(d->directory).append('\0')); + + if (d->saveFile && !d->selectedFiles.isEmpty()) { + // current_file for the file to be pre-selected, current_name for the file name to be + // pre-filled current_file accepts absolute path and requires the file to exist while + // current_name accepts just file name + QFileInfo selectedFileInfo(d->selectedFiles.constFirst()); + if (selectedFileInfo.exists()) + options.insert("current_file"_L1, + QFile::encodeName(d->selectedFiles.constFirst()).append('\0')); + options.insert("current_name"_L1, selectedFileInfo.fileName()); } // Insert filters @@ -209,6 +213,9 @@ void QXdgDesktopPortalFileDialog::openPortal(Qt::WindowFlags windowFlags, Qt::Wi filter.name = mimeType.comment(); filter.filterConditions = filterConditions; + if (filter.name.isEmpty()) + filter.name = mimeTypefilter; + filterList << filter; if (!d->selectedMimeTypeFilter.isEmpty() && d->selectedMimeTypeFilter == mimeTypefilter) @@ -262,7 +269,14 @@ void QXdgDesktopPortalFileDialog::openPortal(Qt::WindowFlags windowFlags, Qt::Wi // TODO choices a(ssa(ss)s) // List of serialized combo boxes to add to the file chooser. - message << parentWindowId << d->title << options; + auto unixServices = dynamic_cast<QGenericUnixServices *>( + QGuiApplicationPrivate::platformIntegration()->services()); + if (parent && unixServices) + message << unixServices->portalWindowIdentifier(parent); + else + message << QString(); + + message << d->title << options; QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp index e6a72e4289..355d3e6cc9 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp @@ -20,8 +20,9 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -class QXdgDesktopPortalThemePrivate : public QPlatformThemePrivate -{ +class QXdgDesktopPortalThemePrivate : public QObject + { + Q_OBJECT public: enum XdgColorschemePref { None, @@ -30,7 +31,7 @@ public: }; QXdgDesktopPortalThemePrivate() - : QPlatformThemePrivate() + : QObject() { } ~QXdgDesktopPortalThemePrivate() @@ -40,7 +41,7 @@ public: /*! \internal - Converts the given Freedesktop color scheme setting \a colorschemePref to a QPlatformTheme::Appearance value. + Converts the given Freedesktop color scheme setting \a colorschemePref to a Qt::ColorScheme value. Specification: https://github.com/flatpak/xdg-desktop-portal/blob/d7a304a00697d7d608821253cd013f3b97ac0fb6/data/org.freedesktop.impl.portal.Settings.xml#L33-L45 Unfortunately the enum numerical values are not defined identically, so we have to convert them. @@ -53,18 +54,29 @@ public: 1: Prefer dark appearance | 2: Dark 2: Prefer light appearance | 1: Light */ - static QPlatformTheme::Appearance appearanceFromXdgPref(const XdgColorschemePref colorschemePref) + static Qt::ColorScheme colorSchemeFromXdgPref(const XdgColorschemePref colorschemePref) { switch (colorschemePref) { - case PreferDark: return QPlatformTheme::Appearance::Dark; - case PreferLight: return QPlatformTheme::Appearance::Light; - default: return QPlatformTheme::Appearance::Unknown; + case PreferDark: return Qt::ColorScheme::Dark; + case PreferLight: return Qt::ColorScheme::Light; + default: return Qt::ColorScheme::Unknown; } } +public Q_SLOTS: + void settingChanged(const QString &group, const QString &key, + const QDBusVariant &value) + { + if (group == "org.freedesktop.appearance"_L1 && key == "color-scheme"_L1) { + colorScheme = colorSchemeFromXdgPref(static_cast<XdgColorschemePref>(value.variant().toUInt())); + QWindowSystemInterface::handleThemeChange(); + } + } + +public: QPlatformTheme *baseTheme = nullptr; uint fileChooserPortalVersion = 0; - QPlatformTheme::Appearance appearance = QPlatformTheme::Appearance::Unknown; + Qt::ColorScheme colorScheme = Qt::ColorScheme::Unknown; }; QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() @@ -75,7 +87,7 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() QStringList themeNames; themeNames += QGuiApplicationPrivate::platform_integration->themeNames(); // 1) Look for a theme plugin. - for (const QString &themeName : qAsConst(themeNames)) { + for (const QString &themeName : std::as_const(themeNames)) { d->baseTheme = QPlatformThemeFactory::create(themeName, nullptr); if (d->baseTheme) break; @@ -84,7 +96,7 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() // 2) If no theme plugin was found ask the platform integration to // create a theme if (!d->baseTheme) { - for (const QString &themeName : qAsConst(themeNames)) { + for (const QString &themeName : std::as_const(themeNames)) { d->baseTheme = QGuiApplicationPrivate::platform_integration->createPlatformTheme(themeName); if (d->baseTheme) break; @@ -104,7 +116,7 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() message << "org.freedesktop.portal.FileChooser"_L1 << "version"_L1; QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [d] (QDBusPendingCallWatcher *watcher) { + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [d] (QDBusPendingCallWatcher *watcher) { QDBusPendingReply<QVariant> reply = *watcher; if (reply.isValid()) { d->fileChooserPortalVersion = reply.value().toUInt(); @@ -124,8 +136,13 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() if (reply.isValid()) { const QDBusVariant dbusVariant = qvariant_cast<QDBusVariant>(reply.value()); const QXdgDesktopPortalThemePrivate::XdgColorschemePref xdgPref = static_cast<QXdgDesktopPortalThemePrivate::XdgColorschemePref>(dbusVariant.variant().toUInt()); - d->appearance = QXdgDesktopPortalThemePrivate::appearanceFromXdgPref(xdgPref); + d->colorScheme = QXdgDesktopPortalThemePrivate::colorSchemeFromXdgPref(xdgPref); } + + QDBusConnection::sessionBus().connect( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Settings"_L1, "SettingChanged"_L1, d_ptr.get(), + SLOT(settingChanged(QString, QString, QDBusVariant))); } QPlatformMenuItem* QXdgDesktopPortalTheme::createPlatformMenuItem() const @@ -205,10 +222,12 @@ QVariant QXdgDesktopPortalTheme::themeHint(ThemeHint hint) const return d->baseTheme->themeHint(hint); } -QPlatformTheme::Appearance QXdgDesktopPortalTheme::appearance() const +Qt::ColorScheme QXdgDesktopPortalTheme::colorScheme() const { Q_D(const QXdgDesktopPortalTheme); - return d->appearance; + if (d->colorScheme == Qt::ColorScheme::Unknown) + return d->baseTheme->colorScheme(); + return d->colorScheme; } QPixmap QXdgDesktopPortalTheme::standardPixmap(StandardPixmap sp, const QSizeF &size) const @@ -245,3 +264,5 @@ QString QXdgDesktopPortalTheme::standardButtonText(int button) const } QT_END_NAMESPACE + +#include "qxdgdesktopportaltheme.moc" diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h index 4390ab73d8..1ac04c45e6 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h @@ -34,7 +34,7 @@ public: QVariant themeHint(ThemeHint hint) const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, diff --git a/src/plugins/printsupport/CMakeLists.txt b/src/plugins/printsupport/CMakeLists.txt index 6d265f9285..7736684762 100644 --- a/src/plugins/printsupport/CMakeLists.txt +++ b/src/plugins/printsupport/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from printsupport.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_cups AND UNIX AND NOT APPLE) add_subdirectory(cups) diff --git a/src/plugins/printsupport/cups/CMakeLists.txt b/src/plugins/printsupport/cups/CMakeLists.txt index 1132ff0845..b6400d2d36 100644 --- a/src/plugins/printsupport/cups/CMakeLists.txt +++ b/src/plugins/printsupport/cups/CMakeLists.txt @@ -1,6 +1,7 @@ -# Generated from cups.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -qt_find_package(Cups PROVIDED_TARGETS Cups::Cups) # special case +qt_find_package(Cups PROVIDED_TARGETS Cups::Cups) ##################################################################### ## QCupsPrinterSupportPlugin Plugin: @@ -25,7 +26,3 @@ qt_internal_add_plugin(QCupsPrinterSupportPlugin Qt::PrintSupport Qt::PrintSupportPrivate ) - -#### Keys ignored in scope 1:.:.:cups.pro:<TRUE>: -# MODULE = "cupsprintersupport" -# OTHER_FILES = "cups.json" diff --git a/src/plugins/printsupport/cups/qcupsprintengine.cpp b/src/plugins/printsupport/cups/qcupsprintengine.cpp index b69d36ab8c..6c50c11c0f 100644 --- a/src/plugins/printsupport/cups/qcupsprintengine.cpp +++ b/src/plugins/printsupport/cups/qcupsprintengine.cpp @@ -144,7 +144,20 @@ bool QCupsPrintEnginePrivate::openPrintDevice() } cupsTempFile = QString::fromLocal8Bit(filename); outDevice = new QFile(); - static_cast<QFile *>(outDevice)->open(fd, QIODevice::WriteOnly); + if (!static_cast<QFile *>(outDevice)->open(fd, QIODevice::WriteOnly)) { + qWarning("QPdfPrinter: Could not open CUPS temporary file descriptor: %s", + qPrintable(outDevice->errorString())); + delete outDevice; + outDevice = nullptr; + +#if defined(Q_OS_WIN) && defined(Q_CC_MSVC) + ::_close(fd); +#else + ::close(fd); +#endif + fd = -1; + return false; + } } return true; @@ -249,9 +262,12 @@ void QCupsPrintEnginePrivate::changePrinter(const QString &newPrinter) duplex = m_printDevice.defaultDuplexMode(); duplexRequestedExplicitly = false; } - QPrint::ColorMode colorMode = grayscale ? QPrint::GrayScale : QPrint::Color; - if (!m_printDevice.supportedColorModes().contains(colorMode)) - grayscale = m_printDevice.defaultColorMode() == QPrint::GrayScale; + QPrint::ColorMode colorMode = static_cast<QPrint::ColorMode>(printerColorMode()); + if (!m_printDevice.supportedColorModes().contains(colorMode)) { + colorModel = (m_printDevice.defaultColorMode() == QPrint::GrayScale) + ? QPdfEngine::ColorModel::Grayscale + : QPdfEngine::ColorModel::RGB; + } // Get the equivalent page size for this printer as supported names may be different if (m_printDevice.supportedPageSize(m_pageLayout.pageSize()).isValid()) diff --git a/src/plugins/printsupport/cups/qcupsprintersupport.cpp b/src/plugins/printsupport/cups/qcupsprintersupport.cpp index c8077bdbf1..6578d8a558 100644 --- a/src/plugins/printsupport/cups/qcupsprintersupport.cpp +++ b/src/plugins/printsupport/cups/qcupsprintersupport.cpp @@ -11,14 +11,14 @@ #include <QtPrintSupport/QPrinterInfo> -#if QT_CONFIG(dialogbuttonbox) +#if QT_CONFIG(cupspassworddialog) #include <QGuiApplication> #include <QDialog> #include <QDialogButtonBox> #include <QFormLayout> #include <QLabel> #include <QLineEdit> -#endif // QT_CONFIG(dialogbuttonbox) +#endif // QT_CONFIG(cupspassworddialog) #include <cups/ppd.h> #ifndef QT_LINUXBASE // LSB merges everything into cups.h @@ -27,7 +27,7 @@ QT_BEGIN_NAMESPACE -#if QT_CONFIG(dialogbuttonbox) +#if QT_CONFIG(cupspassworddialog) static const char *getPasswordCB(const char */*prompt*/, http_t *http, const char */*method*/, const char *resource, void */*user_data*/) { // cups doesn't free the const char * we return so keep around @@ -57,7 +57,7 @@ static const char *getPasswordCB(const char */*prompt*/, http_t *http, const cha QString resourceString = QString::fromLocal8Bit(resource); if (resourceString.startsWith(QStringLiteral("/printers/"))) - resourceString = resourceString.mid(QStringLiteral("/printers/").length()); + resourceString = resourceString.mid(QStringLiteral("/printers/").size()); QLabel *label = new QLabel(); if (hostname == QStringLiteral("localhost")) { @@ -89,16 +89,16 @@ static const char *getPasswordCB(const char */*prompt*/, http_t *http, const cha return password.constData(); } -#endif // QT_CONFIG(dialogbuttonbox) +#endif // QT_CONFIG(cupspassworddialog) QCupsPrinterSupport::QCupsPrinterSupport() : QPlatformPrinterSupport() { -#if QT_CONFIG(dialogbuttonbox) +#if QT_CONFIG(cupspassworddialog) // Only show password dialog if GUI application if (qobject_cast<QGuiApplication*>(QCoreApplication::instance())) cupsSetPasswordCB2(getPasswordCB, nullptr /* user_data */ ); -#endif // QT_CONFIG(dialogbuttonbox) +#endif // QT_CONFIG(cupspassworddialog) } QCupsPrinterSupport::~QCupsPrinterSupport() @@ -129,12 +129,9 @@ QStringList QCupsPrinterSupport::availablePrintDeviceIds() const list.reserve(count); for (int i = 0; i < count; ++i) { QString printerId = QString::fromLocal8Bit(dests[i].name); - if (dests[i].instance) { + if (dests[i].instance) printerId += u'/' + QString::fromLocal8Bit(dests[i].instance); - list.append(printerId); - } else if (cupsGetOption("printer-uri-supported", dests[i].num_options, dests[i].options)) { - list.append(printerId); - } + list.append(printerId); } cupsFreeDests(count, dests); return list; diff --git a/src/plugins/printsupport/cups/qppdprintdevice.cpp b/src/plugins/printsupport/cups/qppdprintdevice.cpp index 487b02784e..95813c90fa 100644 --- a/src/plugins/printsupport/cups/qppdprintdevice.cpp +++ b/src/plugins/printsupport/cups/qppdprintdevice.cpp @@ -428,7 +428,7 @@ bool QPpdPrintDevice::setProperty(QPrintDevice::PrintDevicePropertyKey key, cons { if (key == PDPK_PpdOption) { const QStringList values = value.toStringList(); - if (values.count() == 2) { + if (values.size() == 2) { ppdMarkOption(m_ppd, values[0].toLatin1(), values[1].toLatin1()); return true; } @@ -441,7 +441,7 @@ bool QPpdPrintDevice::isFeatureAvailable(QPrintDevice::PrintDevicePropertyKey ke { if (key == PDPK_PpdChoiceIsInstallableConflict) { const QStringList values = params.toStringList(); - if (values.count() == 2) + if (values.size() == 2) return ppdInstallableConflict(m_ppd, values[0].toLatin1(), values[1].toLatin1()); } diff --git a/src/plugins/sqldrivers/.cmake.conf b/src/plugins/sqldrivers/.cmake.conf index 377be0059e..10bc1fd407 100644 --- a/src/plugins/sqldrivers/.cmake.conf +++ b/src/plugins/sqldrivers/.cmake.conf @@ -1 +1 @@ -set(QT_REPO_MODULE_VERSION "6.5.0") +set(QT_REPO_MODULE_VERSION "6.8.0") diff --git a/src/plugins/sqldrivers/CMakeLists.txt b/src/plugins/sqldrivers/CMakeLists.txt index b704e272f6..43abb00ad1 100644 --- a/src/plugins/sqldrivers/CMakeLists.txt +++ b/src/plugins/sqldrivers/CMakeLists.txt @@ -1,9 +1,17 @@ -# Generated from sqldrivers.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -# special case begin cmake_minimum_required(VERSION 3.16) if (NOT CMAKE_PROJECT_NAME STREQUAL "QtBase" AND NOT CMAKE_PROJECT_NAME STREQUAL "Qt") include(.cmake.conf) + # Store initial build type (if any is specified) to be read by + # qt_internal_set_cmake_build_type(). + # See qt_internal_set_cmake_build_type() for details. + if(DEFINED CACHE{CMAKE_BUILD_TYPE}) + set(__qt_internal_standalone_project_cmake_build_type_before_project_call + "${CMAKE_BUILD_TYPE}") + endif() + project(QSQLiteDriverPlugins VERSION "${QT_REPO_MODULE_VERSION}" DESCRIPTION "Qt6 SQL driver plugins" @@ -48,10 +56,6 @@ if(QT_FEATURE_sql_odbc) add_subdirectory(odbc) endif() -if(QT_FEATURE_sql_tds) -# TODO add_subdirectory(tds) -endif() - if(QT_FEATURE_sql_oci) add_subdirectory(oci) endif() @@ -64,15 +68,14 @@ if(QT_FEATURE_sql_sqlite) add_subdirectory(sqlite) endif() -if(QT_FEATURE_sql_sqlite2) -# TODO add_subdirectory(sqlite2) -endif() - if(QT_FEATURE_sql_ibase) add_subdirectory(ibase) endif() +if(QT_FEATURE_sql_mimer) + add_subdirectory(mimer) +endif() + if(NOT CMAKE_PROJECT_NAME STREQUAL "QtBase" AND NOT CMAKE_PROJECT_NAME STREQUAL "Qt") qt_print_feature_summary() endif() -# special case end diff --git a/src/plugins/sqldrivers/configure.cmake b/src/plugins/sqldrivers/configure.cmake index 25384f3c52..534ac020d8 100644 --- a/src/plugins/sqldrivers/configure.cmake +++ b/src/plugins/sqldrivers/configure.cmake @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + #### Inputs @@ -17,6 +20,7 @@ qt_find_package(Oracle PROVIDED_TARGETS Oracle::OCI MODULE_NAME sqldrivers QMAKE qt_find_package(ODBC PROVIDED_TARGETS ODBC::ODBC MODULE_NAME sqldrivers QMAKE_LIB odbc) qt_find_package(SQLite3 PROVIDED_TARGETS SQLite::SQLite3 MODULE_NAME sqldrivers QMAKE_LIB sqlite3) qt_find_package(Interbase PROVIDED_TARGETS Interbase::Interbase MODULE_NAME sqldrivers QMAKE_LIB ibase) # special case +qt_find_package(Mimer PROVIDED_TARGETS MimerSQL::MimerSQL MODULE_NAME sqldrivers QMAKE_LIB mimer) if(NOT WIN32 AND QT_FEATURE_system_zlib) qt_add_qmake_lib_dependency(sqlite3 zlib) endif() @@ -61,6 +65,11 @@ qt_feature("system-sqlite" PRIVATE AUTODETECT OFF CONDITION QT_FEATURE_sql_sqlite AND SQLite3_FOUND ) +qt_feature("sql-mimer" PRIVATE + LABEL "Mimer" + CONDITION Mimer_FOUND +) + qt_configure_add_summary_section(NAME "Qt Sql Drivers") qt_configure_add_summary_entry(ARGS "sql-db2") qt_configure_add_summary_entry(ARGS "sql-ibase") @@ -70,6 +79,7 @@ qt_configure_add_summary_entry(ARGS "sql-odbc") qt_configure_add_summary_entry(ARGS "sql-psql") qt_configure_add_summary_entry(ARGS "sql-sqlite") qt_configure_add_summary_entry(ARGS "system-sqlite") +qt_configure_add_summary_entry(ARGS "sql-mimer") qt_configure_end_summary_section() # end of "Qt Sql Drivers" section qt_configure_add_report_entry( TYPE WARNING diff --git a/src/plugins/sqldrivers/db2/CMakeLists.txt b/src/plugins/sqldrivers/db2/CMakeLists.txt index 1c693faa3b..119a5b53c9 100644 --- a/src/plugins/sqldrivers/db2/CMakeLists.txt +++ b/src/plugins/sqldrivers/db2/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from db2.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QDB2DriverPlugin Plugin: @@ -20,9 +21,6 @@ qt_internal_add_plugin(QDB2DriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:db2.pro:<TRUE>: -# OTHER_FILES = "db2.json" - ## Scopes: ##################################################################### diff --git a/src/plugins/sqldrivers/db2/qsql_db2.cpp b/src/plugins/sqldrivers/db2/qsql_db2.cpp index 2d9293895a..9b6a06c378 100644 --- a/src/plugins/sqldrivers/db2/qsql_db2.cpp +++ b/src/plugins/sqldrivers/db2/qsql_db2.cpp @@ -14,6 +14,7 @@ #include <QDebug> #include <QtSql/private/qsqldriver_p.h> #include <QtSql/private/qsqlresult_p.h> +#include "private/qtools_p.h" #if defined(Q_CC_BOR) // DB2's sqlsystm.h (included through sqlcli1.h) defines the SQL_BIGINT_TYPE @@ -43,10 +44,13 @@ class QDB2DriverPrivate : public QSqlDriverPrivate Q_DECLARE_PUBLIC(QDB2Driver) public: - QDB2DriverPrivate() : QSqlDriverPrivate(), hEnv(0), hDbc(0) { dbmsType = QSqlDriver::DB2; } - SQLHANDLE hEnv; - SQLHANDLE hDbc; + QDB2DriverPrivate() : QSqlDriverPrivate(QSqlDriver::DB2) {} + SQLHANDLE hEnv = 0; + SQLHANDLE hDbc = 0; QString user; + + void qSplitTableQualifier(const QString &qualifier, QString &catalog, + QString &schema, QString &table) const; }; class QDB2ResultPrivate; @@ -301,7 +305,6 @@ static QSqlField qMakeFieldInfo(const QDB2ResultPrivate* d, int i) // else required is unknown f.setLength(colSize == 0 ? -1 : int(colSize)); f.setPrecision(colScale == 0 ? -1 : int(colScale)); - f.setSqlType(int(colType)); SQLTCHAR tableName[TABLENAMESIZE]; SQLSMALLINT tableNameLen; r = SQLColAttribute(d->hStmt, i + 1, SQL_DESC_BASE_TABLE_NAME, tableName, @@ -461,34 +464,27 @@ static QByteArray qGetBinaryData(SQLHANDLE hStmt, int column, SQLLEN& lengthIndi return fieldVal; } -static void qSplitTableQualifier(const QString & qualifier, QString * catalog, - QString * schema, QString * table) +void QDB2DriverPrivate::qSplitTableQualifier(const QString &qualifier, QString &catalog, + QString &schema, QString &table) const { - if (!catalog || !schema || !table) - return; - QStringList l = qualifier.split(u'.'); - if (l.count() > 3) - return; // can't possibly be a valid table qualifier - int i = 0, n = l.count(); - if (n == 1) { - *table = qualifier; - } else { - for (QStringList::Iterator it = l.begin(); it != l.end(); ++it) { - if (n == 3) { - if (i == 0) - *catalog = *it; - else if (i == 1) - *schema = *it; - else if (i == 2) - *table = *it; - } else if (n == 2) { - if (i == 0) - *schema = *it; - else if (i == 1) - *table = *it; - } - i++; - } + const QList<QStringView> l = QStringView(qualifier).split(u'.'); + switch (l.count()) { + case 1: + table = qualifier; + break; + case 2: + schema = l.at(0).toString(); + table = l.at(1).toString(); + break; + case 3: + catalog = l.at(0).toString(); + schema = l.at(1).toString(); + table = l.at(2).toString(); + break; + default: + qSqlWarning(QString::fromLatin1("QODBCDriver::splitTableQualifier: Unable to split table qualifier '%1'") + .arg(qualifier), this); + break; } } @@ -508,7 +504,6 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt) // else we don't know. f.setLength(qGetIntData(hStmt, 6, isNull)); // column size f.setPrecision(qGetIntData(hStmt, 8, isNull)); // precision - f.setSqlType(type); return f; } @@ -875,7 +870,7 @@ bool QDB2Result::exec() break; } case QMetaType::Int: case QMetaType::Double: - case QMetaType::ByteArray: + case QMetaType::QByteArray: break; case QMetaType::QString: if (bindValueType(i) & QSql::Out) @@ -1353,7 +1348,7 @@ QSqlRecord QDB2Driver::record(const QString& tableName) const SQLHANDLE hStmt; QString catalog, schema, table; - qSplitTableQualifier(tableName, &catalog, &schema, &table); + d->qSplitTableQualifier(tableName, catalog, schema, table); if (schema.isEmpty()) schema = d->user; @@ -1509,7 +1504,7 @@ QSqlIndex QDB2Driver::primaryIndex(const QString& tablename) const return index; } QString catalog, schema, table; - qSplitTableQualifier(tablename, &catalog, &schema, &table); + d->qSplitTableQualifier(tablename, catalog, schema, table); if (isIdentifierEscaped(catalog, QSqlDriver::TableName)) catalog = stripDelimiters(catalog, QSqlDriver::TableName); @@ -1674,17 +1669,17 @@ QString QDB2Driver::formatValue(const QSqlField &field, bool trimStrings) const } } case QMetaType::QByteArray: { - QByteArray ba = field.value().toByteArray(); - QString res; - res += "BLOB(X'"_L1; - static const char hexchars[] = "0123456789abcdef"; - for (int i = 0; i < ba.size(); ++i) { - uchar s = (uchar) ba[i]; - res += QLatin1Char(hexchars[s >> 4]); - res += QLatin1Char(hexchars[s & 0x0f]); + const QByteArray ba = field.value().toByteArray(); + QString r; + r.reserve(ba.size() * 2 + 9); + r += "BLOB(X'"_L1; + for (const char c : ba) { + const uchar s = uchar(c); + r += QLatin1Char(QtMiscUtils::toHexLower(s >> 4)); + r += QLatin1Char(QtMiscUtils::toHexLower(s & 0x0f)); } - res += "')"_L1; - return res; + r += "')"_L1; + return r; } default: return QSqlDriver::formatValue(field, trimStrings); @@ -1702,10 +1697,12 @@ QString QDB2Driver::escapeIdentifier(const QString &identifier, IdentifierType) QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"') ) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } QT_END_NAMESPACE + +#include "moc_qsql_db2_p.cpp" diff --git a/src/plugins/sqldrivers/ibase/CMakeLists.txt b/src/plugins/sqldrivers/ibase/CMakeLists.txt index 8cd5c24dfc..b8f2d2561f 100644 --- a/src/plugins/sqldrivers/ibase/CMakeLists.txt +++ b/src/plugins/sqldrivers/ibase/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QIBaseDriverPlugin OUTPUT_NAME qsqlibase PLUGIN_TYPE sqldrivers @@ -7,6 +10,7 @@ qt_internal_add_plugin(QIBaseDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES Interbase::Interbase Qt::Core diff --git a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp index d228628c08..9bfaeb2e92 100644 --- a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp +++ b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp @@ -4,9 +4,11 @@ #include "qsql_ibase_p.h" #include <QtCore/qcoreapplication.h> #include <QtCore/qdatetime.h> +#include <QtCore/qtimezone.h> #include <QtCore/qdeadlinetimer.h> #include <QtCore/qdebug.h> #include <QtCore/qlist.h> +#include <QtCore/qloggingcategory.h> #include <QtCore/qmap.h> #include <QtCore/qmutex.h> #include <QtCore/qvariant.h> @@ -20,9 +22,12 @@ #include <stdlib.h> #include <limits.h> #include <math.h> +#include <mutex> QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcIbase, "qt.sql.ibase") + using namespace Qt::StringLiterals; #define FBVERSION SQL_DIALECT_V6 @@ -36,7 +41,15 @@ using namespace Qt::StringLiterals; #define blr_boolean_dtype blr_bool #endif -enum { QIBaseChunkSize = SHRT_MAX / 2 }; +constexpr qsizetype QIBaseChunkSize = SHRT_MAX / 2; + +#if (FB_API_VER >= 40) +typedef QMap<quint16, QByteArray> QFbTzIdToIanaIdMap; +typedef QMap<QByteArray, quint16> QIanaIdToFbTzIdMap; +Q_GLOBAL_STATIC(QFbTzIdToIanaIdMap, qFbTzIdToIanaIdMap) +Q_GLOBAL_STATIC(QIanaIdToFbTzIdMap, qIanaIdToFbTzIdMap) +std::once_flag initTZMappingFlag; +#endif static bool getIBaseError(QString& msg, const ISC_STATUS* status, ISC_LONG &sqlcode) { @@ -85,6 +98,9 @@ static void initDA(XSQLDA *sqlda) case SQL_FLOAT: case SQL_DOUBLE: case SQL_TIMESTAMP: +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: +#endif case SQL_TYPE_TIME: case SQL_TYPE_DATE: case SQL_TEXT: @@ -102,6 +118,7 @@ static void initDA(XSQLDA *sqlda) default: // not supported - do not bind. sqlda->sqlvar[i].sqldata = 0; + qCWarning(lcIbase, "initDA: unknown sqltype: %d", sqlda->sqlvar[i].sqltype & ~1); break; } if (sqlda->sqlvar[i].sqltype & 1) { @@ -122,7 +139,7 @@ static void delDA(XSQLDA *&sqlda) delete [] sqlda->sqlvar[i].sqldata; } free(sqlda); - sqlda = 0; + sqlda = nullptr; } static QMetaType::Type qIBaseTypeName(int iType, bool hasScale) @@ -139,6 +156,9 @@ static QMetaType::Type qIBaseTypeName(int iType, bool hasScale) case blr_sql_date: return QMetaType::QDate; case blr_timestamp: +#if (FB_API_VER >= 40) + case blr_timestamp_tz: +#endif return QMetaType::QDateTime; case blr_blob: return QMetaType::QByteArray; @@ -155,7 +175,7 @@ static QMetaType::Type qIBaseTypeName(int iType, bool hasScale) case blr_boolean_dtype: return QMetaType::Bool; } - qWarning("qIBaseTypeName: unknown datatype: %d", iType); + qCWarning(lcIbase, "qIBaseTypeName: unknown datatype: %d", iType); return QMetaType::UnknownType; } @@ -174,6 +194,9 @@ static QMetaType::Type qIBaseTypeName2(int iType, bool hasScale) case SQL_DOUBLE: return QMetaType::Double; case SQL_TIMESTAMP: +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: +#endif return QMetaType::QDateTime; case SQL_TYPE_TIME: return QMetaType::QTime; @@ -186,8 +209,10 @@ static QMetaType::Type qIBaseTypeName2(int iType, bool hasScale) case SQL_BOOLEAN: return QMetaType::Bool; default: - return QMetaType::UnknownType; + break; } + qCWarning(lcIbase, "qIBaseTypeName: unknown datatype: %d", iType); + return QMetaType::UnknownType; } static ISC_TIMESTAMP toTimeStamp(const QDateTime &dt) @@ -208,12 +233,45 @@ static QDateTime fromTimeStamp(char *buffer) // have to demangle the structure ourselves because isc_decode_time // strips the msecs - t = t.addMSecs(int(((ISC_TIMESTAMP*)buffer)->timestamp_time / 10)); - d = bd.addDays(int(((ISC_TIMESTAMP*)buffer)->timestamp_date)); - + auto timebuf = reinterpret_cast<ISC_TIMESTAMP*>(buffer); + t = t.addMSecs(static_cast<int>(timebuf->timestamp_time / 10)); + d = bd.addDays(timebuf->timestamp_date); return QDateTime(d, t); } +#if (FB_API_VER >= 40) +QDateTime fromTimeStampTz(char *buffer) +{ + static const QDate bd(1858, 11, 17); + QTime t(0, 0); + QDate d; + + // have to demangle the structure ourselves because isc_decode_time + // strips the msecs + auto timebuf = reinterpret_cast<ISC_TIMESTAMP_TZ*>(buffer); + t = t.addMSecs(static_cast<int>(timebuf->utc_timestamp.timestamp_time / 10)); + d = bd.addDays(timebuf->utc_timestamp.timestamp_date); + quint16 fpTzID = timebuf->time_zone; + + QByteArray timeZoneName = qFbTzIdToIanaIdMap()->value(fpTzID); + if (!timeZoneName.isEmpty()) + return QDateTime(d, t, QTimeZone(timeZoneName)); + else + return {}; +} + +ISC_TIMESTAMP_TZ toTimeStampTz(const QDateTime &dt) +{ + static const QTime midnight(0, 0, 0, 0); + static const QDate basedate(1858, 11, 17); + ISC_TIMESTAMP_TZ ts; + ts.utc_timestamp.timestamp_time = midnight.msecsTo(dt.time()) * 10; + ts.utc_timestamp.timestamp_date = basedate.daysTo(dt.date()); + ts.time_zone = qIanaIdToFbTzIdMap()->value(dt.timeZone().id().simplified(), 0); + return ts; +} +#endif + static ISC_TIME toTime(QTime t) { static const QTime midnight(0, 0, 0, 0); @@ -282,6 +340,27 @@ public: return true; } +#if (FB_API_VER >= 40) + void initTZMappingCache() + { + Q_Q(QIBaseDriver); + QSqlQuery qry(q->createResult()); + qry.setForwardOnly(true); + qry.exec(QString("select * from RDB$TIME_ZONES"_L1)); + if (qry.lastError().isValid()) { + qCInfo(lcIbase) << "Table 'RDB$TIME_ZONES' not found - not timezone support available"; + return; + } + + while (qry.next()) { + quint16 fbTzId = qry.value(0).value<quint16>(); + QByteArray ianaId = qry.value(1).toByteArray().simplified(); + qFbTzIdToIanaIdMap()->insert(fbTzId, ianaId); + qIanaIdToFbTzIdMap()->insert(ianaId, fbTzId); + } + } +#endif + public: isc_db_handle ibase; isc_tr_handle trans; @@ -320,6 +399,42 @@ protected: int size() override; int numRowsAffected() override; QSqlRecord record() const override; + + template<typename T> + QVariant applyScale(T val, int scale) const + { + if (scale >= 0) + return QVariant(val); + + switch (numericalPrecisionPolicy()) { + case QSql::LowPrecisionInt32: + return QVariant(qint32(val * pow(10.0, scale))); + case QSql::LowPrecisionInt64: + return QVariant(qint64(val * pow(10.0, scale))); + case QSql::LowPrecisionDouble: + return QVariant(double(val * pow(10.0, scale))); + case QSql::HighPrecision: { + const bool negative = val < 0; + QString number; + if constexpr (std::is_signed_v<T> || negative) + number = QString::number(qAbs(val)); + else + number = QString::number(val); + auto len = number.size(); + scale *= -1; + if (scale >= len) { + number = QString(scale - len + 1, u'0') + number; + len = number.size(); + } + const auto sepPos = len - scale; + number = number.left(sepPos) + u'.' + number.mid(sepPos); + if (negative) + number = u'-' + number; + return QVariant(number); + } + } + return QVariant(val); + } }; class QIBaseResultPrivate: public QSqlCachedResultPrivate @@ -352,9 +467,9 @@ public: bool isSelect(); QVariant fetchBlob(ISC_QUAD *bId); - bool writeBlob(int i, const QByteArray &ba); + bool writeBlob(qsizetype iPos, const QByteArray &ba); QVariant fetchArray(int pos, ISC_QUAD *arr); - bool writeArray(int i, const QList<QVariant> &list); + bool writeArray(qsizetype i, const QList<QVariant> &list); public: ISC_STATUS status[20]; isc_tr_handle trans; @@ -374,8 +489,8 @@ QIBaseResultPrivate::QIBaseResultPrivate(QIBaseResult *q, const QIBaseDriver *dr localTransaction(!drv_d_func()->ibase), stmt(0), ibase(drv_d_func()->ibase), - sqlda(0), - inda(0), + sqlda(nullptr), + inda(nullptr), queryType(-1) { } @@ -399,20 +514,20 @@ void QIBaseResultPrivate::cleanup() q->cleanup(); } -bool QIBaseResultPrivate::writeBlob(int i, const QByteArray &ba) +bool QIBaseResultPrivate::writeBlob(qsizetype iPos, const QByteArray &ba) { isc_blob_handle handle = 0; - ISC_QUAD *bId = (ISC_QUAD*)inda->sqlvar[i].sqldata; + ISC_QUAD *bId = (ISC_QUAD*)inda->sqlvar[iPos].sqldata; isc_create_blob2(status, &ibase, &trans, &handle, bId, 0, 0); if (!isError(QT_TRANSLATE_NOOP("QIBaseResult", "Unable to create BLOB"), QSqlError::StatementError)) { - int i = 0; + qsizetype i = 0; while (i < ba.size()) { - isc_put_segment(status, &handle, qMin(ba.size() - i, int(QIBaseChunkSize)), - const_cast<char*>(ba.data()) + i); + isc_put_segment(status, &handle, qMin(ba.size() - i, QIBaseChunkSize), + ba.data() + i); if (isError(QT_TRANSLATE_NOOP("QIBaseResult", "Unable to write BLOB"))) return false; - i += qMin(ba.size() - i, int(QIBaseChunkSize)); + i += qMin(ba.size() - i, QIBaseChunkSize); } } isc_close_blob(status, &handle); @@ -432,7 +547,7 @@ QVariant QIBaseResultPrivate::fetchBlob(ISC_QUAD *bId) QByteArray ba; int chunkSize = QIBaseChunkSize; ba.resize(chunkSize); - int read = 0; + qsizetype read = 0; while (isc_get_segment(status, &handle, &len, chunkSize, ba.data() + read) == 0 || status[1] == isc_segment) { read += len; ba.resize(read + chunkSize); @@ -454,7 +569,7 @@ QVariant QIBaseResultPrivate::fetchBlob(ISC_QUAD *bId) } template<typename T> -static QList<QVariant> toList(char** buf, int count, T* = nullptr) +static QList<QVariant> toList(char** buf, int count) { QList<QVariant> res; for (int i = 0; i < count; ++i) { @@ -493,7 +608,7 @@ static char* readArrayBuffer(QList<QVariant>& list, char *buffer, short curDim, } break; } case blr_long: - valList = toList<int>(&buffer, numElements[dim], static_cast<int *>(0)); + valList = toList<int>(&buffer, numElements[dim]); break; case blr_short: valList = toList<short>(&buffer, numElements[dim]); @@ -513,8 +628,16 @@ static char* readArrayBuffer(QList<QVariant>& list, char *buffer, short curDim, buffer += sizeof(ISC_TIMESTAMP); } break; +#if (FB_API_VER >= 40) + case blr_timestamp_tz: + for (int i = 0; i < numElements[dim]; ++i) { + valList.append(fromTimeStampTz(buffer)); + buffer += sizeof(ISC_TIMESTAMP_TZ); + } + break; +#endif case blr_sql_time: - for(int i = 0; i < numElements[dim]; ++i) { + for (int i = 0; i < numElements[dim]; ++i) { valList.append(fromTime(buffer)); buffer += sizeof(ISC_TIME); } @@ -593,9 +716,8 @@ QVariant QIBaseResultPrivate::fetchArray(int pos, ISC_QUAD *arr) template<typename T> static char* fillList(char *buffer, const QList<QVariant> &list, T* = nullptr) { - for (int i = 0; i < list.size(); ++i) { - T val; - val = qvariant_cast<T>(list.at(i)); + for (const auto &elem : list) { + T val = qvariant_cast<T>(elem); memcpy(buffer, &val, sizeof(T)); buffer += sizeof(T); } @@ -605,11 +727,9 @@ static char* fillList(char *buffer, const QList<QVariant> &list, T* = nullptr) template<> char* fillList<float>(char *buffer, const QList<QVariant> &list, float*) { - for (int i = 0; i < list.size(); ++i) { - double val; - float val2 = 0; - val = qvariant_cast<double>(list.at(i)); - val2 = (float)val; + for (const auto &elem : list) { + double val = qvariant_cast<double>(elem); + float val2 = (float)val; memcpy(buffer, &val2, sizeof(float)); buffer += sizeof(float); } @@ -644,7 +764,6 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, QMetaType::Type type, short curDim, ISC_ARRAY_DESC *arrayDesc, QString& error) { - int i; ISC_ARRAY_BOUND *bounds = arrayDesc->array_desc_bounds; short dim = arrayDesc->array_desc_dimensions - 1; @@ -659,14 +778,14 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, } if (curDim != dim) { - for(i = 0; i < list.size(); ++i) { + for (const auto &elem : list) { - if (list.at(i).typeId() != QMetaType::QVariantList) { // dimensions mismatch + if (elem.typeId() != QMetaType::QVariantList) { // dimensions mismatch error = "Array dimensons mismatch. Fieldname: %1"_L1; return 0; } - buffer = createArrayBuffer(buffer, list.at(i).toList(), type, curDim + 1, + buffer = createArrayBuffer(buffer, elem.toList(), type, curDim + 1, arrayDesc, error); if (!buffer) return 0; @@ -693,29 +812,40 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, buffer = fillList<quint64>(buffer, list); break; case QMetaType::QString: - for (i = 0; i < list.size(); ++i) - buffer = qFillBufferWithString(buffer, list.at(i).toString(), + for (const auto &elem : list) + buffer = qFillBufferWithString(buffer, elem.toString(), arrayDesc->array_desc_length, arrayDesc->array_desc_dtype == blr_varying, true); break; case QMetaType::QDate: - for (i = 0; i < list.size(); ++i) { - *((ISC_DATE*)buffer) = toDate(list.at(i).toDate()); + for (const auto &elem : list) { + *((ISC_DATE*)buffer) = toDate(elem.toDate()); buffer += sizeof(ISC_DATE); } break; case QMetaType::QTime: - for (i = 0; i < list.size(); ++i) { - *((ISC_TIME*)buffer) = toTime(list.at(i).toTime()); + for (const auto &elem : list) { + *((ISC_TIME*)buffer) = toTime(elem.toTime()); buffer += sizeof(ISC_TIME); } break; - case QMetaType::QDateTime: - for (i = 0; i < list.size(); ++i) { - *((ISC_TIMESTAMP*)buffer) = toTimeStamp(list.at(i).toDateTime()); - buffer += sizeof(ISC_TIMESTAMP); + for (const auto &elem : list) { + switch (arrayDesc->array_desc_dtype) { + case blr_timestamp: + *((ISC_TIMESTAMP*)buffer) = toTimeStamp(elem.toDateTime()); + buffer += sizeof(ISC_TIMESTAMP); + break; +#if (FB_API_VER >= 40) + case blr_timestamp_tz: + *((ISC_TIMESTAMP_TZ*)buffer) = toTimeStampTz(elem.toDateTime()); + buffer += sizeof(ISC_TIMESTAMP_TZ); + break; +#endif + default: + break; + } } break; case QMetaType::Bool: @@ -728,7 +858,7 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, return buffer; } -bool QIBaseResultPrivate::writeArray(int column, const QList<QVariant> &list) +bool QIBaseResultPrivate::writeArray(qsizetype column, const QList<QVariant> &list) { Q_Q(QIBaseResult); QString error; @@ -745,7 +875,6 @@ bool QIBaseResultPrivate::writeArray(int column, const QList<QVariant> &list) short arraySize = 1; ISC_LONG bufLen; - QList<QVariant> subList = list; short dimensions = desc.array_desc_dimensions; for(int i = 0; i < dimensions; ++i) { @@ -847,7 +976,7 @@ QIBaseResult::QIBaseResult(const QIBaseDriver *db) bool QIBaseResult::prepare(const QString& query) { Q_D(QIBaseResult); -// qDebug("prepare: %s", qPrintable(query)); +// qDebug("prepare: %ls", qUtf16Printable(query)); if (!driver() || !driver()->isOpen() || driver()->isOpenError()) return false; d->cleanup(); @@ -856,13 +985,13 @@ bool QIBaseResult::prepare(const QString& query) createDA(d->sqlda); if (d->sqlda == (XSQLDA*)0) { - qWarning()<<"QIOBaseResult: createDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: createDA(): failed to allocate memory"; return false; } createDA(d->inda); if (d->inda == (XSQLDA*)0){ - qWarning()<<"QIOBaseResult: createDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: createDA(): failed to allocate memory"; return false; } @@ -886,7 +1015,7 @@ bool QIBaseResult::prepare(const QString& query) if (d->inda->sqld > d->inda->sqln) { enlargeDA(d->inda, d->inda->sqld); if (d->inda == (XSQLDA*)0) { - qWarning()<<"QIOBaseResult: enlargeDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: enlargeDA(): failed to allocate memory"; return false; } @@ -900,7 +1029,7 @@ bool QIBaseResult::prepare(const QString& query) // need more field descriptors enlargeDA(d->sqlda, d->sqlda->sqld); if (d->sqlda == (XSQLDA*)0) { - qWarning()<<"QIOBaseResult: enlargeDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: enlargeDA(): failed to allocate memory"; return false; } @@ -920,7 +1049,6 @@ bool QIBaseResult::prepare(const QString& query) return true; } - bool QIBaseResult::exec() { Q_D(QIBaseResult); @@ -936,20 +1064,17 @@ bool QIBaseResult::exec() if (d->inda) { const QList<QVariant> &values = boundValues(); - int i; if (values.count() > d->inda->sqld) { - qWarning() << "QIBaseResult::exec: Parameter mismatch, expected"_L1 << - d->inda->sqld << ", got"_L1 << values.count() << - "parameters"_L1; + qCWarning(lcIbase) << "QIBaseResult::exec: Parameter mismatch, expected"_L1 + << d->inda->sqld << ", got"_L1 << values.count() + << "parameters"_L1; return false; } - int para = 0; - for (i = 0; i < values.count(); ++i) { - para = i; + for (qsizetype para = 0; para < values.count(); ++para) { if (!d->inda->sqlvar[para].sqldata) // skip unknown datatypes continue; - const QVariant val(values[i]); + const QVariant &val = values[para]; if (d->inda->sqlvar[para].sqltype & 1) { if (QSqlResultPrivate::isVariantNull(val)) { // set null indicator @@ -961,6 +1086,12 @@ bool QIBaseResult::exec() } // a value of 0 means non-null. *(d->inda->sqlvar[para].sqlind) = 0; + } else { + if (QSqlResultPrivate::isVariantNull(val)) { + qCWarning(lcIbase) << "QIBaseResult::exec: Null value replaced by default (zero)"_L1 + << "value for type of column"_L1 << d->inda->sqlvar[para].ownname + << ", which is not nullable."_L1; + } } switch(d->inda->sqlvar[para].sqltype & ~1) { case SQL_INT64: @@ -997,6 +1128,11 @@ bool QIBaseResult::exec() case SQL_TIMESTAMP: *((ISC_TIMESTAMP*)d->inda->sqlvar[para].sqldata) = toTimeStamp(val.toDateTime()); break; +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: + *((ISC_TIMESTAMP_TZ*)d->inda->sqlvar[para].sqldata) = toTimeStampTz(val.toDateTime()); + break; +#endif case SQL_TYPE_TIME: *((ISC_TIME*)d->inda->sqlvar[para].sqldata) = toTime(val.toTime()); break; @@ -1019,8 +1155,8 @@ bool QIBaseResult::exec() *((bool*)d->inda->sqlvar[para].sqldata) = val.toBool(); break; default: - qWarning("QIBaseResult::exec: Unknown datatype %d", - d->inda->sqlvar[para].sqltype & ~1); + qCWarning(lcIbase, "QIBaseResult::exec: Unknown datatype %d", + d->inda->sqlvar[para].sqltype & ~1); break; } } @@ -1132,27 +1268,27 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx) // pascal strings - a short with a length information followed by the data row[idx] = QString::fromUtf8(buf + sizeof(short), *(short*)buf); break; - case SQL_INT64: - if (d->sqlda->sqlvar[i].sqlscale < 0) - row[idx] = *(qint64*)buf * pow(10.0, d->sqlda->sqlvar[i].sqlscale); - else - row[idx] = QVariant(*(qint64*)buf); + case SQL_INT64: { + Q_ASSERT(d->sqlda->sqlvar[i].sqllen == sizeof(qint64)); + const auto val = *(qint64 *)buf; + const auto scale = d->sqlda->sqlvar[i].sqlscale; + row[idx] = applyScale(val, scale); break; + } case SQL_LONG: - if (d->sqlda->sqlvar[i].sqllen == 4) - if (d->sqlda->sqlvar[i].sqlscale < 0) - row[idx] = QVariant(*(qint32*)buf * pow(10.0, d->sqlda->sqlvar[i].sqlscale)); - else - row[idx] = QVariant(*(qint32*)buf); - else + if (d->sqlda->sqlvar[i].sqllen == 4) { + const auto val = *(qint32 *)buf; + const auto scale = d->sqlda->sqlvar[i].sqlscale; + row[idx] = applyScale(val, scale); + } else row[idx] = QVariant(*(qint64*)buf); break; - case SQL_SHORT: - if (d->sqlda->sqlvar[i].sqlscale < 0) - row[idx] = QVariant(long((*(short*)buf)) * pow(10.0, d->sqlda->sqlvar[i].sqlscale)); - else - row[idx] = QVariant(int((*(short*)buf))); + case SQL_SHORT: { + const auto val = *(short *)buf; + const auto scale = d->sqlda->sqlvar[i].sqlscale; + row[idx] = applyScale(val, scale); break; + } case SQL_FLOAT: row[idx] = QVariant(double((*(float*)buf))); break; @@ -1180,32 +1316,18 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx) case SQL_BOOLEAN: row[idx] = QVariant(bool((*(bool*)buf))); break; +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: + row[idx] = fromTimeStampTz(buf); + break; +#endif default: // unknown type - don't even try to fetch + qCWarning(lcIbase, "gotoNext: unknown sqltype: %d", + d->sqlda->sqlvar[i].sqltype & ~1); row[idx] = QVariant(); break; } - if (d->sqlda->sqlvar[i].sqlscale < 0) { - QVariant v = row[idx]; - switch(numericalPrecisionPolicy()) { - case QSql::LowPrecisionInt32: - if (v.convert(QMetaType(QMetaType::Int))) - row[idx]=v; - break; - case QSql::LowPrecisionInt64: - if (v.convert(QMetaType(QMetaType::LongLong))) - row[idx]=v; - break; - case QSql::LowPrecisionDouble: - if (v.convert(QMetaType(QMetaType::Double))) - row[idx]=v; - break; - case QSql::HighPrecision: - if (v.convert(QMetaType(QMetaType::QString))) - row[idx]=v; - break; - } - } } return true; @@ -1286,7 +1408,7 @@ int QIBaseResult::numRowsAffected() bIsProcedure = true; // will sum all changes break; default: - qWarning() << "numRowsAffected: Unknown statement type (" << d->queryType << ")"; + qCWarning(lcIbase) << "numRowsAffected: Unknown statement type (" << d->queryType << ")"; return -1; } @@ -1352,7 +1474,6 @@ QSqlRecord QIBaseResult::record() const f.setRequiredStatus(q.value(3).toBool() ? QSqlField::Required : QSqlField::Optional); } } - f.setSqlType(v.sqltype); rec.append(f); } return rec; @@ -1408,26 +1529,26 @@ bool QIBaseDriver::hasFeature(DriverFeature f) const return false; } -bool QIBaseDriver::open(const QString & db, - const QString & user, - const QString & password, - const QString & host, - int port, - const QString & connOpts) +bool QIBaseDriver::open(const QString &db, + const QString &user, + const QString &password, + const QString &host, + int port, + const QString &connOpts) { Q_D(QIBaseDriver); if (isOpen()) close(); - const QStringList opts(connOpts.split(u';', Qt::SkipEmptyParts)); + const auto opts(QStringView(connOpts).split(u';', Qt::SkipEmptyParts)); QByteArray role; - for (int i = 0; i < opts.count(); ++i) { - QString tmp(opts.at(i).simplified()); + for (const auto &opt : opts) { + const auto tmp(opt.trimmed()); qsizetype idx; if ((idx = tmp.indexOf(u'=')) != -1) { - QString val = tmp.mid(idx + 1).simplified(); - QString opt = tmp.left(idx).simplified(); + const auto val = tmp.mid(idx + 1).trimmed(); + const auto opt = tmp.left(idx).trimmed().toString(); if (opt.toUpper() == "ISC_DPB_SQL_ROLE_NAME"_L1) { role = val.toLocal8Bit(); role.truncate(255); @@ -1478,6 +1599,14 @@ bool QIBaseDriver::open(const QString & db, setOpen(true); setOpenError(false); +#if (FB_API_VER >= 40) + std::call_once(initTZMappingFlag, [d](){ d->initTZMappingCache(); }); + if (lastError().isValid()) + { + setOpen(true); + return false; + } +#endif return true; } @@ -1496,13 +1625,6 @@ void QIBaseDriver::close() qFreeEventBuffer(eBuffer); } d->eventBuffers.clear(); - -#if defined(FB_API_VER) - // TODO check whether this workaround for Firebird crash is still needed - QDeadlineTimer timer(500); - while (!timer.hasExpired()) - QCoreApplication::processEvents(); -#endif } isc_detach_database(d->status, &d->ibase); @@ -1587,8 +1709,8 @@ QStringList QIBaseDriver::tables(QSql::TableType type) const q.setForwardOnly(true); if (!q.exec("select rdb$relation_name from rdb$relations "_L1 + typeFilter)) return res; - while(q.next()) - res << q.value(0).toString().simplified(); + while (q.next()) + res << q.value(0).toString().simplified(); return res; } @@ -1599,13 +1721,9 @@ QSqlRecord QIBaseDriver::record(const QString& tablename) const if (!isOpen()) return rec; + const QString table = stripDelimiters(tablename, QSqlDriver::TableName); QSqlQuery q(createResult()); q.setForwardOnly(true); - QString table = tablename; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - else - table = table.toUpper(); q.exec("SELECT a.RDB$FIELD_NAME, b.RDB$FIELD_TYPE, b.RDB$FIELD_LENGTH, " "b.RDB$FIELD_SCALE, b.RDB$FIELD_PRECISION, a.RDB$NULL_FLAG " "FROM RDB$RELATION_FIELDS a, RDB$FIELDS b " @@ -1625,7 +1743,6 @@ QSqlRecord QIBaseDriver::record(const QString& tablename) const f.setPrecision(0); } f.setRequired(q.value(5).toInt() > 0); - f.setSqlType(type); rec.append(f); } @@ -1638,12 +1755,7 @@ QSqlIndex QIBaseDriver::primaryIndex(const QString &table) const if (!isOpen()) return index; - QString tablename = table; - if (isIdentifierEscaped(tablename, QSqlDriver::TableName)) - tablename = stripDelimiters(tablename, QSqlDriver::TableName); - else - tablename = tablename.toUpper(); - + const QString tablename = stripDelimiters(table, QSqlDriver::TableName); QSqlQuery q(createResult()); q.setForwardOnly(true); q.exec("SELECT a.RDB$INDEX_NAME, b.RDB$FIELD_NAME, d.RDB$FIELD_TYPE, d.RDB$FIELD_SCALE " @@ -1738,13 +1850,13 @@ bool QIBaseDriver::subscribeToNotification(const QString &name) { Q_D(QIBaseDriver); if (!isOpen()) { - qWarning("QIBaseDriver::subscribeFromNotificationImplementation: database not open."); + qCWarning(lcIbase, "QIBaseDriver::subscribeFromNotificationImplementation: database not open."); return false; } if (d->eventBuffers.contains(name)) { - qWarning("QIBaseDriver::subscribeToNotificationImplementation: already subscribing to '%s'.", - qPrintable(name)); + qCWarning(lcIbase, "QIBaseDriver::subscribeToNotificationImplementation: already subscribing to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1785,13 +1897,13 @@ bool QIBaseDriver::unsubscribeFromNotification(const QString &name) { Q_D(QIBaseDriver); if (!isOpen()) { - qWarning("QIBaseDriver::unsubscribeFromNotificationImplementation: database not open."); + qCWarning(lcIbase, "QIBaseDriver::unsubscribeFromNotificationImplementation: database not open."); return false; } if (!d->eventBuffers.contains(name)) { - qWarning("QIBaseDriver::QIBaseSubscriptionState not subscribed to '%s'.", - qPrintable(name)); + qCWarning(lcIbase, "QIBaseDriver::QIBaseSubscriptionState not subscribed to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1846,8 +1958,8 @@ void QIBaseDriver::qHandleEventNotification(void *updatedResultBuffer) (&qEventCallback)), eBuffer->resultBuffer); if (Q_UNLIKELY(status[0] == 1 && status[1])) { - qCritical("QIBaseDriver::qHandleEventNotification: could not resubscribe to '%s'", - qPrintable(i.key())); + qCritical("QIBaseDriver::qHandleEventNotification: could not resubscribe to '%ls'", + qUtf16Printable(i.key())); } return; @@ -1860,8 +1972,8 @@ QString QIBaseDriver::escapeIdentifier(const QString &identifier, IdentifierType QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"') ) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } @@ -1873,3 +1985,5 @@ int QIBaseDriver::maximumIdentifierLength(IdentifierType type) const } QT_END_NAMESPACE + +#include "moc_qsql_ibase_p.cpp" diff --git a/src/plugins/sqldrivers/mimer/CMakeLists.txt b/src/plugins/sqldrivers/mimer/CMakeLists.txt new file mode 100644 index 0000000000..303af7120c --- /dev/null +++ b/src/plugins/sqldrivers/mimer/CMakeLists.txt @@ -0,0 +1,24 @@ +# Generated from mimer.pro. + +##################################################################### +## MIMERSQLDriverPlugin Plugin: +##################################################################### + +qt_internal_add_plugin(QMimerSQLDriverPlugin + OUTPUT_NAME qsqlmimer + PLUGIN_TYPE sqldrivers + SOURCES + main.cpp + qsql_mimer.cpp qsql_mimer.h + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT + LIBRARIES + MimerSQL::MimerSQL + Qt::Core + Qt::SqlPrivate +) + +#### Keys ignored in scope 1:.:.:mimer.pro:<TRUE>: +# OTHER_FILES = "mimer.json" diff --git a/src/plugins/sqldrivers/mimer/README b/src/plugins/sqldrivers/mimer/README new file mode 100644 index 0000000000..02e4c3e162 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/README @@ -0,0 +1,6 @@ +You will need the Mimer SQL development headers and libraries installed before +compiling this plugin. qsql_mimer.h contains an include to mimerapi.h that is +needed for the driver to compile. + +See the Qt SQL documentation for more information on compiling Qt SQL driver +plugins. diff --git a/src/plugins/sqldrivers/mimer/main.cpp b/src/plugins/sqldrivers/mimer/main.cpp new file mode 100644 index 0000000000..560b7da7c7 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/main.cpp @@ -0,0 +1,33 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Mimer Information Technology +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qsql_mimer.h" + +#include <qsqldriverplugin.h> +#include <qstringlist.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +class QMimerSQLDriverPlugin : public QSqlDriverPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QSqlDriverFactoryInterface" FILE "mimer.json") +public: + QMimerSQLDriverPlugin(); + QSqlDriver *create(const QString &) override; +}; + +QMimerSQLDriverPlugin::QMimerSQLDriverPlugin() : QSqlDriverPlugin() { } + +QSqlDriver *QMimerSQLDriverPlugin::create(const QString &name) +{ + if (name == "QMIMER"_L1) + return new QMimerSQLDriver; + return nullptr; +} + +QT_END_NAMESPACE + +#include "main.moc" diff --git a/src/plugins/sqldrivers/mimer/mimer.json b/src/plugins/sqldrivers/mimer/mimer.json new file mode 100644 index 0000000000..fba96b765d --- /dev/null +++ b/src/plugins/sqldrivers/mimer/mimer.json @@ -0,0 +1,5 @@ +{ + "Keys": [ + "QMIMER" + ] +} diff --git a/src/plugins/sqldrivers/mimer/qsql_mimer.cpp b/src/plugins/sqldrivers/mimer/qsql_mimer.cpp new file mode 100644 index 0000000000..7f89e0a0d5 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/qsql_mimer.cpp @@ -0,0 +1,1610 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Mimer Information Technology +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include <qcoreapplication.h> +#include <qvariant.h> +#include <qmetatype.h> +#include <qdatetime.h> +#include <qloggingcategory.h> +#include <qsqlerror.h> +#include <qsqlfield.h> +#include <qsqlindex.h> +#include <qsqlrecord.h> +#include <qsqlquery.h> +#include <qsocketnotifier.h> +#include <qstringlist.h> +#include <qlocale.h> +#if defined(Q_OS_WIN32) +# include <QtCore/qt_windows.h> +#endif +#include <QtSql/private/qsqlresult_p.h> +#include <QtSql/private/qsqldriver_p.h> +#include "qsql_mimer.h" + +#define MIMER_DEFAULT_DATATYPE 1000 + +Q_DECLARE_OPAQUE_POINTER(MimerSession) +Q_DECLARE_METATYPE(MimerSession) + +Q_DECLARE_OPAQUE_POINTER(MimerStatement) +Q_DECLARE_METATYPE(MimerStatement) + +QT_BEGIN_NAMESPACE + +static Q_LOGGING_CATEGORY(lcMimer, "qt.sql.mimer") + +enum class MimerColumnTypes { + Binary, + Clob, + Blob, + String, + Int, + Numeric, + Long, + Float, + Double, + Boolean, + Uuid, + Date, + Time, + Timestamp, + Unknown +}; + +using namespace Qt::StringLiterals; + +class QMimerSQLResultPrivate; + +class QMimerSQLResult final : public QSqlResult +{ + Q_DECLARE_PRIVATE(QMimerSQLResult) +public: + QMimerSQLResult(const QMimerSQLDriver *db); + virtual ~QMimerSQLResult() override; + QVariant handle() const override; + static constexpr int genericError = -1; + static constexpr int lobChunkMaxSizeSet = 1048500; + static constexpr int lobChunkMaxSizeFetch = 65536; + static constexpr int maxStackStringSize = 200; + static constexpr int maxTimeStringSize = 18; + static constexpr int maxDateStringSize = 10; + static constexpr int maxTimestampStringSize = 29; + +private: + void cleanup(); + bool fetch(int i) override; + bool fetchFirst() override; + bool fetchLast() override; + bool fetchNext() override; + QVariant data(int i) override; + bool isNull(int index) override; + bool reset(const QString &query) override; + int size() override; + int numRowsAffected() override; + QSqlRecord record() const override; + bool prepare(const QString &query) override; + bool execBatch(bool arrayBind = false) override; + bool exec() override; + qint64 currentRow(); + QVariant lastInsertId() const override; +}; + +class QMimerSQLDriverPrivate final : public QSqlDriverPrivate +{ + Q_DECLARE_PUBLIC(QMimerSQLDriver) +public: + QMimerSQLDriverPrivate() : QSqlDriverPrivate(QSqlDriver::MimerSQL), sessionhandle(nullptr) { } + MimerSession sessionhandle; + QString dbName; + QString dbUser; + void splitTableQualifier(const QString &qualifier, QString *schema, QString *table) const; +}; + +class QMimerSQLResultPrivate : public QSqlResultPrivate +{ + Q_DECLARE_PUBLIC(QMimerSQLResult) +public: + Q_DECLARE_SQLDRIVER_PRIVATE(QMimerSQLDriver) + QMimerSQLResultPrivate(QMimerSQLResult *q, const QMimerSQLDriver *drv) + : QSqlResultPrivate(q, drv), + statementhandle(nullptr), + lobhandle(nullptr), + rowsAffected(0), + preparedQuery(false), + openCursor(false), + openStatement(false), + executedStatement(false), + callWithOut(false), + execBatch(false), + currentRow(QSql::BeforeFirstRow) + { + } + MimerStatement statementhandle; + MimerLob lobhandle; + int rowsAffected; + bool preparedQuery; + bool openCursor; + bool openStatement; + bool executedStatement; + bool callWithOut; + bool execBatch; + qint64 currentSize = -1; + qint64 currentRow; // Only used when forwardOnly() + QVector<QVariant> batch_vector; +}; + +static QSqlError qMakeError(const QString &err, const int errCode, QSqlError::ErrorType type, + const QMimerSQLDriverPrivate *p) +{ + QString msg; + if (p) { + size_t str_len; + int e_code; + int rc; + str_len = (rc = MimerGetError(p->sessionhandle, &e_code, NULL, 0)) + 1; + if (!MIMER_SUCCEEDED(rc)) { + msg = QCoreApplication::translate("QMimerSQL", "No Mimer SQL error for code %1") + .arg(errCode); + } else { + QVarLengthArray<wchar_t> tmp_buff((qsizetype)str_len); + if (!MIMER_SUCCEEDED( + rc = MimerGetError(p->sessionhandle, &e_code, tmp_buff.data(), str_len))) + msg = QCoreApplication::translate("QMimerSQL", "No Mimer SQL error for code %1") + .arg(errCode); + else + msg = QString::fromWCharArray(tmp_buff.data()); + } + } else { + msg = QCoreApplication::translate("QMimerSQL", "Generic Mimer SQL error"); + } + + return QSqlError("QMIMER: "_L1 + err, msg, type, QString::number(errCode)); +} + +static QString msgCouldNotGet(const char *type, int column) +{ + //: Data type, column + return QCoreApplication::translate("QMimerSQLResult", + "Could not get %1, column %2").arg(QLatin1StringView(type)).arg(column); +} + +static QString msgCouldNotSet(const char *type, int column) +{ + //: Data type, parameter + return QCoreApplication::translate("QMimerSQLResult", + "Could not set %1, parameter %2").arg(QLatin1StringView(type)).arg(column); +} + +QMimerSQLDriver::QMimerSQLDriver(QObject *parent) : QSqlDriver(*new QMimerSQLDriverPrivate, parent) +{ +} + +QMimerSQLDriver::QMimerSQLDriver(MimerSession *conn, QObject *parent) + : QSqlDriver(*new QMimerSQLDriverPrivate, parent) +{ + Q_D(QMimerSQLDriver); + if (conn) + d->sessionhandle = *conn; +} + +QMimerSQLDriver::~QMimerSQLDriver() +{ + close(); +} + +QMimerSQLResult::QMimerSQLResult(const QMimerSQLDriver *db) + : QSqlResult(*new QMimerSQLResultPrivate(this, db)) +{ + Q_D(QMimerSQLResult); + d->preparedQuery = db->hasFeature(QSqlDriver::PreparedQueries); +} + +QMimerSQLResult::~QMimerSQLResult() +{ + cleanup(); +} + +static MimerColumnTypes mimerMapColumnTypes(int32_t t) +{ + switch (t) { + case MIMER_BINARY: + case MIMER_BINARY_VARYING: + return MimerColumnTypes::Binary; + case MIMER_BLOB: + case MIMER_NATIVE_BLOB: + return MimerColumnTypes::Blob; + case MIMER_CLOB: + case MIMER_NCLOB: + case MIMER_NATIVE_CLOB: + case MIMER_NATIVE_NCLOB: + return MimerColumnTypes::Clob; + case MIMER_DATE: + return MimerColumnTypes::Date; + case MIMER_TIME: + return MimerColumnTypes::Time; + case MIMER_TIMESTAMP: + return MimerColumnTypes::Timestamp; + case MIMER_INTERVAL_DAY: + case MIMER_INTERVAL_DAY_TO_HOUR: + case MIMER_INTERVAL_DAY_TO_MINUTE: + case MIMER_INTERVAL_DAY_TO_SECOND: + case MIMER_INTERVAL_HOUR: + case MIMER_INTERVAL_HOUR_TO_MINUTE: + case MIMER_INTERVAL_HOUR_TO_SECOND: + case MIMER_INTERVAL_MINUTE: + case MIMER_INTERVAL_MINUTE_TO_SECOND: + case MIMER_INTERVAL_MONTH: + case MIMER_INTERVAL_SECOND: + case MIMER_INTERVAL_YEAR: + case MIMER_INTERVAL_YEAR_TO_MONTH: + case MIMER_NCHAR: + case MIMER_CHARACTER: + case MIMER_CHARACTER_VARYING: + case MIMER_NCHAR_VARYING: + case MIMER_UTF8: + case MIMER_DEFAULT_DATATYPE: + return MimerColumnTypes::String; + case MIMER_INTEGER: + case MIMER_DECIMAL: + case MIMER_FLOAT: + return MimerColumnTypes::Numeric; + case MIMER_BOOLEAN: + return MimerColumnTypes::Boolean; + case MIMER_T_BIGINT: + case MIMER_T_UNSIGNED_BIGINT: + case MIMER_NATIVE_BIGINT_NULLABLE: + case MIMER_NATIVE_BIGINT: + return MimerColumnTypes::Long; + case MIMER_NATIVE_REAL_NULLABLE: + case MIMER_NATIVE_REAL: + case MIMER_T_REAL: + return MimerColumnTypes::Float; + case MIMER_T_FLOAT: + case MIMER_NATIVE_DOUBLE_NULLABLE: + case MIMER_NATIVE_DOUBLE: + case MIMER_T_DOUBLE: + return MimerColumnTypes::Double; + case MIMER_NATIVE_INTEGER: + case MIMER_NATIVE_INTEGER_NULLABLE: + case MIMER_NATIVE_SMALLINT_NULLABLE: + case MIMER_NATIVE_SMALLINT: + case MIMER_T_INTEGER: + case MIMER_T_SMALLINT: + return MimerColumnTypes::Int; + case MIMER_UUID: + return MimerColumnTypes::Uuid; + default: + qCWarning(lcMimer) << "QMimerSQLDriver::mimerMapColumnTypes: Unknown data type:" << t; + } + return MimerColumnTypes::Unknown; +} + +static QMetaType::Type qDecodeMSQLType(int32_t t) +{ + switch (t) { + case MIMER_BINARY: + case MIMER_BINARY_VARYING: + case MIMER_BLOB: + case MIMER_NATIVE_BLOB: + return QMetaType::QByteArray; + case MIMER_CLOB: + case MIMER_NCLOB: + case MIMER_NATIVE_CLOB: + case MIMER_NATIVE_NCLOB: + case MIMER_INTERVAL_DAY: + case MIMER_DECIMAL: + case MIMER_INTERVAL_DAY_TO_HOUR: + case MIMER_INTERVAL_DAY_TO_MINUTE: + case MIMER_INTERVAL_DAY_TO_SECOND: + case MIMER_INTERVAL_HOUR: + case MIMER_INTERVAL_HOUR_TO_MINUTE: + case MIMER_INTERVAL_HOUR_TO_SECOND: + case MIMER_INTERVAL_MINUTE: + case MIMER_INTERVAL_MINUTE_TO_SECOND: + case MIMER_INTERVAL_MONTH: + case MIMER_INTERVAL_SECOND: + case MIMER_INTERVAL_YEAR: + case MIMER_INTERVAL_YEAR_TO_MONTH: + case MIMER_NCHAR: + case MIMER_CHARACTER: + case MIMER_CHARACTER_VARYING: + case MIMER_NCHAR_VARYING: + case MIMER_UTF8: + case MIMER_DEFAULT_DATATYPE: + case MIMER_INTEGER: + case MIMER_FLOAT: + return QMetaType::QString; + case MIMER_BOOLEAN: + return QMetaType::Bool; + case MIMER_T_BIGINT: + case MIMER_T_UNSIGNED_BIGINT: + case MIMER_NATIVE_BIGINT_NULLABLE: + case MIMER_NATIVE_BIGINT: + return QMetaType::LongLong; + case MIMER_NATIVE_REAL_NULLABLE: + case MIMER_NATIVE_REAL: + case MIMER_T_REAL: + return QMetaType::Float; + case MIMER_T_FLOAT: + case MIMER_NATIVE_DOUBLE_NULLABLE: + case MIMER_NATIVE_DOUBLE: + case MIMER_T_DOUBLE: + return QMetaType::Double; + case MIMER_NATIVE_INTEGER_NULLABLE: + case MIMER_T_INTEGER: + case MIMER_NATIVE_INTEGER: + return QMetaType::Int; + case MIMER_NATIVE_SMALLINT_NULLABLE: + case MIMER_T_SMALLINT: + return QMetaType::Int; + case MIMER_DATE: + return QMetaType::QDate; + case MIMER_TIME: + return QMetaType::QTime; + break; + case MIMER_TIMESTAMP: + return QMetaType::QDateTime; + case MIMER_UUID: + return QMetaType::QUuid; + default: + qCWarning(lcMimer) << "QMimerSQLDriver::qDecodeMSQLType: Unknown data type:" << t; + return QMetaType::UnknownType; + } +} + +static int32_t qLookupMimDataType(QStringView s) +{ + if (s == u"BINARY") + return MIMER_BINARY; + if (s == u"BINARY VARYING") + return MIMER_BINARY_VARYING; + if (s == u"BINARY LARGE OBJECT") + return MIMER_BLOB; + if (s == u"CHARACTER LARGE OBJECT") + return MIMER_CLOB; + if (s == u"NATIONAL CHAR LARGE OBJECT") + return MIMER_NCLOB; + if (s == u"INTERVAL DAY") + return MIMER_INTERVAL_DAY; + if (s == u"DECIMAL") + return MIMER_DECIMAL; + if (s == u"INTERVAL DAY TO HOUR") + return MIMER_INTERVAL_DAY_TO_HOUR; + if (s == u"INTERVAL DAY TO MINUTE") + return MIMER_INTERVAL_DAY_TO_MINUTE; + if (s == u"INTERVAL DAY TO SECOND") + return MIMER_INTERVAL_DAY_TO_SECOND; + if (s == u"INTERVAL HOUR") + return MIMER_INTERVAL_HOUR; + if (s == u"INTERVAL HOUR TO MINUTE") + return MIMER_INTERVAL_HOUR_TO_MINUTE; + if (s == u"INTERVAL HOUR TO SECOND") + return MIMER_INTERVAL_HOUR_TO_SECOND; + if (s == u"INTERVAL MINUTE") + return MIMER_INTERVAL_MINUTE; + if (s == u"INTERVAL MINUTE TO SECOND") + return MIMER_INTERVAL_MINUTE_TO_SECOND; + if (s == u"INTERVAL MONTH") + return MIMER_INTERVAL_MONTH; + if (s == u"INTERVAL SECOND") + return MIMER_INTERVAL_SECOND; + if (s == u"INTERVAL YEAR") + return MIMER_INTERVAL_YEAR; + if (s == u"INTERVAL YEAR TO MONTH") + return MIMER_INTERVAL_YEAR_TO_MONTH; + if (s == u"NATIONAL CHARACTER") + return MIMER_NCHAR; + if (s == u"CHARACTER") + return MIMER_CHARACTER; + if (s == u"CHARACTER VARYING") + return MIMER_CHARACTER_VARYING; + if (s == u"NATIONAL CHARACTER VARYING") + return MIMER_NCHAR_VARYING; + if (s == u"UTF-8") + return MIMER_UTF8; + if (s == u"BOOLEAN") + return MIMER_BOOLEAN; + if (s == u"BIGINT") + return MIMER_T_BIGINT; + if (s == u"REAL") + return MIMER_T_REAL; + if (s == u"FLOAT") + return MIMER_T_FLOAT; + if (s == u"DOUBLE PRECISION") + return MIMER_T_DOUBLE; + if (s == u"INTEGER") + return MIMER_T_INTEGER; + if (s == u"SMALLINT") + return MIMER_T_SMALLINT; + if (s == u"DATE") + return MIMER_DATE; + if (s == u"TIME") + return MIMER_TIME; + if (s == u"TIMESTAMP") + return MIMER_TIMESTAMP; + if (s == u"BUILTIN.UUID") + return MIMER_UUID; + if (s == u"USER-DEFINED") + return MIMER_DEFAULT_DATATYPE; + qCWarning(lcMimer) << "QMimerSQLDriver::qLookupMimDataType: Unhandled data type:" << s; + return MIMER_DEFAULT_DATATYPE; +} + +QVariant QMimerSQLResult::handle() const +{ + Q_D(const QMimerSQLResult); + return QVariant::fromValue(d->statementhandle); +} + +void QMimerSQLResult::cleanup() +{ + Q_D(QMimerSQLResult); + if (!driver() || !driver()->isOpen()) { + d->openCursor = false; + d->openStatement = false; + return; + } + if (d->openCursor) { + const int32_t err = MimerCloseCursor(d->statementhandle); + if (!MIMER_SUCCEEDED(err)) + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not close cursor"), err, + QSqlError::StatementError, d->drv_d_func())); + d->openCursor = false; + } + if (d->openStatement) { + const int32_t err = MimerEndStatement(&d->statementhandle); + if (!MIMER_SUCCEEDED(err)) + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not close statement"), + err, QSqlError::StatementError, d->drv_d_func())); + d->openStatement = false; + } + d->currentSize = -1; +} + +qint64 QMimerSQLResult::currentRow() +{ + Q_D(const QMimerSQLResult); + return d->currentRow; +} + +bool QMimerSQLResult::fetch(int i) +{ + Q_D(const QMimerSQLResult); + int32_t err = 0; + if (!isActive() || !isSelect()) + return false; + if (i == at()) + return true; + if (i < 0) + return false; + + if (isForwardOnly() && i < at()) + return false; + + if (isForwardOnly()) { + bool rc; + do { + rc = fetchNext(); + } while (rc && currentRow() < i); + return rc; + } else { + err = MimerFetchScroll(d->statementhandle, MIMER_ABSOLUTE, i + 1); + if (err == MIMER_NO_DATA) + return false; + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(QCoreApplication::translate("QMimerSQLResult", "Fetch did not succeed"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + setAt(MimerCurrentRow(d->statementhandle) - 1); + return true; +} + +bool QMimerSQLResult::fetchFirst() +{ + Q_D(const QMimerSQLResult); + int32_t err = 0; + if (!isActive() || !isSelect()) + return false; + if (isForwardOnly()) { + if (currentRow() < 0) + return fetchNext(); + else if (currentRow() == 0) + setAt(0); + else + return false; + } else { + err = MimerFetchScroll(d->statementhandle, MIMER_FIRST, 0); + if (MIMER_SUCCEEDED(err) && err != MIMER_NO_DATA) + setAt(0); + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Fetch first did not succeed"), err, + QSqlError::StatementError, d->drv_d_func())); + return false; + } + if (err == MIMER_NO_DATA) + return false; + return true; +} + +bool QMimerSQLResult::fetchLast() +{ + Q_D(const QMimerSQLResult); + int32_t err = 0; + int row = 0; + if (!isActive() || !isSelect()) + return false; + if (isForwardOnly()) { + bool rc; + do { + rc = fetchNext(); + } while (rc); + + return currentRow() >= 0; + } else { + err = MimerFetchScroll(d->statementhandle, static_cast<std::int32_t>(MIMER_LAST), 0); + if (err == MIMER_NO_DATA) + return false; + if (MIMER_SUCCEEDED(err)) { + row = MimerCurrentRow(d->statementhandle) - 1; + } else { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult:", "Fetch last did not succeed"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + } + + if (row < 0) { + setAt(QSql::BeforeFirstRow); + return false; + } else { + setAt(row); + return true; + } +} + +bool QMimerSQLResult::fetchNext() +{ + Q_D(QMimerSQLResult); + int32_t err = 0; + if (!isActive() || !isSelect()) + return false; + if (isForwardOnly()) + err = MimerFetch(d->statementhandle); + else + err = MimerFetchScroll(d->statementhandle, MIMER_NEXT, 0); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not fetch next row"), err, + QSqlError::StatementError, d->drv_d_func())); + if (isForwardOnly()) + d->currentRow = QSql::BeforeFirstRow; + return false; + } + if (err == MIMER_NO_DATA) + return false; + if (isForwardOnly()) + setAt(++d->currentRow); + else + setAt(MimerCurrentRow(d->statementhandle) - 1); + return true; +} + +QVariant QMimerSQLResult::data(int i) +{ + Q_D(QMimerSQLResult); + int32_t err; + int32_t mType; + if (d->callWithOut) { + if (i >= MimerParameterCount(d->statementhandle)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult:", "Column %1 out of range") + .arg(i), + genericError, QSqlError::StatementError, nullptr)); + return QVariant(); + } + mType = MimerParameterType(d->statementhandle, static_cast<std::int16_t>(i + 1)); + } else { + if (i >= MimerColumnCount(d->statementhandle)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult:", "Column %1 out of range") + .arg(i), + genericError, QSqlError::StatementError, nullptr)); + return QVariant(); + } + mType = MimerColumnType(d->statementhandle, static_cast<std::int16_t>(i + 1)); + } + const QMetaType::Type type = qDecodeMSQLType(mType); + const MimerColumnTypes mimDataType = mimerMapColumnTypes(mType); + err = MimerIsNull(d->statementhandle, static_cast<std::int16_t>(i + 1)); + if (err > 0) { + return QVariant(QMetaType(type), nullptr); + } else { + switch (mimDataType) { + case MimerColumnTypes::Date: { + wchar_t dateString_w[maxDateStringSize + 1]; + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), dateString_w, + sizeof(dateString_w) / sizeof(dateString_w[0])); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("date", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return QDate::fromString(QString::fromWCharArray(dateString_w), "yyyy-MM-dd"_L1); + } + case MimerColumnTypes::Time: { + wchar_t timeString_w[maxTimeStringSize + 1]; + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), timeString_w, + sizeof(timeString_w) / sizeof(timeString_w[0])); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("time", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + QString timeString = QString::fromWCharArray(timeString_w); + QString timeFormatString = "HH:mm:ss"_L1; + if (timeString.size() > 8) { + timeFormatString.append(".zzz"_L1); + timeString = timeString.left(12); + } + return QTime::fromString(timeString, timeFormatString); + } + case MimerColumnTypes::Timestamp: { + wchar_t dateTimeString_w[maxTimestampStringSize + 1]; + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), + dateTimeString_w, + sizeof(dateTimeString_w) / sizeof(dateTimeString_w[0])); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("date time", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + QString dateTimeString = QString::fromWCharArray(dateTimeString_w); + QString dateTimeFormatString = "yyyy-MM-dd HH:mm:ss"_L1; + if (dateTimeString.size() > 19) { + dateTimeFormatString.append(".zzz"_L1); + dateTimeString = dateTimeString.left(23); + } + return QDateTime::fromString(dateTimeString, dateTimeFormatString); + } + case MimerColumnTypes::Int: { + int resInt; + err = MimerGetInt32(d->statementhandle, static_cast<std::int16_t>(i + 1), &resInt); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("int32", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return resInt; + } + case MimerColumnTypes::Long: { + int64_t resLongLong; + err = MimerGetInt64(d->statementhandle, static_cast<std::int16_t>(i + 1), &resLongLong); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("int64", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return (qlonglong)resLongLong; + } + case MimerColumnTypes::Boolean: { + err = MimerGetBoolean(d->statementhandle, static_cast<std::int16_t>(i + 1)); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("boolean", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return err == 1; + } + case MimerColumnTypes::Float: { + float resFloat; + err = MimerGetFloat(d->statementhandle, static_cast<std::int16_t>(i + 1), &resFloat); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("float", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return resFloat; + } + case MimerColumnTypes::Double: { + double resDouble; + err = MimerGetDouble(d->statementhandle, static_cast<std::int16_t>(i + 1), &resDouble); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("double", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + switch (numericalPrecisionPolicy()) { + case QSql::LowPrecisionInt32: + return static_cast<std::int32_t>(resDouble); + case QSql::LowPrecisionInt64: + return static_cast<qint64>(resDouble); + case QSql::LowPrecisionDouble: + return static_cast<qreal>(resDouble); + case QSql::HighPrecision: + return QString::number(resDouble, 'g', 17); + } + return QVariant(QMetaType(type), nullptr); + } + case MimerColumnTypes::Binary: { + QByteArray byteArray; + // Get size + err = MimerGetBinary(d->statementhandle, static_cast<std::int16_t>(i + 1), NULL, 0); + if (MIMER_SUCCEEDED(err)) { + byteArray.resize(err); + err = MimerGetBinary(d->statementhandle, static_cast<std::int16_t>(i + 1), + byteArray.data(), err); + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("binary", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return byteArray; + } + case MimerColumnTypes::Blob: { + QByteArray byteArray; + size_t size; + err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i + 1), &size, + &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + constexpr size_t maxSize = lobChunkMaxSizeFetch; + QVarLengthArray<char> blobchar(lobChunkMaxSizeFetch); + byteArray.reserve(size); + size_t left_to_return = size; + while (left_to_return > 0) { + const size_t bytesToReceive = + left_to_return <= maxSize ? left_to_return : maxSize; + err = MimerGetBlobData(&d->lobhandle, blobchar.data(), bytesToReceive); + byteArray.append(QByteArray::fromRawData(blobchar.data(), bytesToReceive)); + left_to_return -= bytesToReceive; + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("BLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + } + } else { + setLastError(qMakeError(msgCouldNotGet("BLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return byteArray; + } + case MimerColumnTypes::Numeric: + case MimerColumnTypes::String: { + wchar_t resString_w[maxStackStringSize + 1]; + // Get size + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), resString_w, + 0); + if (MIMER_SUCCEEDED(err)) { + int size = err; + if (err <= maxStackStringSize) { // For smaller strings, use a small buffer for + // efficiency + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), + resString_w, maxStackStringSize + 1); + if (MIMER_SUCCEEDED(err)) + return QString::fromWCharArray(resString_w); + } else { // For larger strings, dynamically allocate memory + QVarLengthArray<wchar_t> largeResString_w(size + 1); + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), + largeResString_w.data(), size + 1); + if (MIMER_SUCCEEDED(err)) + return QString::fromWCharArray(largeResString_w.data()); + } + } + setLastError(qMakeError(msgCouldNotGet( + mimDataType == MimerColumnTypes::Numeric ? "numeric" : "string", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + case MimerColumnTypes::Clob: { + size_t size; + err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i + 1), &size, + &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + constexpr size_t maxSize = lobChunkMaxSizeFetch; + QVarLengthArray<wchar_t> clobstring_w(lobChunkMaxSizeFetch + 1); + + size_t left_to_return = size; + QString returnString; + while (left_to_return > 0) { + const size_t bytesToReceive = + left_to_return <= maxSize ? left_to_return : maxSize; + err = MimerGetNclobData(&d->lobhandle, clobstring_w.data(), bytesToReceive + 1); + returnString.append(QString::fromWCharArray(clobstring_w.data())); + left_to_return -= bytesToReceive; + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + } + return returnString; + } + setLastError(qMakeError(msgCouldNotGet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + case MimerColumnTypes::Uuid: { + unsigned char uuidChar[16]; + err = MimerGetUUID(d->statementhandle, static_cast<std::int16_t>(i + 1), uuidChar); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("UUID", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + const QByteArray uuidByteArray = QByteArray(reinterpret_cast<char *>(uuidChar), 16); + return QUuid::fromRfc4122(uuidByteArray); + } + case MimerColumnTypes::Unknown: + default: + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Unknown data type %1").arg(i), + genericError, QSqlError::StatementError, nullptr)); + } + return QVariant(QMetaType(type), nullptr); + } +} + +bool QMimerSQLResult::isNull(int index) +{ + Q_D(const QMimerSQLResult); + const int32_t rc = MimerIsNull(d->statementhandle, static_cast<std::int16_t>(index + 1)); + if (!MIMER_SUCCEEDED(rc)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not check null, column %1") + .arg(index), + rc, QSqlError::StatementError, d->drv_d_func())); + return false; + } + return rc != 0; +} + +bool QMimerSQLResult::reset(const QString &query) +{ + if (!prepare(query)) + return false; + return exec(); +} + +int QMimerSQLResult::size() +{ + Q_D(QMimerSQLResult); + if (!isActive() || !isSelect() || isForwardOnly()) + return -1; + + if (d->currentSize != -1) + return d->currentSize; + + const int currentRow = MimerCurrentRow(d->statementhandle); + MimerFetchScroll(d->statementhandle, static_cast<std::int32_t>(MIMER_LAST), 0); + int size = MimerCurrentRow(d->statementhandle); + if (!MIMER_SUCCEEDED(size)) + size = -1; + MimerFetchScroll(d->statementhandle, MIMER_ABSOLUTE, currentRow); + d->currentSize = size; + return size; +} + +int QMimerSQLResult::numRowsAffected() +{ + Q_D(const QMimerSQLResult); + return d->rowsAffected; +} + +QSqlRecord QMimerSQLResult::record() const +{ + Q_D(const QMimerSQLResult); + QSqlRecord rec; + if (!isActive() || !isSelect() || !driver()) + return rec; + QSqlField field; + const int colSize = MimerColumnCount(d->statementhandle); + for (int i = 0; i < colSize; i++) { + wchar_t colName_w[100]; + MimerColumnName(d->statementhandle, static_cast<std::int16_t>(i + 1), colName_w, + sizeof(colName_w) / sizeof(colName_w[0])); + field.setName(QString::fromWCharArray(colName_w)); + const int32_t mType = MimerColumnType(d->statementhandle, static_cast<std::int16_t>(i + 1)); + const QMetaType::Type type = qDecodeMSQLType(mType); + field.setMetaType(QMetaType(type)); + field.setValue(QVariant(field.metaType())); + // field.setPrecision(); Should be implemented once the Mimer API can give this + // information. + // field.setLength(); Should be implemented once the Mimer API can give + // this information. + rec.insert(i, field); + } + return rec; +} + +bool QMimerSQLResult::prepare(const QString &query) +{ + Q_D(QMimerSQLResult); + int32_t err; + if (!driver()) + return false; + if (!d->preparedQuery) + return QSqlResult::prepare(query); + if (query.isEmpty()) + return false; + cleanup(); + const int option = isForwardOnly() ? MIMER_FORWARD_ONLY : MIMER_SCROLLABLE; + err = MimerBeginStatement8(d->drv_d_func()->sessionhandle, query.toUtf8().constData(), option, + &d->statementhandle); + if (err == MIMER_STATEMENT_CANNOT_BE_PREPARED) { + err = MimerExecuteStatement8(d->drv_d_func()->sessionhandle, query.toUtf8().constData()); + if (MIMER_SUCCEEDED(err)) { + d->executedStatement = true; + d->openCursor = false; + d->openStatement = false; + return true; + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(QCoreApplication::translate("QMimerSQLResult", + "Could not prepare/execute statement"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + d->openStatement = true; + return true; +} + +bool QMimerSQLResult::exec() +{ + Q_D(QMimerSQLResult); + int32_t err; + if (!driver()) + return false; + if (!d->preparedQuery) + return QSqlResult::exec(); + if (d->executedStatement) { + d->executedStatement = false; + return true; + } + if (d->openCursor) { + setAt(QSql::BeforeFirstRow); + err = MimerCloseCursor(d->statementhandle); + d->openCursor = false; + d->currentSize = -1; + } + QVector<QVariant> &values = boundValues(); + if (d->execBatch) + values = d->batch_vector; + int mimParamCount = MimerParameterCount(d->statementhandle); + if (!MIMER_SUCCEEDED(mimParamCount)) + mimParamCount = 0; + if (mimParamCount != values.size()) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Wrong number of parameters"), + genericError, QSqlError::StatementError, nullptr)); + return false; + } + for (int i = 0; i < mimParamCount; i++) { + if (bindValueType(i) == QSql::Out) { + d->callWithOut = true; + continue; + } + const QVariant &val = values.at(i); + if (QSqlResultPrivate::isVariantNull(val) || val.isNull() || val.toString().isNull()) { + err = MimerSetNull(d->statementhandle, i + 1); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("null", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + continue; + } + + const int mimParamType = MimerParameterType(d->statementhandle, i + 1); + const MimerColumnTypes mimDataType = mimerMapColumnTypes(mimParamType); + switch (mimDataType) { + case MimerColumnTypes::Int: { + bool convertOk; + err = MimerSetInt32(d->statementhandle, i + 1, val.toInt(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("int32", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Long: { + bool convertOk; + err = MimerSetInt64(d->statementhandle, i + 1, val.toLongLong(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("int64", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Float: { + bool convertOk; + err = MimerSetFloat(d->statementhandle, i + 1, val.toFloat(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("float", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Double: { + bool convertOk; + err = MimerSetDouble(d->statementhandle, i + 1, val.toDouble(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("double", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Binary: { + const QByteArray binArr = val.toByteArray(); + size_t size = static_cast<std::size_t>(binArr.size()); + err = MimerSetBinary(d->statementhandle, i + 1, binArr.data(), size); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("binary", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Boolean: { + err = MimerSetBoolean(d->statementhandle, i + 1, val.toBool() == true ? 1 : 0); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("boolean", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Uuid: { + const QByteArray uuidArray = + QByteArray::fromHex(val.toUuid().toString(QUuid::WithoutBraces).toLatin1()); + const unsigned char *uuid = + reinterpret_cast<const unsigned char *>(uuidArray.constData()); + err = MimerSetUUID(d->statementhandle, i + 1, uuid); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("UUID", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Numeric: + case MimerColumnTypes::String: { + QByteArray string_b = val.toString().trimmed().toUtf8(); + const char *string_u = string_b.constData(); + err = MimerSetString8(d->statementhandle, i + 1, string_u); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet( + mimDataType == MimerColumnTypes::Numeric ? "numeric" : "string", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Date: { + err = MimerSetString8(d->statementhandle, i + 1, val.toString().toUtf8().constData()); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("date", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Time: { + QString timeFormatString = "hh:mm:ss"_L1; + const QTime timeVal = val.toTime(); + if (timeVal.msec() > 0) + timeFormatString.append(".zzz"_L1); + err = MimerSetString8(d->statementhandle, i + 1, + timeVal.toString(timeFormatString).toUtf8().constData()); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("time", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Timestamp: { + QString dateTimeFormatString = "yyyy-MM-dd hh:mm:ss"_L1; + const QDateTime dateTimeVal = val.toDateTime(); + if (dateTimeVal.time().msec() > 0) + dateTimeFormatString.append(".zzz"_L1); + err = MimerSetString8( + d->statementhandle, i + 1, + val.toDateTime().toString(dateTimeFormatString).toUtf8().constData()); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotSet("datetime", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Blob: { + QByteArray blobArr = val.toByteArray(); + const char *blobData = blobArr.constData(); + qsizetype size = blobArr.size(); + err = MimerSetLob(d->statementhandle, i + 1, size, &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + qsizetype maxSize = lobChunkMaxSizeSet; + if (size > maxSize) { + qsizetype left_to_send = size; + for (qsizetype k = 0; left_to_send > 0; k++) { + if (left_to_send <= maxSize) { + err = MimerSetBlobData(&d->lobhandle, &blobData[k * maxSize], + left_to_send); + left_to_send = 0; + } else { + err = MimerSetBlobData(&d->lobhandle, &blobData[k * maxSize], maxSize); + left_to_send = left_to_send - maxSize; + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("BLOB byte array", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + } else { + err = MimerSetBlobData(&d->lobhandle, blobArr, size); + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotSet("BLOB byte array", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Clob: { + QByteArray string_b = val.toString().trimmed().toUtf8(); + const char *string_u = string_b.constData(); + size_t size_c = 1; + size_t size = 0; + while (string_u[size++]) + if ((string_u[size] & 0xc0) != 0x80) + size_c++; + err = MimerSetLob(d->statementhandle, i + 1, size_c, &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + constexpr size_t maxSize = lobChunkMaxSizeSet; + if (size > maxSize) { + size_t left_to_send = size; + size_t pos = 0; + uint step_back = 0; + while (left_to_send > 0 && step_back < maxSize) { + step_back = 0; + if (left_to_send <= maxSize) { + err = MimerSetNclobData8(&d->lobhandle, &string_u[pos], left_to_send); + left_to_send = 0; + } else { + // Check that we don't split a multi-byte utf-8 characters + while (pos + maxSize - step_back > 0 + && (string_u[pos + maxSize - step_back] & 0xc0) == 0x80) + step_back++; + err = MimerSetNclobData8(&d->lobhandle, &string_u[pos], + maxSize - step_back); + left_to_send = left_to_send - maxSize + step_back; + pos += maxSize - step_back; + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotSet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + } + } else { + err = MimerSetNclobData8(&d->lobhandle, string_u, size); + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Unknown: + default: + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Unknown datatype, parameter %1") + .arg(i), + genericError, QSqlError::StatementError, nullptr)); + return false; + } + } + if (d->execBatch) + return true; + err = MimerExecute(d->statementhandle); + if (MIMER_SUCCEEDED(err)) { + d->rowsAffected = err; + int k = 0; + for (qsizetype i = 0; i < values.size(); i++) { + if (bindValueType(i) == QSql::Out || bindValueType(i) == QSql::InOut) { + bindValue(i, data(k), QSql::In); + k++; + } + } + d->callWithOut = false; + } + setSelect(false); + if (MIMER_SEQUENCE_ERROR == err) { + err = MimerOpenCursor(d->statementhandle); + d->rowsAffected = err; + d->openCursor = true; + d->currentRow = QSql::BeforeFirstRow; + setSelect(true); + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(QCoreApplication::translate("QMimerSQLResult", + "Could not execute statement/open cursor"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + setActive(true); + return true; +} + +bool QMimerSQLResult::execBatch(bool arrayBind) +{ + Q_D(QMimerSQLResult); + Q_UNUSED(arrayBind); + int32_t err; + const QVector<QVariant> values = boundValues(); + + // Check that we only have input parameters. Currently + // we can only handle batch operations without output parameters. + for (qsizetype i = 0; i < values.first().toList().size(); i++) + if (bindValueType(i) == QSql::Out || bindValueType(i) == QSql::InOut) { + setLastError(qMakeError(QCoreApplication::translate( + "QMimerSQLResult", + "Only input parameters can be used in batch operations"), + genericError, QSqlError::StatementError, nullptr)); + d->execBatch = false; + return false; + } + d->execBatch = true; + for (qsizetype i = 0; i < values.first().toList().size(); i++) { + for (qsizetype j = 0; j < values.size(); j++) + d->batch_vector.append(values.at(j).toList().at(i)); + exec(); + if (i != (values.at(0).toList().size() - 1)) { + err = MimerAddBatch(d->statementhandle); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + //: %1 is the batch number + QCoreApplication::translate("QMimerSQLResult", "Could not add batch %1") + .arg(i), + err, QSqlError::StatementError, d->drv_d_func())); + d->execBatch = false; + return false; + } + } + d->batch_vector.clear(); + } + d->execBatch = false; + err = MimerExecute(d->statementhandle); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not execute batch"), err, + QSqlError::StatementError, d->drv_d_func())); + return false; + } + return true; +} + +QVariant QMimerSQLResult::lastInsertId() const +{ + Q_D(const QMimerSQLResult); + int64_t lastSequence; + const int32_t err = MimerGetSequenceInt64(d->statementhandle, &lastSequence); + if (!MIMER_SUCCEEDED(err)) + return QVariant(QMetaType(QMetaType::LongLong), nullptr); + return QVariant(qint64(lastSequence)); +} + +bool QMimerSQLDriver::hasFeature(DriverFeature f) const +{ + switch (f) { + case NamedPlaceholders: // Is true in reality but Qt parses Sql statement... + case EventNotifications: + case LowPrecisionNumbers: + case MultipleResultSets: + case SimpleLocking: + case CancelQuery: + return false; + case FinishQuery: + case LastInsertId: + case Transactions: + case QuerySize: + case BLOB: + case Unicode: + case PreparedQueries: + case PositionalPlaceholders: + case BatchOperations: + return true; + } + return true; +} + +bool QMimerSQLDriver::open(const QString &db, const QString &user, const QString &password, + const QString &host, int port, const QString &connOpts) +{ + Q_D(QMimerSQLDriver); + Q_UNUSED(host); + Q_UNUSED(port); + Q_UNUSED(connOpts); + if (isOpen()) + close(); + const int32_t err = MimerBeginSession8(db.toUtf8().constData(), user.toUtf8().constData(), + password.toUtf8().constData(), &d->sessionhandle); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not connect to database") + + " "_L1 + db, + err, QSqlError::ConnectionError, nullptr)); + setOpenError(true); + return false; + } + d->dbUser = user; + d->dbName = db; + setOpen(true); + setOpenError(false); + return true; +} + +void QMimerSQLDriver::close() +{ + Q_D(QMimerSQLDriver); + if (isOpen()) { + const int end_err = MimerEndSession(&d->sessionhandle); + if (MIMER_SUCCEEDED(end_err)) { + setOpen(false); + setOpenError(false); + } + } +} + +QSqlResult *QMimerSQLDriver::createResult() const +{ + return new QMimerSQLResult(this); +} + +QStringList QMimerSQLDriver::tables(QSql::TableType type) const +{ + QStringList tl; + if (!isOpen()) + return tl; + QSqlQuery t(createResult()); + QString sql; + switch (type) { + case QSql::Tables: { + sql = "select table_name from information_schema.tables where " + "table_type=\'BASE TABLE\' AND table_schema = CURRENT_USER"_L1; + break; + } + case QSql::SystemTables: { + sql = "select table_name from information_schema.tables where " + "table_type=\'BASE TABLE\' AND table_schema = \'SYSTEM\'"_L1; + break; + } + case QSql::Views: { + sql = "select table_name from information_schema.tables where " + "table_type=\'VIEW\' AND table_schema = CURRENT_USER"_L1; + break; + } + case QSql::AllTables: { + sql = "select table_name from information_schema.tables where " + "(table_type=\'VIEW\' or table_type=\'BASE TABLE\')" + " AND (table_schema = CURRENT_USER OR table_schema =\'SYSTEM\')"_L1; + break; + } + default: + break; + } + if (sql.length() > 0) { + t.exec(sql); + while (t.next()) + tl.append(t.value(0).toString()); + } + return tl; +} + +QSqlIndex QMimerSQLDriver::primaryIndex(const QString &tablename) const +{ + Q_D(const QMimerSQLDriver); + if (!isOpen()) + return QSqlIndex(); + QString table = tablename; + if (isIdentifierEscaped(table, QSqlDriver::TableName)) + table = stripDelimiters(table, QSqlDriver::TableName); + QSqlIndex index(tablename); + QSqlQuery t(createResult()); + QString schema; + QString qualifiedName = table; + d->splitTableQualifier(qualifiedName, &schema, &table); + QString sql = + "select information_schema.ext_access_paths.column_name," + "case when data_type = 'INTERVAL' then 'INTERVAL '|| interval_type " + "when data_type = 'INTEGER' and numeric_precision > 10 then 'BIGINT' " + "when data_type = 'INTEGER' and numeric_precision <= 10 AND NUMERIC_PRECISION > 5 " + "then 'INTEGER' when data_type = 'INTEGER' and numeric_precision <= 5 then 'SMALLINT' " + "else upper(data_type) end as data_type " + "from information_schema.ext_access_paths full outer join " + "information_schema.columns on information_schema.ext_access_paths.column_name = " + "information_schema.columns.column_name and " + "information_schema.ext_access_paths.table_name = " + "information_schema.columns.table_name where " + "information_schema.ext_access_paths.table_name = \'"_L1; + sql.append(table) + .append("\' and index_type = \'PRIMARY KEY\'"_L1); + if (schema.length() == 0) + sql.append(" and table_schema = CURRENT_USER"_L1); + else + sql.append(" and table_schema = \'"_L1).append(schema).append("\'"_L1); + + if (!t.exec(sql)) + return QSqlIndex(); + int i = 0; + while (t.next()) { + QSqlField field(t.value(0).toString(), + QMetaType(qDecodeMSQLType(qLookupMimDataType(t.value(1).toString()))), + tablename); + index.insert(i, field); + index.setName(t.value(0).toString()); + i++; + } + return index; +} + +QSqlRecord QMimerSQLDriver::record(const QString &tablename) const +{ + Q_D(const QMimerSQLDriver); + if (!isOpen()) + return QSqlRecord(); + QSqlRecord rec; + QSqlQuery t(createResult()); + QString qualifiedName = tablename; + if (isIdentifierEscaped(qualifiedName, QSqlDriver::TableName)) + qualifiedName = stripDelimiters(qualifiedName, QSqlDriver::TableName); + QString schema, table; + d->splitTableQualifier(qualifiedName, &schema, &table); + + QString sql = + "select column_name, case when data_type = 'INTERVAL' then 'INTERVAL '|| interval_type " + "when data_type = 'INTEGER' and numeric_precision > 10 then 'BIGINT' " + "when data_type = 'INTEGER' and numeric_precision <= 10 AND numeric_precision > 5 " + "then 'INTEGER' when data_type = 'INTEGER' and numeric_precision <= 5 then 'SMALLINT' " + "else UPPER(data_type) end as data_type, case when is_nullable = 'YES' then false else " + "true end as required, " + "coalesce(numeric_precision, coalesce(datetime_precision,coalesce(interval_precision, " + "-1))) as prec from information_schema.columns where table_name = \'"_L1; + if (schema.length() == 0) + sql.append(table).append("\' and table_schema = CURRENT_USER"_L1); + else + sql.append(table).append("\' and table_schema = \'"_L1).append(schema).append("\'"_L1); + sql.append(" order by ordinal_position"_L1); + if (!t.exec(sql)) + return QSqlRecord(); + + while (t.next()) { + QSqlField field(t.value(0).toString(), + QMetaType(qDecodeMSQLType(qLookupMimDataType(t.value(1).toString()))), + tablename); + field.setRequired(t.value(3).toBool()); + if (t.value(3).toInt() != -1) + field.setPrecision(t.value(3).toInt()); + rec.append(field); + } + + return rec; +} + +QVariant QMimerSQLDriver::handle() const +{ + Q_D(const QMimerSQLDriver); + return QVariant::fromValue(d->sessionhandle); +} + +QString QMimerSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const +{ + Q_UNUSED(type); + QString res = identifier; + if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"')) { + res.replace(u'"', "\"\""_L1); + res = u'"' + res + u'"'; + res.replace(u'.', "\".\""_L1); + } + return res; +} + +bool QMimerSQLDriver::beginTransaction() +{ + Q_D(const QMimerSQLDriver); + const int32_t err = MimerBeginTransaction(d->sessionhandle, MIMER_TRANS_READWRITE); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not start transaction"), err, + QSqlError::TransactionError, d)); + return false; + } + return true; +} + +bool QMimerSQLDriver::commitTransaction() +{ + Q_D(const QMimerSQLDriver); + const int32_t err = MimerEndTransaction(d->sessionhandle, MIMER_COMMIT); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not commit transaction"), err, + QSqlError::TransactionError, d)); + return false; + } + return true; +} + +bool QMimerSQLDriver::rollbackTransaction() +{ + Q_D(const QMimerSQLDriver); + const int32_t err = MimerEndTransaction(d->sessionhandle, MIMER_ROLLBACK); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not roll back transaction"), + err, QSqlError::TransactionError, d)); + return false; + } + return true; +} + +void QMimerSQLDriverPrivate::splitTableQualifier(const QString &qualifiedName, QString *schema, + QString *table) const +{ + const QList<QStringView> l = QStringView(qualifiedName).split(u'.'); + int n = l.count(); + if (n > 2) { + return; // can't possibly be a valid table qualifier + } else if (n == 1) { + *schema = QString(); + *table = l.at(0).toString(); + } else { + *schema = l.at(0).toString(); + *table = l.at(1).toString(); + } +} + +QT_END_NAMESPACE + +#include "moc_qsql_mimer.cpp" diff --git a/src/plugins/sqldrivers/mimer/qsql_mimer.h b/src/plugins/sqldrivers/mimer/qsql_mimer.h new file mode 100644 index 0000000000..03ad8f21f2 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/qsql_mimer.h @@ -0,0 +1,50 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Mimer Information Technology +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QSQL_MIMER_H +#define QSQL_MIMER_H + +#include <QtSql/qsqldriver.h> +#include <QUuid> +#include <mimerapi.h> + +#ifdef QT_PLUGIN +# define Q_EXPORT_SQLDRIVER_MIMER +#else +# define Q_EXPORT_SQLDRIVER_MIMER Q_SQL_EXPORT +#endif + +QT_BEGIN_NAMESPACE + +class QMimerSQLDriverPrivate; + +class Q_EXPORT_SQLDRIVER_MIMER QMimerSQLDriver : public QSqlDriver +{ + friend class QMimerSQLResultPrivate; + Q_DECLARE_PRIVATE(QMimerSQLDriver) + Q_OBJECT +public: + explicit QMimerSQLDriver(QObject *parent = nullptr); + explicit QMimerSQLDriver(MimerSession *conn, QObject *parent = nullptr); + ~QMimerSQLDriver() override; + bool hasFeature(DriverFeature f) const override; + bool open(const QString &db, const QString &user, const QString &password, const QString &host, + int port, const QString &connOpts) override; + void close() override; + QSqlResult *createResult() const override; + QStringList tables(QSql::TableType type) const override; + QSqlIndex primaryIndex(const QString &tablename) const override; + QSqlRecord record(const QString &tablename) const override; + QVariant handle() const override; + QString escapeIdentifier(const QString &identifier, IdentifierType type) const override; +protected: + bool beginTransaction() override; + bool commitTransaction() override; + bool rollbackTransaction() override; + +private: +}; + +QT_END_NAMESPACE + +#endif // QSQL_MIMER diff --git a/src/plugins/sqldrivers/mysql/CMakeLists.txt b/src/plugins/sqldrivers/mysql/CMakeLists.txt index a05fc513f1..2e3d028584 100644 --- a/src/plugins/sqldrivers/mysql/CMakeLists.txt +++ b/src/plugins/sqldrivers/mysql/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from mysql.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QMYSQLDriverPlugin Plugin: @@ -13,6 +14,7 @@ qt_internal_add_plugin(QMYSQLDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES MySQL::MySQL Qt::Core @@ -20,7 +22,4 @@ qt_internal_add_plugin(QMYSQLDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:mysql.pro:<TRUE>: -# OTHER_FILES = "mysql.json" - qt_internal_force_macos_intel_arch(QMYSQLDriverPlugin) diff --git a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp index 39e797d9b9..cfd4931b46 100644 --- a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp +++ b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp @@ -10,12 +10,14 @@ #include <qdebug.h> #include <qfile.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qsqlerror.h> #include <qsqlfield.h> #include <qsqlindex.h> #include <qsqlquery.h> #include <qsqlrecord.h> #include <qstringlist.h> +#include <qtimezone.h> #include <QtSql/private/qsqldriver_p.h> #include <QtSql/private/qsqlresult_p.h> @@ -29,12 +31,30 @@ Q_DECLARE_METATYPE(MYSQL_RES*) Q_DECLARE_METATYPE(MYSQL*) Q_DECLARE_METATYPE(MYSQL_STMT*) +// MYSQL_TYPE_JSON was introduced with MySQL 5.7.9 +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID < 50709 +#define MYSQL_TYPE_JSON 245 +#endif + // MySQL above version 8 removed my_bool typedef while MariaDB kept it, // by redefining it we can regain source compatibility. using my_bool = decltype(mysql_stmt_bind_result(nullptr, nullptr)); +// this is a copy of the old MYSQL_TIME before an additional integer was added in +// 8.0.27.0. This kills the sanity check during retrieving this struct from mysql +// when another libmysql version is used during runtime than during compile time +struct QT_MYSQL_TIME +{ + unsigned int year, month, day, hour, minute, second; + unsigned long second_part; /**< microseconds */ + my_bool neg; + enum enum_mysql_timestamp_type time_type; +}; + QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcMysql, "qt.sql.mysql") + using namespace Qt::StringLiterals; class QMYSQLDriverPrivate : public QSqlDriverPrivate @@ -45,6 +65,7 @@ public: QMYSQLDriverPrivate() : QSqlDriverPrivate(QSqlDriver::MySqlServer) {} MYSQL *mysql = nullptr; + QString dbName; bool preparedQuerysEnabled = false; }; @@ -80,9 +101,15 @@ static inline QVariant qDateTimeFromString(QString &val) #else if (val.isEmpty()) return QVariant(QDateTime()); - if (val.length() == 14) - // TIMESTAMPS have the format yyyyMMddhhmmss + + // TIMESTAMPS have either the format "yyyyMMddhhmmss" or "yyyy-MM-dd + // hh:mm:ss". QDateTime::fromString() can convert the latter, but not the + // former, so adapt it if necessary. + if (val.size() == 14) val.insert(4, u'-').insert(7, u'-').insert(10, u'T').insert(13, u':').insert(16, u':'); + + if (!val.endsWith(u'Z')) + val.append(u'Z'); // make UTC return QVariant(QDateTime::fromString(val, Qt::ISODate)); #endif } @@ -101,6 +128,18 @@ static inline bool checkPreparedQueries(MYSQL *mysql) return mysql_stmt_param_count(stmt.get()) == 2; } +// used with prepared queries and bound arguments +static inline void setUtcTimeZone(MYSQL *mysql) +{ + std::unique_ptr<MYSQL_STMT, decltype(&mysql_stmt_close)> stmt(mysql_stmt_init(mysql), &mysql_stmt_close); + if (!stmt) + return; + + static const char query[] = "SET time_zone = '+00:00'"; + if (mysql_stmt_prepare(stmt.get(), query, sizeof(query) - 1)) + mysql_stmt_execute(stmt.get()); +} + class QMYSQLResultPrivate; class QMYSQLResult : public QSqlResult @@ -171,68 +210,70 @@ public: bool preparedQuery = false; }; -static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, - const QMYSQLDriverPrivate* p) +static QSqlError qMakeError(const QString &err, QSqlError::ErrorType type, + const QMYSQLDriverPrivate *p) { - const char *cerr = p->mysql ? mysql_error(p->mysql) : 0; + const char *cerr = p->mysql ? mysql_error(p->mysql) : nullptr; return QSqlError("QMYSQL: "_L1 + err, QString::fromUtf8(cerr), type, QString::number(mysql_errno(p->mysql))); } -static QMetaType qDecodeMYSQLType(int mysqltype, uint flags) +static QMetaType qDecodeMYSQLType(enum_field_types mysqltype, uint flags) { QMetaType::Type type; switch (mysqltype) { - case FIELD_TYPE_TINY : + case MYSQL_TYPE_TINY: type = (flags & UNSIGNED_FLAG) ? QMetaType::UChar : QMetaType::Char; break; - case FIELD_TYPE_SHORT : + case MYSQL_TYPE_SHORT: type = (flags & UNSIGNED_FLAG) ? QMetaType::UShort : QMetaType::Short; break; - case FIELD_TYPE_LONG : - case FIELD_TYPE_INT24 : + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_INT24: type = (flags & UNSIGNED_FLAG) ? QMetaType::UInt : QMetaType::Int; break; - case FIELD_TYPE_YEAR : + case MYSQL_TYPE_YEAR: type = QMetaType::Int; break; - case FIELD_TYPE_LONGLONG : + case MYSQL_TYPE_BIT: + case MYSQL_TYPE_LONGLONG: type = (flags & UNSIGNED_FLAG) ? QMetaType::ULongLong : QMetaType::LongLong; break; - case FIELD_TYPE_FLOAT : - case FIELD_TYPE_DOUBLE : - case FIELD_TYPE_DECIMAL : -#if defined(FIELD_TYPE_NEWDECIMAL) - case FIELD_TYPE_NEWDECIMAL: -#endif + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_DOUBLE: + case MYSQL_TYPE_DECIMAL: + case MYSQL_TYPE_NEWDECIMAL: type = QMetaType::Double; break; - case FIELD_TYPE_DATE : + case MYSQL_TYPE_DATE: type = QMetaType::QDate; break; - case FIELD_TYPE_TIME : + case MYSQL_TYPE_TIME: // A time field can be within the range '-838:59:59' to '838:59:59' so // use QString instead of QTime since QTime is limited to 24 hour clock type = QMetaType::QString; break; - case FIELD_TYPE_DATETIME : - case FIELD_TYPE_TIMESTAMP : + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: type = QMetaType::QDateTime; break; - case FIELD_TYPE_STRING : - case FIELD_TYPE_VAR_STRING : - case FIELD_TYPE_BLOB : - case FIELD_TYPE_TINY_BLOB : - case FIELD_TYPE_MEDIUM_BLOB : - case FIELD_TYPE_LONG_BLOB : - case FIELD_TYPE_GEOMETRY : + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + case MYSQL_TYPE_GEOMETRY: + case MYSQL_TYPE_JSON: type = (flags & BINARY_FLAG) ? QMetaType::QByteArray : QMetaType::QString; break; - default: - case FIELD_TYPE_ENUM : - case FIELD_TYPE_SET : + case MYSQL_TYPE_ENUM: + case MYSQL_TYPE_SET: + type = QMetaType::QString; + break; + default: // needed because there are more enum values which are not available in all headers type = QMetaType::QString; break; } @@ -242,18 +283,17 @@ static QMetaType qDecodeMYSQLType(int mysqltype, uint flags) static QSqlField qToField(MYSQL_FIELD *field) { QSqlField f(QString::fromUtf8(field->name), - qDecodeMYSQLType(int(field->type), field->flags), + qDecodeMYSQLType(field->type, field->flags), QString::fromUtf8(field->table)); f.setRequired(IS_NOT_NULL(field->flags)); f.setLength(field->length); f.setPrecision(field->decimals); - f.setSqlType(field->type); f.setAutoValue(field->flags & AUTO_INCREMENT_FLAG); return f; } -static QSqlError qMakeStmtError(const QString& err, QSqlError::ErrorType type, - MYSQL_STMT* stmt) +static QSqlError qMakeStmtError(const QString &err, QSqlError::ErrorType type, + MYSQL_STMT *stmt) { const char *cerr = mysql_stmt_error(stmt); return QSqlError("QMYSQL: "_L1 + err, @@ -261,15 +301,16 @@ static QSqlError qMakeStmtError(const QString& err, QSqlError::ErrorType type, type, QString::number(mysql_stmt_errno(stmt))); } -static bool qIsBlob(int t) +static bool qIsBlob(enum_field_types t) { return t == MYSQL_TYPE_TINY_BLOB || t == MYSQL_TYPE_BLOB || t == MYSQL_TYPE_MEDIUM_BLOB - || t == MYSQL_TYPE_LONG_BLOB; + || t == MYSQL_TYPE_LONG_BLOB + || t == MYSQL_TYPE_JSON; } -static bool qIsTimeOrDate(int t) +static bool qIsTimeOrDate(enum_field_types t) { // *not* MYSQL_TYPE_TIME because its range is bigger than QTime // (see above) @@ -284,9 +325,14 @@ static bool qIsInteger(int t) || t == QMetaType::LongLong || t == QMetaType::ULongLong; } +static inline bool qIsBitfield(enum_field_types type) +{ + return type == MYSQL_TYPE_BIT; +} + void QMYSQLResultPrivate::bindBlobs() { - for (int i = 0; i < fields.count(); ++i) { + for (int i = 0; i < fields.size(); ++i) { const MYSQL_FIELD *fieldInfo = fields.at(i).myField; if (qIsBlob(inBinds[i].buffer_type) && meta && fieldInfo) { MYSQL_BIND *bind = &inBinds[i]; @@ -328,7 +374,7 @@ bool QMYSQLResultPrivate::bindInValues() bind->buffer_length = f.bufLength = 0; hasBlobs = true; } else if (qIsTimeOrDate(fieldInfo->type)) { - bind->buffer_length = f.bufLength = sizeof(MYSQL_TIME); + bind->buffer_length = f.bufLength = sizeof(QT_MYSQL_TIME); } else if (qIsInteger(f.type.id())) { bind->buffer_length = f.bufLength = 8; } else { @@ -382,7 +428,7 @@ void QMYSQLResult::cleanup() if (d->stmt) { if (mysql_stmt_close(d->stmt)) - qWarning("QMYSQLResult::cleanup: unable to free statement handle"); + qCWarning(lcMysql, "QMYSQLResult::cleanup: unable to free statement handle"); d->stmt = 0; } @@ -391,9 +437,8 @@ void QMYSQLResult::cleanup() d->meta = 0; } - int i; - for (i = 0; i < d->fields.count(); ++i) - delete[] d->fields[i].outField; + for (const QMYSQLResultPrivate::QMyField &f : std::as_const(d->fields)) + delete[] f.outField; if (d->outBinds) { delete[] d->outBinds; @@ -483,12 +528,7 @@ bool QMYSQLResult::fetchLast() return success; } - my_ulonglong numRows; - if (d->preparedQuery) { - numRows = mysql_stmt_num_rows(d->stmt); - } else { - numRows = mysql_num_rows(d->result); - } + my_ulonglong numRows = d->preparedQuery ? mysql_stmt_num_rows(d->stmt) : mysql_num_rows(d->result); if (at() == int(numRows)) return true; if (!numRows) @@ -506,11 +546,25 @@ bool QMYSQLResult::fetchFirst() return fetch(0); } +static inline uint64_t +qDecodeBitfield(const QMYSQLResultPrivate::QMyField &f, const char *outField) +{ + // byte-aligned length + const auto numBytes = (f.myField->length + 7) / 8; + uint64_t val = 0; + for (unsigned long i = 0; i < numBytes && outField; ++i) { + uint64_t tmp = static_cast<uint8_t>(outField[i]); + val <<= 8; + val |= tmp; + } + return val; +} + QVariant QMYSQLResult::data(int field) { Q_D(QMYSQLResult); - if (!isSelect() || field >= d->fields.count()) { - qWarning("QMYSQLResult::data: column %d out of range", field); + if (!isSelect() || field >= d->fields.size()) { + qCWarning(lcMysql, "QMYSQLResult::data: column %d out of range", field); return QVariant(); } @@ -523,8 +577,9 @@ QVariant QMYSQLResult::data(int field) if (d->preparedQuery) { if (f.nullIndicator) return QVariant(f.type); - - if (qIsInteger(f.type.id())) { + if (qIsBitfield(f.myField->type)) { + return QVariant::fromValue(qDecodeBitfield(f, f.outField)); + } else if (qIsInteger(f.type.id())) { QVariant variant(f.type, f.outField); // we never want to return char variants here, see QTBUG-53397 if (f.type.id() == QMetaType::UChar) @@ -532,8 +587,8 @@ QVariant QMYSQLResult::data(int field) else if (f.type.id() == QMetaType::Char) return variant.toInt(); return variant; - } else if (qIsTimeOrDate(f.myField->type) && f.bufLength == sizeof(MYSQL_TIME)) { - auto t = reinterpret_cast<const MYSQL_TIME *>(f.outField); + } else if (qIsTimeOrDate(f.myField->type) && f.bufLength >= sizeof(QT_MYSQL_TIME)) { + auto t = reinterpret_cast<const QT_MYSQL_TIME *>(f.outField); QDate date; QTime time; if (f.type.id() != QMetaType::QTime) @@ -541,7 +596,7 @@ QVariant QMYSQLResult::data(int field) if (f.type.id() != QMetaType::QDate) time = QTime(t->hour, t->minute, t->second, t->second_part / 1000); if (f.type.id() == QMetaType::QDateTime) - return QDateTime(date, time); + return QDateTime(date, time, QTimeZone::UTC); else if (f.type.id() == QMetaType::QDate) return date; else @@ -556,6 +611,9 @@ QVariant QMYSQLResult::data(int field) return QVariant(f.type); } + if (qIsBitfield(f.myField->type)) + return QVariant::fromValue(qDecodeBitfield(f, d->row[field])); + fieldLength = mysql_fetch_lengths(d->result)[field]; if (f.type.id() != QMetaType::QByteArray) @@ -625,7 +683,7 @@ QVariant QMYSQLResult::data(int field) bool QMYSQLResult::isNull(int field) { Q_D(const QMYSQLResult); - if (field < 0 || field >= d->fields.count()) + if (field < 0 || field >= d->fields.size()) return true; if (d->preparedQuery) return d->fields.at(field).nullIndicator; @@ -644,7 +702,7 @@ bool QMYSQLResult::reset (const QString& query) cleanup(); const QByteArray encQuery = query.toUtf8(); - if (mysql_real_query(d->drv_d_func()->mysql, encQuery.data(), encQuery.length())) { + if (mysql_real_query(d->drv_d_func()->mysql, encQuery.data(), encQuery.size())) { setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to execute query"), QSqlError::StatementError, d->drv_d_func())); return false; @@ -664,6 +722,7 @@ bool QMYSQLResult::reset (const QString& query) for(int i = 0; i < numFields; i++) { MYSQL_FIELD* field = mysql_fetch_field_direct(d->result, i); d->fields[i].type = qDecodeMYSQLType(field->type, field->flags); + d->fields[i].myField = field; } setAt(QSql::BeforeFirstRow); } @@ -729,7 +788,7 @@ QSqlRecord QMYSQLResult::record() const if (!mysql_errno(d->drv_d_func()->mysql)) { mysql_field_seek(res, 0); MYSQL_FIELD* field = mysql_fetch_field(res); - while(field) { + while (field) { info.append(qToField(field)); field = mysql_fetch_field(res); } @@ -752,8 +811,8 @@ bool QMYSQLResult::nextResult() d->result = 0; setSelect(false); - for (int i = 0; i < d->fields.count(); ++i) - delete[] d->fields[i].outField; + for (const QMYSQLResultPrivate::QMyField &f : std::as_const(d->fields)) + delete[] f.outField; d->fields.clear(); int status = mysql_next_result(d->drv_d_func()->mysql); @@ -766,7 +825,7 @@ bool QMYSQLResult::nextResult() } d->result = mysql_store_result(d->drv_d_func()->mysql); - int numFields = mysql_field_count(d->drv_d_func()->mysql); + unsigned int numFields = mysql_field_count(d->drv_d_func()->mysql); if (!d->result && numFields > 0) { setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to store next result"), QSqlError::StatementError, d->drv_d_func())); @@ -778,9 +837,10 @@ bool QMYSQLResult::nextResult() d->rowsAffected = mysql_affected_rows(d->drv_d_func()->mysql); if (isSelect()) { - for (int i = 0; i < numFields; i++) { - MYSQL_FIELD* field = mysql_fetch_field_direct(d->result, i); + for (unsigned int i = 0; i < numFields; i++) { + MYSQL_FIELD *field = mysql_fetch_field_direct(d->result, i); d->fields[i].type = qDecodeMYSQLType(field->type, field->flags); + d->fields[i].myField = field; } } @@ -793,29 +853,6 @@ void QMYSQLResult::virtual_hook(int id, void *data) QSqlResult::virtual_hook(id, data); } -static MYSQL_TIME *toMySqlDate(QDate date, QTime time, int type) -{ - Q_ASSERT(type == QMetaType::QTime || type == QMetaType::QDate - || type == QMetaType::QDateTime); - - MYSQL_TIME *myTime = new MYSQL_TIME; - memset(myTime, 0, sizeof(MYSQL_TIME)); - - if (type == QMetaType::QTime || type == QMetaType::QDateTime) { - myTime->hour = time.hour(); - myTime->minute = time.minute(); - myTime->second = time.second(); - myTime->second_part = time.msec() * 1000; - } - if (type == QMetaType::QDate || type == QMetaType::QDateTime) { - myTime->year = date.year(); - myTime->month = date.month(); - myTime->day = date.day(); - } - - return myTime; -} - bool QMYSQLResult::prepare(const QString& query) { Q_D(QMYSQLResult); @@ -840,7 +877,7 @@ bool QMYSQLResult::prepare(const QString& query) } const QByteArray encQuery = query.toUtf8(); - r = mysql_stmt_prepare(d->stmt, encQuery.constData(), encQuery.length()); + r = mysql_stmt_prepare(d->stmt, encQuery.constData(), encQuery.size()); if (r != 0) { setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult", "Unable to prepare statement"), QSqlError::StatementError, d->stmt)); @@ -848,9 +885,9 @@ bool QMYSQLResult::prepare(const QString& query) return false; } - if (mysql_stmt_param_count(d->stmt) > 0) {// allocate memory for outvalues - d->outBinds = new MYSQL_BIND[mysql_stmt_param_count(d->stmt)]; - } + const auto paramCount = mysql_stmt_param_count(d->stmt); + if (paramCount > 0) // allocate memory for outvalues + d->outBinds = new MYSQL_BIND[paramCount](); setSelect(d->bindInValues()); d->preparedQuery = true; @@ -868,7 +905,7 @@ bool QMYSQLResult::exec() return false; int r = 0; - QList<MYSQL_TIME *> timeVector; + QList<QT_MYSQL_TIME *> timeVector; QList<QByteArray> stringVector; QList<my_bool> nullVector; @@ -881,11 +918,10 @@ bool QMYSQLResult::exec() return false; } - if (mysql_stmt_param_count(d->stmt) > 0 && - mysql_stmt_param_count(d->stmt) == (uint)values.count()) { - - nullVector.resize(values.count()); - for (int i = 0; i < values.count(); ++i) { + const unsigned long paramCount = mysql_stmt_param_count(d->stmt); + if (paramCount > 0 && paramCount == static_cast<size_t>(values.size())) { + nullVector.resize(values.size()); + for (qsizetype i = 0; i < values.size(); ++i) { const QVariant &val = boundValues().at(i); void *data = const_cast<void *>(val.constData()); @@ -906,27 +942,41 @@ bool QMYSQLResult::exec() case QMetaType::QTime: case QMetaType::QDate: case QMetaType::QDateTime: { - MYSQL_TIME *myTime = toMySqlDate(val.toDate(), val.toTime(), val.userType()); + auto myTime = new QT_MYSQL_TIME{}; timeVector.append(myTime); - currBind->buffer = myTime; - switch (val.userType()) { - case QMetaType::QTime: + + QDate date; + QTime time; + int type = val.userType(); + if (type == QMetaType::QTime) { + time = val.toTime(); currBind->buffer_type = MYSQL_TYPE_TIME; myTime->time_type = MYSQL_TIMESTAMP_TIME; - break; - case QMetaType::QDate: + } else if (type == QMetaType::QDate) { + date = val.toDate(); currBind->buffer_type = MYSQL_TYPE_DATE; myTime->time_type = MYSQL_TIMESTAMP_DATE; - break; - case QMetaType::QDateTime: + } else { + QDateTime dt = val.toDateTime().toUTC(); + date = dt.date(); + time = dt.time(); currBind->buffer_type = MYSQL_TYPE_DATETIME; myTime->time_type = MYSQL_TIMESTAMP_DATETIME; - break; - default: - break; } - currBind->buffer_length = sizeof(MYSQL_TIME); + + if (type == QMetaType::QTime || type == QMetaType::QDateTime) { + myTime->hour = time.hour(); + myTime->minute = time.minute(); + myTime->second = time.second(); + myTime->second_part = time.msec() * 1000; + } + if (type == QMetaType::QDate || type == QMetaType::QDateTime) { + myTime->year = date.year(); + myTime->month = date.month(); + myTime->day = date.day(); + } + currBind->buffer_length = sizeof(QT_MYSQL_TIME); currBind->length = 0; break; } case QMetaType::UInt: @@ -960,12 +1010,16 @@ bool QMYSQLResult::exec() stringVector.append(ba); currBind->buffer_type = MYSQL_TYPE_STRING; currBind->buffer = const_cast<char *>(ba.constData()); - currBind->buffer_length = ba.length(); + currBind->buffer_length = ba.size(); break; } } } +#if defined(MARIADB_VERSION_ID) || MYSQL_VERSION_ID < 80300 r = mysql_stmt_bind_param(d->stmt, d->outBinds); +#else + r = mysql_stmt_bind_named_param(d->stmt, d->outBinds, paramCount, nullptr); +#endif if (r != 0) { setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult", "Unable to bind value"), QSqlError::StatementError, d->stmt)); @@ -1036,7 +1090,7 @@ static void qLibraryInit() return; if (mysql_library_init(0, 0, 0)) { - qWarning("QMYSQLDriver::qServerInit: unable to start server."); + qCWarning(lcMysql, "QMYSQLDriver::qServerInit: unable to start server."); } #endif // Q_NO_MYSQL_EMBEDDED @@ -1128,7 +1182,7 @@ bool QMYSQLDriver::hasFeature(DriverFeature f) const return false; } -static void setOptionFlag(uint &optionFlags, const QString &opt) +static void setOptionFlag(uint &optionFlags, QStringView opt) { if (opt == "CLIENT_COMPRESS"_L1) optionFlags |= CLIENT_COMPRESS; @@ -1143,85 +1197,167 @@ static void setOptionFlag(uint &optionFlags, const QString &opt) else if (opt == "CLIENT_ODBC"_L1) optionFlags |= CLIENT_ODBC; else if (opt == "CLIENT_SSL"_L1) - qWarning("QMYSQLDriver: SSL_KEY, SSL_CERT and SSL_CA should be used instead of CLIENT_SSL."); + qCWarning(lcMysql, "QMYSQLDriver: MYSQL_OPT_SSL_KEY, MYSQL_OPT_SSL_CERT " + "and MYSQL_OPT_SSL_CA should be used instead of CLIENT_SSL."); else - qWarning("QMYSQLDriver::open: Unknown connect option '%s'", opt.toLocal8Bit().constData()); + qCWarning(lcMysql, "QMYSQLDriver::open: Unknown connect option '%ls'", + qUtf16Printable(QString(opt))); +} + +static bool setOptionString(MYSQL *mysql, mysql_option option, QStringView v) +{ + return mysql_options(mysql, option, v.toUtf8().constData()) == 0; } -bool QMYSQLDriver::open(const QString& db, - const QString& user, - const QString& password, - const QString& host, - int port, - const QString& connOpts) +static bool setOptionInt(MYSQL *mysql, mysql_option option, QStringView v) +{ + bool bOk; + const auto val = v.toInt(&bOk); + return bOk ? mysql_options(mysql, option, &val) == 0 : false; +} + +static bool setOptionBool(MYSQL *mysql, mysql_option option, QStringView v) +{ + bool val = (v.isEmpty() || v == "TRUE"_L1 || v == "1"_L1); + return mysql_options(mysql, option, &val) == 0; +} + +// MYSQL_OPT_SSL_MODE was introduced with MySQL 5.7.11 +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50711 && !defined(MARIADB_VERSION_ID) +static bool setOptionSslMode(MYSQL *mysql, mysql_option option, QStringView v) +{ + mysql_ssl_mode sslMode = SSL_MODE_DISABLED; + if (v == "DISABLED"_L1 || v == "SSL_MODE_DISABLED"_L1) + sslMode = SSL_MODE_DISABLED; + else if (v == "PREFERRED"_L1 || v == "SSL_MODE_PREFERRED"_L1) + sslMode = SSL_MODE_PREFERRED; + else if (v == "REQUIRED"_L1 || v == "SSL_MODE_REQUIRED"_L1) + sslMode = SSL_MODE_REQUIRED; + else if (v == "VERIFY_CA"_L1 || v == "SSL_MODE_VERIFY_CA"_L1) + sslMode = SSL_MODE_VERIFY_CA; + else if (v == "VERIFY_IDENTITY"_L1 || v == "SSL_MODE_VERIFY_IDENTITY"_L1) + sslMode = SSL_MODE_VERIFY_IDENTITY; + else + qCWarning(lcMysql, "Unknown ssl mode '%ls' - using SSL_MODE_DISABLED", + qUtf16Printable(QString(v))); + return mysql_options(mysql, option, &sslMode) == 0; +} +#endif + +static bool setOptionProtocol(MYSQL *mysql, mysql_option option, QStringView v) +{ + mysql_protocol_type proto = MYSQL_PROTOCOL_DEFAULT; + if (v == "TCP"_L1 || v == "MYSQL_PROTOCOL_TCP"_L1) + proto = MYSQL_PROTOCOL_TCP; + else if (v == "SOCKET"_L1 || v == "MYSQL_PROTOCOL_SOCKET"_L1) + proto = MYSQL_PROTOCOL_SOCKET; + else if (v == "PIPE"_L1 || v == "MYSQL_PROTOCOL_PIPE"_L1) + proto = MYSQL_PROTOCOL_PIPE; + else if (v == "MEMORY"_L1 || v == "MYSQL_PROTOCOL_MEMORY"_L1) + proto = MYSQL_PROTOCOL_MEMORY; + else if (v == "DEFAULT"_L1 || v == "MYSQL_PROTOCOL_DEFAULT"_L1) + proto = MYSQL_PROTOCOL_DEFAULT; + else + qCWarning(lcMysql, "Unknown protocol '%ls' - using MYSQL_PROTOCOL_DEFAULT", + qUtf16Printable(QString(v))); + return mysql_options(mysql, option, &proto) == 0; +} + +bool QMYSQLDriver::open(const QString &db, + const QString &user, + const QString &password, + const QString &host, + int port, + const QString &connOpts) { Q_D(QMYSQLDriver); if (isOpen()) close(); + if (!(d->mysql = mysql_init(nullptr))) { + setLastError(qMakeError(tr("Unable to allocate a MYSQL object"), + QSqlError::ConnectionError, d)); + setOpenError(true); + return false; + } + + typedef bool (*SetOptionFunc)(MYSQL*, mysql_option, QStringView); + struct mysqloptions { + QLatin1StringView key; + mysql_option option; + SetOptionFunc func; + }; + const mysqloptions options[] = { + {"SSL_KEY"_L1, MYSQL_OPT_SSL_KEY, setOptionString}, + {"SSL_CERT"_L1, MYSQL_OPT_SSL_CERT, setOptionString}, + {"SSL_CA"_L1, MYSQL_OPT_SSL_CA, setOptionString}, + {"SSL_CAPATH"_L1, MYSQL_OPT_SSL_CAPATH, setOptionString}, + {"SSL_CIPHER"_L1, MYSQL_OPT_SSL_CIPHER, setOptionString}, + {"MYSQL_OPT_SSL_KEY"_L1, MYSQL_OPT_SSL_KEY, setOptionString}, + {"MYSQL_OPT_SSL_CERT"_L1, MYSQL_OPT_SSL_CERT, setOptionString}, + {"MYSQL_OPT_SSL_CA"_L1, MYSQL_OPT_SSL_CA, setOptionString}, + {"MYSQL_OPT_SSL_CAPATH"_L1, MYSQL_OPT_SSL_CAPATH, setOptionString}, + {"MYSQL_OPT_SSL_CIPHER"_L1, MYSQL_OPT_SSL_CIPHER, setOptionString}, + {"MYSQL_OPT_SSL_CRL"_L1, MYSQL_OPT_SSL_CRL, setOptionString}, + {"MYSQL_OPT_SSL_CRLPATH"_L1, MYSQL_OPT_SSL_CRLPATH, setOptionString}, +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50710 + {"MYSQL_OPT_TLS_VERSION"_L1, MYSQL_OPT_TLS_VERSION, setOptionString}, +#endif +#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50711 && !defined(MARIADB_VERSION_ID) + {"MYSQL_OPT_SSL_MODE"_L1, MYSQL_OPT_SSL_MODE, setOptionSslMode}, +#endif + {"MYSQL_OPT_CONNECT_TIMEOUT"_L1, MYSQL_OPT_CONNECT_TIMEOUT, setOptionInt}, + {"MYSQL_OPT_READ_TIMEOUT"_L1, MYSQL_OPT_READ_TIMEOUT, setOptionInt}, + {"MYSQL_OPT_WRITE_TIMEOUT"_L1, MYSQL_OPT_WRITE_TIMEOUT, setOptionInt}, + {"MYSQL_OPT_RECONNECT"_L1, MYSQL_OPT_RECONNECT, setOptionBool}, + {"MYSQL_OPT_LOCAL_INFILE"_L1, MYSQL_OPT_LOCAL_INFILE, setOptionInt}, + {"MYSQL_OPT_PROTOCOL"_L1, MYSQL_OPT_PROTOCOL, setOptionProtocol}, + {"MYSQL_SHARED_MEMORY_BASE_NAME"_L1, MYSQL_SHARED_MEMORY_BASE_NAME, setOptionString}, + }; + auto trySetOption = [&](const QStringView &key, const QStringView &value) -> bool { + for (const mysqloptions &opt : options) { + if (key == opt.key) { + if (!opt.func(d->mysql, opt.option, value)) { + qCWarning(lcMysql, "QMYSQLDriver::open: Could not set connect option value " + "'%ls' to '%ls'", + qUtf16Printable(QString(key)), qUtf16Printable(QString(value))); + } + return true; + } + } + return false; + }; + /* This is a hack to get MySQL's stored procedure support working. Since a stored procedure _may_ return multiple result sets, we have to enable CLIEN_MULTI_STATEMENTS here, otherwise _any_ stored procedure call will fail. */ unsigned int optionFlags = CLIENT_MULTI_STATEMENTS; - const QStringList opts(connOpts.split(u';', Qt::SkipEmptyParts)); + const QList<QStringView> opts(QStringView(connOpts).split(u';', Qt::SkipEmptyParts)); QString unixSocket; - QString sslCert; - QString sslCA; - QString sslKey; - QString sslCAPath; - QString sslCipher; - my_bool reconnect=false; - uint connectTimeout = 0; - uint readTimeout = 0; - uint writeTimeout = 0; // extract the real options from the string - for (int i = 0; i < opts.count(); ++i) { - QString tmp(opts.at(i).simplified()); + for (const auto &option : opts) { + const QStringView sv = QStringView(option).trimmed(); qsizetype idx; - if ((idx = tmp.indexOf(u'=')) != -1) { - QString val = tmp.mid(idx + 1).simplified(); - QString opt = tmp.left(idx).simplified(); - if (opt == "UNIX_SOCKET"_L1) - unixSocket = val; - else if (opt == "MYSQL_OPT_RECONNECT"_L1) { - if (val == "TRUE"_L1 || val == "1"_L1 || val.isEmpty()) - reconnect = true; - } else if (opt == "MYSQL_OPT_CONNECT_TIMEOUT"_L1) - connectTimeout = val.toInt(); - else if (opt == "MYSQL_OPT_READ_TIMEOUT"_L1) - readTimeout = val.toInt(); - else if (opt == "MYSQL_OPT_WRITE_TIMEOUT"_L1) - writeTimeout = val.toInt(); - else if (opt == "SSL_KEY"_L1) - sslKey = val; - else if (opt == "SSL_CERT"_L1) - sslCert = val; - else if (opt == "SSL_CA"_L1) - sslCA = val; - else if (opt == "SSL_CAPATH"_L1) - sslCAPath = val; - else if (opt == "SSL_CIPHER"_L1) - sslCipher = val; + if ((idx = sv.indexOf(u'=')) != -1) { + const QStringView key = sv.left(idx).trimmed(); + const QStringView val = sv.mid(idx + 1).trimmed(); + if (trySetOption(key, val)) + continue; + else if (key == "UNIX_SOCKET"_L1) + unixSocket = val.toString(); else if (val == "TRUE"_L1 || val == "1"_L1) - setOptionFlag(optionFlags, tmp.left(idx).simplified()); + setOptionFlag(optionFlags, key); else - qWarning("QMYSQLDriver::open: Illegal connect option value '%s'", - tmp.toLocal8Bit().constData()); + qCWarning(lcMysql, "QMYSQLDriver::open: Illegal connect option value '%ls'", + qUtf16Printable(QString(sv))); } else { - setOptionFlag(optionFlags, tmp); + setOptionFlag(optionFlags, sv); } } - if (!(d->mysql = mysql_init(nullptr))) { - setLastError(qMakeError(tr("Unable to allocate a MYSQL object"), - QSqlError::ConnectionError, d)); - setOpenError(true); - return false; - } - // try utf8 with non BMP first, utf8 (BMP only) if that fails static const char wanted_charsets[][8] = { "utf8mb4", "utf8" }; #ifdef MARIADB_VERSION_ID @@ -1240,23 +1376,6 @@ bool QMYSQLDriver::open(const QString& db, } *cs = nullptr; #endif - if (!sslKey.isNull() || !sslCert.isNull() || !sslCA.isNull() || - !sslCAPath.isNull() || !sslCipher.isNull()) { - mysql_ssl_set(d->mysql, - sslKey.isNull() ? nullptr : sslKey.toUtf8().constData(), - sslCert.isNull() ? nullptr : sslCert.toUtf8().constData(), - sslCA.isNull() ? nullptr : sslCA.toUtf8().constData(), - sslCAPath.isNull() ? nullptr : sslCAPath.toUtf8().constData(), - sslCipher.isNull() ? nullptr : sslCipher.toUtf8().constData()); - } - - if (connectTimeout != 0) - mysql_options(d->mysql, MYSQL_OPT_CONNECT_TIMEOUT, &connectTimeout); - if (readTimeout != 0) - mysql_options(d->mysql, MYSQL_OPT_READ_TIMEOUT, &readTimeout); - if (writeTimeout != 0) - mysql_options(d->mysql, MYSQL_OPT_WRITE_TIMEOUT, &writeTimeout); - MYSQL *mysql = mysql_real_connect(d->mysql, host.isNull() ? nullptr : host.toUtf8().constData(), user.isNull() ? nullptr : user.toUtf8().constData(), @@ -1285,9 +1404,10 @@ bool QMYSQLDriver::open(const QString& db, } } if (!ok) - qWarning("MySQL: Unable to set the client character set to utf8 (\"%s\"). Using '%s' instead.", - mysql_error(d->mysql), - mysql_character_set_name(d->mysql)); + qCWarning(lcMysql, "MySQL: Unable to set the client character set to utf8 (\"%s\"). " + "Using '%s' instead.", + mysql_error(d->mysql), + mysql_character_set_name(d->mysql)); } if (!db.isEmpty() && mysql_select_db(d->mysql, db.toUtf8().constData())) { @@ -1297,10 +1417,11 @@ bool QMYSQLDriver::open(const QString& db, return false; } - if (reconnect) - mysql_options(d->mysql, MYSQL_OPT_RECONNECT, &reconnect); - d->preparedQuerysEnabled = checkPreparedQueries(d->mysql); + d->dbName = db; + + if (d->preparedQuerysEnabled) + setUtcTimeZone(d->mysql); #if QT_CONFIG(thread) mysql_thread_init(); @@ -1320,6 +1441,7 @@ void QMYSQLDriver::close() #endif mysql_close(d->mysql); d->mysql = nullptr; + d->dbName.clear(); setOpen(false); setOpenError(false); } @@ -1336,14 +1458,14 @@ QStringList QMYSQLDriver::tables(QSql::TableType type) const QStringList tl; QSqlQuery q(createResult()); if (type & QSql::Tables) { - QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + QLatin1StringView(d->mysql->db) + "' and table_type = 'BASE TABLE'"_L1; + QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + d->dbName + "' and table_type = 'BASE TABLE'"_L1; q.exec(sql); while (q.next()) tl.append(q.value(0).toString()); } if (type & QSql::Views) { - QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + QLatin1StringView(d->mysql->db) + "' and table_type = 'VIEW'"_L1; + QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + d->dbName + "' and table_type = 'VIEW'"_L1; q.exec(sql); while (q.next()) @@ -1352,7 +1474,7 @@ QStringList QMYSQLDriver::tables(QSql::TableType type) const return tl; } -QSqlIndex QMYSQLDriver::primaryIndex(const QString& tablename) const +QSqlIndex QMYSQLDriver::primaryIndex(const QString &tablename) const { QSqlIndex idx; if (!isOpen()) @@ -1373,26 +1495,41 @@ QSqlIndex QMYSQLDriver::primaryIndex(const QString& tablename) const return idx; } -QSqlRecord QMYSQLDriver::record(const QString& tablename) const +QSqlRecord QMYSQLDriver::record(const QString &tablename) const { Q_D(const QMYSQLDriver); - QString table=tablename; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - - QSqlRecord info; if (!isOpen()) - return info; - MYSQL_RES* r = mysql_list_fields(d->mysql, table.toUtf8().constData(), 0); - if (!r) { - return info; + return {}; + QSqlQuery i(createResult()); + QString stmt("SELECT * FROM %1 LIMIT 0"_L1); + i.exec(stmt.arg(escapeIdentifier(tablename, QSqlDriver::TableName))); + auto r = i.record(); + if (r.isEmpty()) + return r; + // no binding of WHERE possible with MySQL + // escaping on WHERE clause does not work, so use mysql_real_escape_string() + stmt = "SELECT column_name, column_default FROM information_schema.columns WHERE table_name = '%1'"_L1; + const auto baTableName = tablename.toUtf8(); + QVarLengthArray<char> tableNameQuoted(baTableName.size() * 2 + 1); +#if defined(MARIADB_VERSION_ID) + const auto len = mysql_real_escape_string(d->mysql, tableNameQuoted.data(), + baTableName.data(), baTableName.size()); +#else + const auto len = mysql_real_escape_string_quote(d->mysql, tableNameQuoted.data(), + baTableName.data(), baTableName.size(), '\''); +#endif + if (i.exec(stmt.arg(QString::fromUtf8(tableNameQuoted.data(), len)))) { + while (i.next()) { + const auto colName = i.value(0).toString(); + const auto recordIdx = r.indexOf(colName); + if (recordIdx >= 0) { + auto field = r.field(recordIdx); + field.setDefaultValue(i.value(1)); + r.replace(recordIdx, field); + } + } } - MYSQL_FIELD* field; - - while ((field = mysql_fetch_field(r))) - info.append(qToField(field)); - mysql_free_result(r); - return info; + return r; } QVariant QMYSQLDriver::handle() const @@ -1405,7 +1542,7 @@ bool QMYSQLDriver::beginTransaction() { Q_D(QMYSQLDriver); if (!isOpen()) { - qWarning("QMYSQLDriver::beginTransaction: Database not open"); + qCWarning(lcMysql, "QMYSQLDriver::beginTransaction: Database not open"); return false; } if (mysql_query(d->mysql, "BEGIN WORK")) { @@ -1420,7 +1557,7 @@ bool QMYSQLDriver::commitTransaction() { Q_D(QMYSQLDriver); if (!isOpen()) { - qWarning("QMYSQLDriver::commitTransaction: Database not open"); + qCWarning(lcMysql, "QMYSQLDriver::commitTransaction: Database not open"); return false; } if (mysql_query(d->mysql, "COMMIT")) { @@ -1435,7 +1572,7 @@ bool QMYSQLDriver::rollbackTransaction() { Q_D(QMYSQLDriver); if (!isOpen()) { - qWarning("QMYSQLDriver::rollbackTransaction: Database not open"); + qCWarning(lcMysql, "QMYSQLDriver::rollbackTransaction: Database not open"); return false; } if (mysql_query(d->mysql, "ROLLBACK")) { @@ -1469,10 +1606,10 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons QVarLengthArray<char, 512> buffer(ba.size() * 2 + 1); auto escapedSize = mysql_real_escape_string(d->mysql, buffer.data(), ba.data(), ba.size()); r.reserve(escapedSize + 3); - r = u'\'' + QString::fromUtf8(buffer) + u'\''; + r = u'\'' + QString::fromUtf8(buffer.data(), escapedSize) + u'\''; break; } else { - qWarning("QMYSQLDriver::formatValue: Database not open"); + qCWarning(lcMysql, "QMYSQLDriver::formatValue: Database not open"); } Q_FALLTHROUGH(); case QMetaType::QDateTime: @@ -1481,7 +1618,6 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons // "+00:00" starting in version 8.0.19. However, if we got here, // it's because the MySQL server is too old for prepared queries // in the first place, so it won't understand timezones either. - // Besides, MYSQL_TIME does not support timezones, so match it. r = u'\'' + dt.date().toString(Qt::ISODate) + u'T' + @@ -1499,9 +1635,9 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons QString QMYSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) const { QString res = identifier; - if (!identifier.isEmpty() && !identifier.startsWith(u'`') && !identifier.endsWith(u'`') ) { - res.prepend(u'`').append(u'`'); + if (!identifier.isEmpty() && !identifier.startsWith(u'`') && !identifier.endsWith(u'`')) { res.replace(u'.', "`.`"_L1); + res = u'`' + res + u'`'; } return res; } diff --git a/src/plugins/sqldrivers/oci/CMakeLists.txt b/src/plugins/sqldrivers/oci/CMakeLists.txt index 15ab7f7f87..66c4219905 100644 --- a/src/plugins/sqldrivers/oci/CMakeLists.txt +++ b/src/plugins/sqldrivers/oci/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from oci.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QOCIDriverPlugin Plugin: @@ -20,9 +21,6 @@ qt_internal_add_plugin(QOCIDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:oci.pro:<TRUE>: -# OTHER_FILES = "oci.json" - ## Scopes: ##################################################################### diff --git a/src/plugins/sqldrivers/oci/main.cpp b/src/plugins/sqldrivers/oci/main.cpp index 933de8f8bf..6cc0062671 100644 --- a/src/plugins/sqldrivers/oci/main.cpp +++ b/src/plugins/sqldrivers/oci/main.cpp @@ -17,7 +17,7 @@ class QOCIDriverPlugin : public QSqlDriverPlugin public: QOCIDriverPlugin(); - QSqlDriver* create(const QString &); + QSqlDriver* create(const QString &) override; }; QOCIDriverPlugin::QOCIDriverPlugin() diff --git a/src/plugins/sqldrivers/oci/qsql_oci.cpp b/src/plugins/sqldrivers/oci/qsql_oci.cpp index 2b844218bf..68e303490d 100644 --- a/src/plugins/sqldrivers/oci/qsql_oci.cpp +++ b/src/plugins/sqldrivers/oci/qsql_oci.cpp @@ -7,6 +7,7 @@ #include <qdatetime.h> #include <qdebug.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qmetatype.h> #if QT_CONFIG(regularexpression) #include <qregularexpression.h> @@ -19,7 +20,9 @@ #include <QtSql/private/qsqlcachedresult_p.h> #include <QtSql/private/qsqldriver_p.h> #include <qstringlist.h> +#if QT_CONFIG(timezone) #include <qtimezone.h> +#endif #include <qvariant.h> #include <qvarlengtharray.h> @@ -28,14 +31,7 @@ #define _int64 __int64 #endif - #include <oci.h> -#ifdef max -#undef max -#endif -#ifdef min -#undef min -#endif #include <stdlib.h> @@ -56,6 +52,8 @@ Q_DECLARE_METATYPE(OCIStmt*) QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcOci, "qt.sql.oci") + using namespace Qt::StringLiterals; #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN @@ -138,8 +136,8 @@ QOCIDateTime::QOCIDateTime(OCIEnv *env, OCIError *err, const QDateTime &dt) if (dt.isValid()) { const QDate date = dt.date(); const QTime time = dt.time(); - // Zone in +hh:mm format (stripping UTC prefix from OffsetName) - QString timeZone = dt.timeZone().displayName(dt, QTimeZone::OffsetName).mid(3); + // Zone in +hh:mm format + const QString timeZone = dt.toString("ttt"_L1); const OraText *tz = reinterpret_cast<const OraText *>(timeZone.utf16()); OCIDateTimeConstruct(env, err, dateTime, date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.msec() * 1000000, @@ -168,7 +166,7 @@ QDateTime QOCIDateTime::fromOCIDateTime(OCIEnv *env, OCIError *err, OCIDateTime secondsOffset = -secondsOffset; // OCIDateTimeGetTime gives "fractions of second" as nanoseconds return QDateTime(QDate(year, month, day), QTime(hour, minute, second, nsec / 1000000), - Qt::OffsetFromUTC, secondsOffset); + QTimeZone::fromSecondsAheadOfUtc(secondsOffset)); } struct TempStorage { @@ -194,6 +192,7 @@ public: OCISession *authp = nullptr; OCITrans *trans = nullptr; OCIError *err = nullptr; + ub4 authMode = OCI_DEFAULT; bool transaction = false; int serverVersion = -1; int prefetchRows = -1; @@ -277,7 +276,7 @@ public: 0); #ifdef QOCI_DEBUG if (r != 0) - qWarning("QOCIResultPrivate::setCharset: Couldn't set OCI_ATTR_CHARSET_FORM."); + qCWarning(lcOci, "QOCIResultPrivate::setCharset: Couldn't set OCI_ATTR_CHARSET_FORM."); #endif #endif @@ -424,6 +423,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in break; } } // fall through for OUT values + Q_FALLTHROUGH(); default: { if (val.typeId() >= QMetaType::User) { if (val.canConvert<QOCIRowIdPointer>() && !isOutValue(pos)) { @@ -436,7 +436,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in -1, SQLT_RDD, indPtr, 0, 0, 0, 0, OCI_DEFAULT); } else { - qWarning("Unknown bind variable"); + qCWarning(lcOci, "Unknown bind variable"); r = OCI_ERROR; } } else { @@ -552,7 +552,7 @@ void QOCIDriverPrivate::allocErrorHandle() OCI_HTYPE_ERROR, 0, nullptr); if (r != OCI_SUCCESS) - qWarning("QOCIDriver: unable to allocate error handle"); + qCWarning(lcOci, "QOCIDriver: unable to allocate error handle"); } struct OraFieldInfo @@ -588,12 +588,7 @@ QString qOraWarn(OCIError *err, int *errorCode) void qOraWarning(const char* msg, OCIError *err) { -#ifdef QOCI_DEBUG - qWarning("%s %s", msg, qPrintable(qOraWarn(err))); -#else - Q_UNUSED(msg); - Q_UNUSED(err); -#endif + qCWarning(lcOci, "%s %ls", msg, qUtf16Printable(qOraWarn(err))); } static int qOraErrorNumber(OCIError *err) @@ -656,7 +651,7 @@ QMetaType qDecodeOCIType(const QString& ocitype, QSql::NumericalPrecisionPolicy else if (ocitype == "UNDEFINED"_L1) type = QMetaType::UnknownType; if (type == QMetaType::UnknownType) - qWarning("qDecodeOCIType: unknown type: %s", ocitype.toLocal8Bit().constData()); + qCWarning(lcOci, "qDecodeOCIType: unknown type: %ls", qUtf16Printable(ocitype)); return QMetaType(type); } @@ -724,7 +719,7 @@ QMetaType qDecodeOCIType(int ocitype, QSql::NumericalPrecisionPolicy precisionPo type = QMetaType::QDateTime; break; default: - qWarning("qDecodeOCIType: unknown OCI datatype: %d", ocitype); + qCWarning(lcOci, "qDecodeOCIType: unknown OCI datatype: %d", ocitype); break; } return QMetaType(type); @@ -741,7 +736,6 @@ static QSqlField qFromOraInf(const OraFieldInfo &ofi) f.setLength(ofi.oraPrecision == 0 ? 38 : int(ofi.oraPrecision)); f.setPrecision(ofi.oraScale); - f.setSqlType(int(ofi.oraType)); return f; } @@ -840,7 +834,7 @@ QOCICols::OraFieldInf::~OraFieldInf() if (lob) { int r = OCIDescriptorFree(lob, OCI_DTYPE_LOB); if (r != 0) - qWarning("QOCICols: Cannot free LOB descriptor"); + qCWarning(lcOci, "QOCICols: Cannot free LOB descriptor"); } if (dataPtr) { switch (typ.id()) { @@ -849,7 +843,7 @@ QOCICols::OraFieldInf::~OraFieldInf() case QMetaType::QDateTime: { int r = OCIDescriptorFree(dataPtr, OCI_DTYPE_TIMESTAMP_TZ); if (r != OCI_SUCCESS) - qWarning("QOCICols: Cannot free OCIDateTime descriptor"); + qCWarning(lcOci, "QOCICols: Cannot free OCIDateTime descriptor"); break; } default: @@ -904,7 +898,7 @@ QOCICols::QOCICols(int size, QOCIResultPrivate* dp) case QMetaType::QDateTime: r = OCIDescriptorAlloc(d->env, (void **)&fieldInf[idx].dataPtr, OCI_DTYPE_TIMESTAMP_TZ, 0, 0); if (r != OCI_SUCCESS) { - qWarning("QOCICols: Unable to allocate the OCIDateTime descriptor"); + qCWarning(lcOci, "QOCICols: Unable to allocate the OCIDateTime descriptor"); break; } r = OCIDefineByPos(d->sql, @@ -1075,7 +1069,7 @@ OCILobLocator **QOCICols::createLobLocator(int position, OCIEnv* env) 0, 0); if (r != 0) { - qWarning("QOCICols: Cannot create LOB locator"); + qCWarning(lcOci, "QOCICols: Cannot create LOB locator"); lob = 0; } return &lob; @@ -1313,7 +1307,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a return false; #ifdef QOCI_DEBUG - qDebug() << "columnCount:" << columnCount << boundValues; + qCDebug(lcOci) << "columnCount:" << columnCount << boundValues; #endif int i; @@ -1432,7 +1426,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a // we may now populate column with data for (uint row = 0; row < col.recordCount; ++row) { - const QVariant &val = boundValues.at(i).toList().at(row); + const QVariant val = boundValues.at(i).toList().at(row); if (QSqlResultPrivate::isVariantNull(val) && !d->isOutValue(i)) { columns[i].indicators[row] = -1; @@ -1510,13 +1504,13 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a QOCIBatchColumn &bindColumn = columns[i]; #ifdef QOCI_DEBUG - qDebug("OCIBindByPos(%p, %p, %p, %d, %p, %d, %d, %p, %p, 0, %d, %p, OCI_DEFAULT)", + qCDebug(lcOci, "OCIBindByPos(%p, %p, %p, %d, %p, %d, %d, %p, %p, 0, %d, %p, OCI_DEFAULT)", d->sql, &bindColumn.bindh, d->err, i + 1, bindColumn.data, bindColumn.maxLen, bindColumn.bindAs, bindColumn.indicators, bindColumn.lengths, arrayBind ? bindColumn.maxarr_len : 0, arrayBind ? &bindColumn.curelep : 0); for (int ii = 0; ii < (int)bindColumn.recordCount; ++ii) { - qDebug(" record %d: indicator %d, length %d", ii, bindColumn.indicators[ii], + qCDebug(lcOci, " record %d: indicator %d, length %d", ii, bindColumn.indicators[ii], bindColumn.lengths[ii]); } #endif @@ -1536,7 +1530,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a OCI_DEFAULT); #ifdef QOCI_DEBUG - qDebug("After OCIBindByPos: r = %d, bindh = %p", r, bindColumn.bindh); + qCDebug(lcOci, "After OCIBindByPos: r = %d, bindh = %p", r, bindColumn.bindh); #endif if (r != OCI_SUCCESS && r != OCI_SUCCESS_WITH_INFO) { @@ -1796,7 +1790,7 @@ void QOCICols::getValues(QVariantList &v, int index) v[index + i] = QVariant(QMetaType(QMetaType::QByteArray)); break; default: - qWarning("QOCICols::value: unknown data type"); + qCWarning(lcOci, "QOCICols::value: unknown data type"); break; } } @@ -1817,7 +1811,7 @@ QOCIResultPrivate::QOCIResultPrivate(QOCIResult *q, const QOCIDriver *drv) OCI_HTYPE_ERROR, 0, nullptr); if (r != OCI_SUCCESS) - qWarning("QOCIResult: unable to alloc error handle"); + qCWarning(lcOci, "QOCIResult: unable to alloc error handle"); } QOCIResultPrivate::~QOCIResultPrivate() @@ -1825,10 +1819,10 @@ QOCIResultPrivate::~QOCIResultPrivate() delete cols; if (sql && OCIHandleFree(sql, OCI_HTYPE_STMT) != OCI_SUCCESS) - qWarning("~QOCIResult: unable to free statement handle"); + qCWarning(lcOci, "~QOCIResult: unable to free statement handle"); if (OCIHandleFree(err, OCI_HTYPE_ERROR) != OCI_SUCCESS) - qWarning("~QOCIResult: unable to free error report handle"); + qCWarning(lcOci, "~QOCIResult: unable to free error report handle"); } @@ -1885,7 +1879,7 @@ bool QOCIResult::gotoNext(QSqlCachedResult::ValueCache &values, int index) break; case OCI_ERROR: if (qOraErrorNumber(d->err) == 1406) { - qWarning("QOCI Warning: data truncated for %s", lastQuery().toLocal8Bit().constData()); + qCWarning(lcOci, "QOCI Warning: data truncated for %ls", qUtf16Printable(lastQuery())); r = OCI_SUCCESS; /* ignore it */ break; } @@ -1999,7 +1993,7 @@ bool QOCIResult::exec() setLastError(qMakeError(QCoreApplication::translate("QOCIResult", "Unable to get statement type"), QSqlError::StatementError, d->err)); #ifdef QOCI_DEBUG - qDebug() << "lastQuery()" << lastQuery(); + qCDebug(lcOci) << "lastQuery()" << lastQuery(); #endif return false; } @@ -2014,7 +2008,7 @@ bool QOCIResult::exec() setLastError(qMakeError(QCoreApplication::translate("QOCIResult", "Unable to bind value"), QSqlError::StatementError, d->err)); #ifdef QOCI_DEBUG - qDebug() << "lastQuery()" << lastQuery(); + qCDebug(lcOci) << "lastQuery()" << lastQuery(); #endif return false; } @@ -2033,7 +2027,7 @@ bool QOCIResult::exec() setLastError(qMakeError(QCoreApplication::translate("QOCIResult", "Unable to execute statement"), QSqlError::StatementError, d->err)); #ifdef QOCI_DEBUG - qDebug() << "lastQuery()" << lastQuery(); + qCDebug(lcOci) << "lastQuery()" << lastQuery(); #endif return false; } @@ -2125,7 +2119,7 @@ QOCIDriver::QOCIDriver(QObject* parent) 0, NULL); if (r != 0) { - qWarning("QOCIDriver: unable to create environment"); + qCWarning(lcOci, "QOCIDriver: unable to create environment"); setLastError(qMakeError(tr("Unable to initialize", "QOCIDriver"), QSqlError::ConnectionError, d->err)); return; @@ -2156,10 +2150,10 @@ QOCIDriver::~QOCIDriver() close(); int r = OCIHandleFree(d->err, OCI_HTYPE_ERROR); if (r != OCI_SUCCESS) - qWarning("Unable to free Error handle: %d", r); + qCWarning(lcOci, "Unable to free Error handle: %d", r); r = OCIHandleFree(d->env, OCI_HTYPE_ENV); if (r != OCI_SUCCESS) - qWarning("Unable to free Environment handle: %d", r); + qCWarning(lcOci, "Unable to free Environment handle: %d", r); } bool QOCIDriver::hasFeature(DriverFeature f) const @@ -2190,17 +2184,16 @@ bool QOCIDriver::hasFeature(DriverFeature f) const static void qParseOpts(const QString &options, QOCIDriverPrivate *d) { - const QStringList opts(options.split(u';', Qt::SkipEmptyParts)); - for (int i = 0; i < opts.count(); ++i) { - const QString tmp(opts.at(i)); + const QVector<QStringView> opts(QStringView(options).split(u';', Qt::SkipEmptyParts)); + for (const auto tmp : opts) { qsizetype idx; if ((idx = tmp.indexOf(u'=')) == -1) { - qWarning("QOCIDriver::parseArgs: Invalid parameter: '%s'", - tmp.toLocal8Bit().constData()); + qCWarning(lcOci, "QOCIDriver::parseArgs: Invalid parameter: '%ls'", + qUtf16Printable(tmp.toString())); continue; } - const QString opt = tmp.left(idx); - const QString val = tmp.mid(idx + 1).simplified(); + const QStringView opt = tmp.left(idx); + const QStringView val = tmp.mid(idx + 1).trimmed(); bool ok; if (opt == "OCI_ATTR_PREFETCH_ROWS"_L1) { d->prefetchRows = val.toInt(&ok); @@ -2210,9 +2203,18 @@ static void qParseOpts(const QString &options, QOCIDriverPrivate *d) d->prefetchMem = val.toInt(&ok); if (!ok) d->prefetchMem = -1; + } else if (opt == "OCI_AUTH_MODE"_L1) { + if (val == "OCI_SYSDBA"_L1) { + d->authMode = OCI_SYSDBA; + } else if (val == "OCI_SYSOPER"_L1) { + d->authMode = OCI_SYSOPER; + } else if (val != "OCI_DEFAULT"_L1) { + qCWarning(lcOci, "QOCIDriver::parseArgs: Unsupported value for OCI_AUTH_MODE: '%ls'", + qUtf16Printable(val.toString())); + } } else { - qWarning ("QOCIDriver::parseArgs: Invalid parameter: '%s'", - opt.toLocal8Bit().constData()); + qCWarning(lcOci, "QOCIDriver::parseArgs: Invalid parameter: '%ls'", + qUtf16Printable(opt.toString())); } } } @@ -2276,9 +2278,9 @@ bool QOCIDriver::open(const QString & db, if (r == OCI_SUCCESS) { if (user.isEmpty() && password.isEmpty()) - r = OCISessionBegin(d->svc, d->err, d->authp, OCI_CRED_EXT, OCI_DEFAULT); + r = OCISessionBegin(d->svc, d->err, d->authp, OCI_CRED_EXT, d->authMode); else - r = OCISessionBegin(d->svc, d->err, d->authp, OCI_CRED_RDBMS, OCI_DEFAULT); + r = OCISessionBegin(d->svc, d->err, d->authp, OCI_CRED_RDBMS, d->authMode); } if (r == OCI_SUCCESS || r == OCI_SUCCESS_WITH_INFO) r = OCIAttrSet(d->svc, OCI_HTYPE_SVCCTX, d->authp, 0, OCI_ATTR_SESSION, d->err); @@ -2309,7 +2311,7 @@ bool QOCIDriver::open(const QString & db, sizeof(vertxt), OCI_HTYPE_SVCCTX); if (r != 0) { - qWarning("QOCIDriver::open: could not get Oracle server version."); + qCWarning(lcOci, "QOCIDriver::open: could not get Oracle server version."); } else { QString versionStr; versionStr = QString(reinterpret_cast<const QChar *>(vertxt)); @@ -2358,7 +2360,7 @@ bool QOCIDriver::beginTransaction() { Q_D(QOCIDriver); if (!isOpen()) { - qWarning("QOCIDriver::beginTransaction: Database not open"); + qCWarning(lcOci, "QOCIDriver::beginTransaction: Database not open"); return false; } int r = OCITransStart(d->svc, @@ -2379,7 +2381,7 @@ bool QOCIDriver::commitTransaction() { Q_D(QOCIDriver); if (!isOpen()) { - qWarning("QOCIDriver::commitTransaction: Database not open"); + qCWarning(lcOci, "QOCIDriver::commitTransaction: Database not open"); return false; } int r = OCITransCommit(d->svc, @@ -2399,7 +2401,7 @@ bool QOCIDriver::rollbackTransaction() { Q_D(QOCIDriver); if (!isOpen()) { - qWarning("QOCIDriver::rollbackTransaction: Database not open"); + qCWarning(lcOci, "QOCIDriver::rollbackTransaction: Database not open"); return false; } int r = OCITransRollback(d->svc, @@ -2432,7 +2434,7 @@ static QString make_where_clause(const QString &user, Expression e) "WMSYS", }; static const char joinC[][4] = { "or" , "and" }; - static constexpr QLatin1Char bang[] = { u' ', u'!' }; + static constexpr char16_t bang[] = { u' ', u'!' }; const QLatin1StringView join(joinC[e]); @@ -2551,8 +2553,7 @@ QSqlRecord QOCIDriver::record(const QString& tablename) const // eg. a sub-query on the sys.synonyms table QString stmt("select column_name, data_type, data_length, " "data_precision, data_scale, nullable, data_default%1" - "from all_tab_columns a " - "where a.table_name=%2"_L1); + "from all_tab_columns a "_L1); if (d->serverVersion >= 9) stmt = stmt.arg(", char_length "_L1); else @@ -2566,7 +2567,7 @@ QSqlRecord QOCIDriver::record(const QString& tablename) const else table = table.toUpper(); - tmpStmt = stmt.arg(u'\'' + table + u'\''); + tmpStmt = stmt + "where a.table_name='"_L1 + table + u'\''; if (owner.isEmpty()) { owner = d->user; } @@ -2680,7 +2681,7 @@ QSqlIndex QOCIDriver::primaryIndex(const QString& tablename) const QString QOCIDriver::formatValue(const QSqlField &field, bool trimStrings) const { - switch (field.typeID()) { + switch (field.metaType().id()) { case QMetaType::QDateTime: { QDateTime datetime = field.value().toDateTime(); QString datestring; @@ -2742,10 +2743,19 @@ QString QOCIDriver::escapeIdentifier(const QString &identifier, IdentifierType t QString res = identifier; if (!identifier.isEmpty() && !isIdentifierEscaped(identifier, type)) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } +int QOCIDriver::maximumIdentifierLength(IdentifierType type) const +{ + Q_D(const QOCIDriver); + Q_UNUSED(type); + return d->serverVersion > 12 ? 128 : 30; +} + QT_END_NAMESPACE + +#include "moc_qsql_oci_p.cpp" diff --git a/src/plugins/sqldrivers/oci/qsql_oci_p.h b/src/plugins/sqldrivers/oci/qsql_oci_p.h index fd173e21e8..91fef9de7b 100644 --- a/src/plugins/sqldrivers/oci/qsql_oci_p.h +++ b/src/plugins/sqldrivers/oci/qsql_oci_p.h @@ -42,7 +42,7 @@ public: explicit QOCIDriver(QObject *parent = nullptr); QOCIDriver(OCIEnv *env, OCISvcCtx *ctx, QObject *parent = nullptr); ~QOCIDriver(); - bool hasFeature(DriverFeature f) const; + bool hasFeature(DriverFeature f) const override; bool open(const QString &db, const QString &user, const QString &password, @@ -58,6 +58,7 @@ public: bool trimStrings) const override; QVariant handle() const override; QString escapeIdentifier(const QString &identifier, IdentifierType) const override; + int maximumIdentifierLength(IdentifierType type) const override; protected: bool beginTransaction() override; diff --git a/src/plugins/sqldrivers/odbc/CMakeLists.txt b/src/plugins/sqldrivers/odbc/CMakeLists.txt index c521f38160..7812aced2c 100644 --- a/src/plugins/sqldrivers/odbc/CMakeLists.txt +++ b/src/plugins/sqldrivers/odbc/CMakeLists.txt @@ -1,10 +1,11 @@ -# Generated from odbc.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QODBCDriverPlugin Plugin: ##################################################################### -qt_find_package(ODBC) # special case +qt_find_package(ODBC) qt_internal_add_plugin(QODBCDriverPlugin OUTPUT_NAME qsqlodbc @@ -15,6 +16,7 @@ qt_internal_add_plugin(QODBCDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES ODBC::ODBC Qt::Core @@ -22,9 +24,6 @@ qt_internal_add_plugin(QODBCDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:odbc.pro:<TRUE>: -# OTHER_FILES = "odbc.json" - ## Scopes: ##################################################################### diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index bfcff3ad47..976911d458 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -10,10 +10,12 @@ #include <qcoreapplication.h> #include <qdatetime.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qmath.h> #include <qsqlerror.h> #include <qsqlfield.h> #include <qsqlindex.h> +#include <qstringconverter.h> #include <qstringlist.h> #include <qvariant.h> #include <qvarlengtharray.h> @@ -21,61 +23,89 @@ #include <QSqlQuery> #include <QtSql/private/qsqldriver_p.h> #include <QtSql/private/qsqlresult_p.h> +#include "private/qtools_p.h" QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcOdbc, "qt.sql.odbc") + using namespace Qt::StringLiterals; +// non-standard ODBC SQL data type from SQL Server sometimes used instead of SQL_TIME +#ifndef SQL_SS_TIME2 +#define SQL_SS_TIME2 (-154) +#endif + // undefine this to prevent initial check of the ODBC driver #define ODBC_CHECK_DRIVER -static const int COLNAMESIZE = 256; -static const SQLSMALLINT TABLENAMESIZE = 128; +static constexpr int COLNAMESIZE = 256; +static constexpr SQLSMALLINT TABLENAMESIZE = 128; //Map Qt parameter types to ODBC types -static const SQLSMALLINT qParamType[4] = { SQL_PARAM_INPUT, SQL_PARAM_INPUT, SQL_PARAM_OUTPUT, SQL_PARAM_INPUT_OUTPUT }; +static constexpr SQLSMALLINT qParamType[4] = { SQL_PARAM_INPUT, SQL_PARAM_INPUT, SQL_PARAM_OUTPUT, SQL_PARAM_INPUT_OUTPUT }; -inline static QString fromSQLTCHAR(const QVarLengthArray<SQLTCHAR>& input, qsizetype size=-1) +class SqlStmtHandle { - QString result; +public: + SqlStmtHandle(SQLHANDLE hDbc) + { + SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &stmtHandle); + } + ~SqlStmtHandle() + { + if (stmtHandle != SQL_NULL_HSTMT) + SQLFreeHandle(SQL_HANDLE_STMT, stmtHandle); + } + SQLHANDLE handle() const + { + return stmtHandle; + } + bool isValid() const + { + return stmtHandle != SQL_NULL_HSTMT; + } + SQLHANDLE stmtHandle = SQL_NULL_HSTMT; +}; +template<typename C, int SIZE = sizeof(SQLTCHAR)> +inline static QString fromSQLTCHAR(const C &input, qsizetype size = -1) +{ // Remove any trailing \0 as some drivers misguidedly append one - int realsize = qMin(size, input.size()); - if (realsize > 0 && input[realsize-1] == 0) + qsizetype realsize = qMin(size, input.size()); + if (realsize > 0 && input[realsize - 1] == 0) realsize--; - switch(sizeof(SQLTCHAR)) { - case 1: - result=QString::fromUtf8((const char *)input.constData(), realsize); - break; - case 2: - result = QString::fromUtf16(reinterpret_cast<const char16_t *>(input.constData()), realsize); - break; - case 4: - result = QString::fromUcs4(reinterpret_cast<const char32_t *>(input.constData()), realsize); - break; - default: - qCritical("sizeof(SQLTCHAR) is %d. Don't know how to handle this.", int(sizeof(SQLTCHAR))); - } - return result; + if constexpr (SIZE == 1) + return QString::fromUtf8(reinterpret_cast<const char *>(input.constData()), realsize); + else if constexpr (SIZE == 2) + return QString::fromUtf16(reinterpret_cast<const char16_t *>(input.constData()), realsize); + else if constexpr (SIZE == 4) + return QString::fromUcs4(reinterpret_cast<const char32_t *>(input.constData()), realsize); + else + static_assert(QtPrivate::value_dependent_false<SIZE>(), + "Don't know how to handle sizeof(SQLTCHAR) != 1/2/4"); +} + +template<int SIZE = sizeof(SQLTCHAR)> +QStringConverter::Encoding encodingForSqlTChar() +{ + if constexpr (SIZE == 1) + return QStringConverter::Utf8; + else if constexpr (SIZE == 2) + return QStringConverter::Utf16; + else if constexpr (SIZE == 4) + return QStringConverter::Utf32; + else + static_assert(QtPrivate::value_dependent_false<SIZE>(), + "Don't know how to handle sizeof(SQLTCHAR) != 1/2/4"); } -inline static QVarLengthArray<SQLTCHAR> toSQLTCHAR(const QString &input) +inline static QVarLengthArray<SQLTCHAR> toSQLTCHAR(QStringView input) { QVarLengthArray<SQLTCHAR> result; - result.resize(input.size()); - switch(sizeof(SQLTCHAR)) { - case 1: - memcpy(result.data(), input.toUtf8().data(), input.size()); - break; - case 2: - memcpy(result.data(), input.unicode(), input.size() * 2); - break; - case 4: - memcpy(result.data(), input.toUcs4().data(), input.size() * 4); - break; - default: - qCritical("sizeof(SQLTCHAR) is %d. Don't know how to handle this.", int(sizeof(SQLTCHAR))); - } - result.append(0); // make sure it's null terminated, doesn't matter if it already is, it does if it isn't. + QStringEncoder enc(encodingForSqlTChar()); + result.resize(enc.requiredSpace(input.size())); + const auto end = enc.appendToBuffer(reinterpret_cast<char *>(result.data()), input); + result.resize((end - reinterpret_cast<char *>(result.data())) / sizeof(SQLTCHAR)); return result; } @@ -84,7 +114,7 @@ class QODBCDriverPrivate : public QSqlDriverPrivate Q_DECLARE_PUBLIC(QODBCDriver) public: - enum DefaultCase {Lower, Mixed, Upper, Sensitive}; + enum class DefaultCase {Lower, Mixed, Upper, Sensitive}; using QSqlDriverPrivate::QSqlDriverPrivate; SQLHANDLE hEnv = nullptr; @@ -105,15 +135,18 @@ public: void checkHasMultiResults(); void checkSchemaUsage(); void checkDateTimePrecision(); + void checkDefaultCase(); bool setConnectionOptions(const QString& connOpts); void splitTableQualifier(const QString &qualifier, QString &catalog, - QString &schema, QString &table); - DefaultCase defaultCase() const; + QString &schema, QString &table) const; QString adjustCase(const QString&) const; QChar quoteChar(); + SQLRETURN sqlFetchNext(const SqlStmtHandle &hStmt) const; + SQLRETURN sqlFetchNext(SQLHANDLE hStmt) const; private: bool isQuoteInitialized = false; QChar quote = u'"'; + DefaultCase m_defaultCase = DefaultCase::Mixed; }; class QODBCResultPrivate; @@ -193,118 +226,121 @@ void QODBCResultPrivate::updateStmtHandleState() disconnectCount = drv_d_func() ? drv_d_func()->disconnectCount : 0; } -static QString qWarnODBCHandle(int handleType, SQLHANDLE handle, int *nativeCode = nullptr) +struct DiagRecord +{ + QString description; + QString sqlState; + QString errorCode; +}; +static QList<DiagRecord> qWarnODBCHandle(int handleType, SQLHANDLE handle) { - SQLINTEGER nativeCode_ = 0; + SQLINTEGER nativeCode = 0; SQLSMALLINT msgLen = 0; + SQLSMALLINT i = 1; SQLRETURN r = SQL_NO_DATA; - SQLTCHAR state_[SQL_SQLSTATE_SIZE+1]; - QVarLengthArray<SQLTCHAR> description_(SQL_MAX_MESSAGE_LENGTH); - QString result; - int i = 1; + QVarLengthArray<SQLTCHAR, SQL_SQLSTATE_SIZE + 1> state(SQL_SQLSTATE_SIZE + 1); + QVarLengthArray<SQLTCHAR, SQL_MAX_MESSAGE_LENGTH + 1> description(SQL_MAX_MESSAGE_LENGTH + 1); + QList<DiagRecord> result; - description_[0] = 0; + if (!handle) + return result; do { r = SQLGetDiagRec(handleType, handle, i, - state_, - &nativeCode_, - 0, - 0, + state.data(), + &nativeCode, + description.data(), + description.size(), &msgLen); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && msgLen > 0) - description_.resize(msgLen+1); - r = SQLGetDiagRec(handleType, - handle, - i, - state_, - &nativeCode_, - description_.data(), - description_.size(), - &msgLen); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - if (nativeCode) - *nativeCode = nativeCode_; - const QString tmpstore = fromSQLTCHAR(description_, msgLen); - if (result != tmpstore) { - if (!result.isEmpty()) - result += u' '; - result += tmpstore; - } + if (msgLen >= description.size()) { + description.resize(msgLen + 1); // incl. \0 termination + continue; + } + if (SQL_SUCCEEDED(r)) { + result.push_back({fromSQLTCHAR(description, msgLen), + fromSQLTCHAR(state), + QString::number(nativeCode)}); } else if (r == SQL_ERROR || r == SQL_INVALID_HANDLE) { - return result; + break; } ++i; } while (r != SQL_NO_DATA); return result; } -static QString qODBCWarn(const SQLHANDLE hStmt, const SQLHANDLE envHandle = 0, - const SQLHANDLE pDbC = 0, int *nativeCode = nullptr) +static QList<DiagRecord> qODBCWarn(const SQLHANDLE hStmt, + const SQLHANDLE envHandle = nullptr, + const SQLHANDLE pDbC = nullptr) { - QString result; - if (envHandle) - result += qWarnODBCHandle(SQL_HANDLE_ENV, envHandle, nativeCode); - if (pDbC) { - const QString dMessage = qWarnODBCHandle(SQL_HANDLE_DBC, pDbC, nativeCode); - if (!dMessage.isEmpty()) { - if (!result.isEmpty()) - result += u' '; - result += dMessage; - } - } - if (hStmt) { - const QString hMessage = qWarnODBCHandle(SQL_HANDLE_STMT, hStmt, nativeCode); - if (!hMessage.isEmpty()) { - if (!result.isEmpty()) - result += u' '; - result += hMessage; - } - } + QList<DiagRecord> result; + result.append(qWarnODBCHandle(SQL_HANDLE_ENV, envHandle)); + result.append(qWarnODBCHandle(SQL_HANDLE_DBC, pDbC)); + result.append(qWarnODBCHandle(SQL_HANDLE_STMT, hStmt)); return result; } -static QString qODBCWarn(const QODBCResultPrivate* odbc, int *nativeCode = nullptr) +static QList<DiagRecord> qODBCWarn(const QODBCResultPrivate *odbc) { - return qODBCWarn(odbc->hStmt, odbc->dpEnv(), odbc->dpDbc(), nativeCode); + return qODBCWarn(odbc->hStmt, odbc->dpEnv(), odbc->dpDbc()); } -static QString qODBCWarn(const QODBCDriverPrivate* odbc, int *nativeCode = nullptr) +static QList<DiagRecord> qODBCWarn(const QODBCDriverPrivate *odbc) { - return qODBCWarn(0, odbc->hEnv, odbc->hDbc, nativeCode); + return qODBCWarn(nullptr, odbc->hEnv, odbc->hDbc); } -static void qSqlWarning(const QString& message, const QODBCResultPrivate* odbc) +static DiagRecord combineRecords(const QList<DiagRecord> &records) { - qWarning() << message << "\tError:" << qODBCWarn(odbc); + const auto add = [](const DiagRecord &a, const DiagRecord &b) { + return DiagRecord{a.description + u' ' + b.description, + a.sqlState + u';' + b.sqlState, + a.errorCode + u';' + b.errorCode}; + }; + if (records.isEmpty()) + return {}; + return std::accumulate(std::next(records.begin()), records.end(), records.front(), add); } -static void qSqlWarning(const QString &message, const QODBCDriverPrivate *odbc) +static QSqlError errorFromDiagRecords(const QString &err, + QSqlError::ErrorType type, + const QList<DiagRecord> &records) { - qWarning() << message << "\tError:" << qODBCWarn(odbc); + if (records.empty()) + return QSqlError("QODBC: unknown error"_L1, {}, type, {}); + const auto combined = combineRecords(records); + return QSqlError("QODBC: "_L1 + err, combined.description + ", "_L1 + combined.sqlState, type, + combined.errorCode); } -static void qSqlWarning(const QString &message, const SQLHANDLE hStmt) +static QString errorStringFromDiagRecords(const QList<DiagRecord>& records) { - qWarning() << message << "\tError:" << qODBCWarn(hStmt); + const auto combined = combineRecords(records); + return combined.description; } -static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, const QODBCResultPrivate* p) +template<class T> +static void qSqlWarning(const QString &message, T &&val) { - int nativeCode = -1; - QString message = qODBCWarn(p, &nativeCode); - return QSqlError("QODBC: "_L1 + err, message, type, - nativeCode != -1 ? QString::number(nativeCode) : QString()); + const auto addMsg = errorStringFromDiagRecords(qODBCWarn(val)); + if (addMsg.isEmpty()) + qCWarning(lcOdbc) << message; + else + qCWarning(lcOdbc) << message << "\tError:" << addMsg; +} + +static QSqlError qMakeError(const QString &err, + QSqlError::ErrorType type, + const QODBCResultPrivate *p) +{ + return errorFromDiagRecords(err, type, qODBCWarn(p)); } -static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, - const QODBCDriverPrivate* p) +static QSqlError qMakeError(const QString &err, + QSqlError::ErrorType type, + const QODBCDriverPrivate *p) { - int nativeCode = -1; - QString message = qODBCWarn(p, &nativeCode); - return QSqlError("QODBC: "_L1 + err, message, type, - nativeCode != -1 ? QString::number(nativeCode) : QString()); + return errorFromDiagRecords(err, type, qODBCWarn(p)); } static QMetaType qDecodeODBCType(SQLSMALLINT sqltype, bool isSigned = true) @@ -342,6 +378,7 @@ static QMetaType qDecodeODBCType(SQLSMALLINT sqltype, bool isSigned = true) case SQL_TYPE_DATE: type = QMetaType::QDate; break; + case SQL_SS_TIME2: case SQL_TIME: case SQL_TYPE_TIME: type = QMetaType::QTime; @@ -370,119 +407,69 @@ static QMetaType qDecodeODBCType(SQLSMALLINT sqltype, bool isSigned = true) return QMetaType(type); } -static QString qGetStringData(SQLHANDLE hStmt, int column, int colSize, bool unicode = false) +template <typename CT> +static QVariant getStringDataImpl(SQLHANDLE hStmt, SQLUSMALLINT column, qsizetype colSize, SQLSMALLINT targetType) { QString fieldVal; SQLRETURN r = SQL_ERROR; SQLLEN lengthIndicator = 0; - - // NB! colSize must be a multiple of 2 for unicode enabled DBs - if (colSize <= 0) { - colSize = 256; - } else if (colSize > 65536) { // limit buffer size to 64 KB - colSize = 65536; - } else { - colSize++; // make sure there is room for more than the 0 termination - } - if (unicode) { + QVarLengthArray<CT> buf(colSize); + while (true) { r = SQLGetData(hStmt, - column+1, - SQL_C_TCHAR, - NULL, - 0, - &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0) - colSize = int(lengthIndicator / sizeof(SQLTCHAR) + 1); - QVarLengthArray<SQLTCHAR> buf(colSize); - memset(buf.data(), 0, colSize*sizeof(SQLTCHAR)); - while (true) { - r = SQLGetData(hStmt, - column+1, - SQL_C_TCHAR, - (SQLPOINTER)buf.data(), - colSize*sizeof(SQLTCHAR), - &lengthIndicator); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - if (lengthIndicator == SQL_NULL_DATA) { - fieldVal.clear(); - break; - } - // starting with ODBC Native Client 2012, SQL_NO_TOTAL is returned - // instead of the length (which sometimes was wrong in older versions) - // see link for more info: http://msdn.microsoft.com/en-us/library/jj219209.aspx - // if length indicator equals SQL_NO_TOTAL, indicating that - // more data can be fetched, but size not known, collect data - // and fetch next block - if (lengthIndicator == SQL_NO_TOTAL) { - fieldVal += fromSQLTCHAR(buf, colSize); - continue; - } - // if SQL_SUCCESS_WITH_INFO is returned, indicating that - // more data can be fetched, the length indicator does NOT - // contain the number of bytes returned - it contains the - // total number of bytes that CAN be fetched - int rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : int(lengthIndicator / sizeof(SQLTCHAR)); - fieldVal += fromSQLTCHAR(buf, rSize); - if (lengthIndicator < SQLLEN(colSize*sizeof(SQLTCHAR))) { - // workaround for Drivermanagers that don't return SQL_NO_DATA - break; - } - } else if (r == SQL_NO_DATA) { - break; - } else { - qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')'; - fieldVal.clear(); - break; + column + 1, + targetType, + SQLPOINTER(buf.data()), SQLINTEGER(buf.size() * sizeof(CT)), + &lengthIndicator); + if (SQL_SUCCEEDED(r)) { + if (lengthIndicator == SQL_NULL_DATA) { + return {}; } - } - } else { - r = SQLGetData(hStmt, - column+1, - SQL_C_CHAR, - NULL, - 0, - &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0) - colSize = lengthIndicator + 1; - QVarLengthArray<SQLCHAR> buf(colSize); - while (true) { - r = SQLGetData(hStmt, - column+1, - SQL_C_CHAR, - (SQLPOINTER)buf.data(), - colSize, - &lengthIndicator); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - if (lengthIndicator == SQL_NULL_DATA || lengthIndicator == SQL_NO_TOTAL) { - fieldVal.clear(); - break; - } - // if SQL_SUCCESS_WITH_INFO is returned, indicating that - // more data can be fetched, the length indicator does NOT - // contain the number of bytes returned - it contains the - // total number of bytes that CAN be fetched - qsizetype rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : lengthIndicator; - // Remove any trailing \0 as some drivers misguidedly append one - int realsize = qMin(rSize, buf.size()); - if (realsize > 0 && buf[realsize - 1] == 0) - realsize--; - fieldVal += QString::fromUtf8(reinterpret_cast<const char *>(buf.constData()), realsize); - if (lengthIndicator < SQLLEN(colSize)) { - // workaround for Drivermanagers that don't return SQL_NO_DATA - break; - } - } else if (r == SQL_NO_DATA) { - break; - } else { - qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')'; - fieldVal.clear(); + // starting with ODBC Native Client 2012, SQL_NO_TOTAL is returned + // instead of the length (which sometimes was wrong in older versions) + // see link for more info: http://msdn.microsoft.com/en-us/library/jj219209.aspx + // if length indicator equals SQL_NO_TOTAL, indicating that + // more data can be fetched, but size not known, collect data + // and fetch next block + if (lengthIndicator == SQL_NO_TOTAL) { + fieldVal += fromSQLTCHAR<QVarLengthArray<CT>, sizeof(CT)>(buf, buf.size()); + continue; + } + // if SQL_SUCCESS_WITH_INFO is returned, indicating that + // more data can be fetched, the length indicator does NOT + // contain the number of bytes returned - it contains the + // total number of bytes that CAN be fetched + const qsizetype rSize = (r == SQL_SUCCESS_WITH_INFO) + ? buf.size() + : qsizetype(lengthIndicator / sizeof(CT)); + fieldVal += fromSQLTCHAR<QVarLengthArray<CT>, sizeof(CT)>(buf, rSize); + // lengthIndicator does not contain the termination character + if (lengthIndicator < SQLLEN((buf.size() - 1) * sizeof(CT))) { + // workaround for Drivermanagers that don't return SQL_NO_DATA break; } + } else if (r == SQL_NO_DATA) { + break; + } else { + qSqlWarning("QODBC::getStringData: Error while fetching data"_L1, hStmt); + return {}; } } return fieldVal; } +static QVariant qGetStringData(SQLHANDLE hStmt, SQLUSMALLINT column, int colSize, bool unicode) +{ + if (colSize <= 0) { + colSize = 256; // default Prealloc size of QVarLengthArray + } else if (colSize > 65536) { // limit buffer size to 64 KB + colSize = 65536; + } else { + colSize++; // make sure there is room for more than the 0 termination + } + return unicode ? getStringDataImpl<SQLTCHAR>(hStmt, column, colSize, SQL_C_TCHAR) + : getStringDataImpl<SQLCHAR>(hStmt, column, colSize, SQL_C_CHAR); +} + static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) { QByteArray fieldVal; @@ -494,19 +481,19 @@ static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) SQLLEN lengthIndicator = 0; SQLRETURN r = SQL_ERROR; - QVarLengthArray<SQLTCHAR> colName(COLNAMESIZE); + QVarLengthArray<SQLTCHAR, COLNAMESIZE> colName(COLNAMESIZE); r = SQLDescribeCol(hStmt, column + 1, - colName.data(), - COLNAMESIZE, + colName.data(), SQLSMALLINT(colName.size()), &colNameLen, &colType, &colSize, &colScale, &nullable); if (r != SQL_SUCCESS) - qWarning() << "qGetBinaryData: Unable to describe column" << column; + qSqlWarning(("QODBC::qGetBinaryData: Unable to describe column %1"_L1) + .arg(QString::number(column)), hStmt); // SQLDescribeCol may return 0 if size cannot be determined if (!colSize) colSize = 255; @@ -521,7 +508,7 @@ static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) const_cast<char *>(fieldVal.constData() + read), colSize, &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) + if (!SQL_SUCCEEDED(r)) break; if (lengthIndicator == SQL_NULL_DATA) return QVariant(QMetaType(QMetaType::QByteArray)); @@ -550,7 +537,7 @@ static QVariant qGetIntData(SQLHANDLE hStmt, int column, bool isSigned = true) (SQLPOINTER)&intbuf, sizeof(intbuf), &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) + if (!SQL_SUCCEEDED(r)) return QVariant(); if (lengthIndicator == SQL_NULL_DATA) return QVariant(QMetaType::fromType<int>()); @@ -570,7 +557,7 @@ static QVariant qGetDoubleData(SQLHANDLE hStmt, int column) (SQLPOINTER) &dblbuf, 0, &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { return QVariant(); } if (lengthIndicator == SQL_NULL_DATA) @@ -590,7 +577,7 @@ static QVariant qGetBigIntData(SQLHANDLE hStmt, int column, bool isSigned = true (SQLPOINTER) &lngbuf, sizeof(lngbuf), &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) + if (!SQL_SUCCEEDED(r)) return QVariant(); if (lengthIndicator == SQL_NULL_DATA) return QVariant(QMetaType::fromType<qlonglong>()); @@ -606,28 +593,25 @@ static bool isAutoValue(const SQLHANDLE hStmt, int column) SQLLEN nNumericAttribute = 0; // Check for auto-increment const SQLRETURN r = ::SQLColAttribute(hStmt, column + 1, SQL_DESC_AUTO_UNIQUE_VALUE, 0, 0, 0, &nNumericAttribute); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { - qSqlWarning(QStringLiteral("qMakeField: Unable to get autovalue attribute for column ") - + QString::number(column), hStmt); + if (!SQL_SUCCEEDED(r)) { + qSqlWarning(("QODBC::isAutoValue: Unable to get autovalue attribute for column %1"_L1) + .arg(QString::number(column)), hStmt); return false; } return nNumericAttribute != SQL_FALSE; } -static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMessage); - // creates a QSqlField from a valid hStmt generated // by SQLColumns. The hStmt has to point to a valid position. static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, const QODBCDriverPrivate* p) { - QString fname = qGetStringData(hStmt, 3, -1, p->unicode); + QString fname = qGetStringData(hStmt, 3, -1, p->unicode).toString(); int type = qGetIntData(hStmt, 4).toInt(); // column type QSqlField f(fname, qDecodeODBCType(type, p)); QVariant var = qGetIntData(hStmt, 6); f.setLength(var.isNull() ? -1 : var.toInt()); // column size var = qGetIntData(hStmt, 8).toInt(); f.setPrecision(var.isNull() ? -1 : var.toInt()); // precision - f.setSqlType(type); int required = qGetIntData(hStmt, 10).toInt(); // nullable-flag // required can be SQL_NO_NULLS, SQL_NULLABLE or SQL_NULLABLE_UNKNOWN if (required == SQL_NO_NULLS) @@ -638,16 +622,7 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, const QODBCDriverPrivate* return f; } -static QSqlField qMakeFieldInfo(const QODBCResultPrivate* p, int i ) -{ - QString errorMessage; - const QSqlField result = qMakeFieldInfo(p->hStmt, i, &errorMessage); - if (!errorMessage.isEmpty()) - qSqlWarning(errorMessage, p); - return result; -} - -static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMessage) +static QSqlField qMakeFieldInfo(const QODBCResultPrivate *p, int i) { SQLSMALLINT colNameLen; SQLSMALLINT colType; @@ -655,12 +630,10 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess SQLSMALLINT colScale; SQLSMALLINT nullable; SQLRETURN r = SQL_ERROR; - QVarLengthArray<SQLTCHAR> colName(COLNAMESIZE); - errorMessage->clear(); - r = SQLDescribeCol(hStmt, + QVarLengthArray<SQLTCHAR, COLNAMESIZE> colName(COLNAMESIZE); + r = SQLDescribeCol(p->hStmt, i+1, - colName.data(), - (SQLSMALLINT)COLNAMESIZE, + colName.data(), SQLSMALLINT(colName.size()), &colNameLen, &colType, &colSize, @@ -668,12 +641,13 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess &nullable); if (r != SQL_SUCCESS) { - *errorMessage = QStringLiteral("qMakeField: Unable to describe column ") + QString::number(i); + qSqlWarning(("QODBC::qMakeFieldInfo: Unable to describe column %1"_L1) + .arg(QString::number(i)), p); return QSqlField(); } SQLLEN unsignedFlag = SQL_FALSE; - r = SQLColAttribute (hStmt, + r = SQLColAttribute (p->hStmt, i + 1, SQL_DESC_UNSIGNED, 0, @@ -681,15 +655,14 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess 0, &unsignedFlag); if (r != SQL_SUCCESS) { - qSqlWarning(QStringLiteral("qMakeField: Unable to get column attributes for column ") - + QString::number(i), hStmt); + qSqlWarning(("QODBC::qMakeFieldInfo: Unable to get column attributes for column %1"_L1) + .arg(QString::number(i)), p); } const QString qColName(fromSQLTCHAR(colName, colNameLen)); // nullable can be SQL_NO_NULLS, SQL_NULLABLE or SQL_NULLABLE_UNKNOWN QMetaType type = qDecodeODBCType(colType, unsignedFlag == SQL_FALSE); QSqlField f(qColName, type); - f.setSqlType(colType); f.setLength(colSize == 0 ? -1 : int(colSize)); f.setPrecision(colScale == 0 ? -1 : int(colScale)); if (nullable == SQL_NO_NULLS) @@ -697,13 +670,18 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess else if (nullable == SQL_NULLABLE) f.setRequired(false); // else we don't know - f.setAutoValue(isAutoValue(hStmt, i)); - QVarLengthArray<SQLTCHAR> tableName(TABLENAMESIZE); + f.setAutoValue(isAutoValue(p->hStmt, i)); + QVarLengthArray<SQLTCHAR, TABLENAMESIZE> tableName(TABLENAMESIZE); SQLSMALLINT tableNameLen; - r = SQLColAttribute(hStmt, i + 1, SQL_DESC_BASE_TABLE_NAME, tableName.data(), - TABLENAMESIZE, &tableNameLen, 0); + r = SQLColAttribute(p->hStmt, + i + 1, + SQL_DESC_BASE_TABLE_NAME, + tableName.data(), + SQLSMALLINT(tableName.size() * sizeof(SQLTCHAR)), // SQLColAttribute needs/returns size in bytes + &tableNameLen, + 0); if (r == SQL_SUCCESS) - f.setTableName(fromSQLTCHAR(tableName, tableNameLen)); + f.setTableName(fromSQLTCHAR(tableName, tableNameLen / sizeof(SQLTCHAR))); return f; } @@ -724,7 +702,7 @@ QChar QODBCDriverPrivate::quoteChar() &driverResponse, sizeof(driverResponse), &length); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) quote = QChar(driverResponse[0]); else quote = u'"'; @@ -733,176 +711,193 @@ QChar QODBCDriverPrivate::quoteChar() return quote; } +SQLRETURN QODBCDriverPrivate::sqlFetchNext(const SqlStmtHandle &hStmt) const +{ + return sqlFetchNext(hStmt.handle()); +} -bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) +SQLRETURN QODBCDriverPrivate::sqlFetchNext(SQLHANDLE hStmt) const +{ + if (hasSQLFetchScroll) + return SQLFetchScroll(hStmt, SQL_FETCH_NEXT, 0); + return SQLFetch(hStmt); +} + +static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, QStringView val) +{ + auto encoded = toSQLTCHAR(val); + return SQLSetConnectAttr(handle, attr, + encoded.data(), + SQLINTEGER(encoded.size() * sizeof(SQLTCHAR))); // size in bytes +} + + +bool QODBCDriverPrivate::setConnectionOptions(const QString &connOpts) { // Set any connection attributes - const QStringList opts(connOpts.split(u';', Qt::SkipEmptyParts)); SQLRETURN r = SQL_SUCCESS; - for (int i = 0; i < opts.count(); ++i) { - const QString tmp(opts.at(i)); + for (const auto connOpt : QStringTokenizer{connOpts, u';'}) { int idx; - if ((idx = tmp.indexOf(u'=')) == -1) { - qWarning() << "QODBCDriver::open: Illegal connect option value '" << tmp << '\''; + if ((idx = connOpt.indexOf(u'=')) == -1) { + qSqlWarning(("QODBCDriver::open: Illegal connect option value '%1'"_L1) + .arg(connOpt), this); continue; } - const QString opt(tmp.left(idx)); - const QString val(tmp.mid(idx + 1).simplified()); + const auto opt(connOpt.left(idx)); + const auto val(connOpt.mid(idx + 1).trimmed()); SQLUINTEGER v = 0; r = SQL_SUCCESS; - if (opt.toUpper() == "SQL_ATTR_ACCESS_MODE"_L1) { - if (val.toUpper() == "SQL_MODE_READ_ONLY"_L1) { + if (opt == "SQL_ATTR_ACCESS_MODE"_L1) { + if (val == "SQL_MODE_READ_ONLY"_L1) { v = SQL_MODE_READ_ONLY; - } else if (val.toUpper() == "SQL_MODE_READ_WRITE"_L1) { + } else if (val == "SQL_MODE_READ_WRITE"_L1) { v = SQL_MODE_READ_WRITE; } else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CONNECTION_TIMEOUT"_L1) { + } else if (opt == "SQL_ATTR_CONNECTION_TIMEOUT"_L1) { v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_CONNECTION_TIMEOUT, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_LOGIN_TIMEOUT"_L1) { + } else if (opt == "SQL_ATTR_LOGIN_TIMEOUT"_L1) { v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_LOGIN_TIMEOUT, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CURRENT_CATALOG"_L1) { - val.utf16(); // 0 terminate - r = SQLSetConnectAttr(hDbc, SQL_ATTR_CURRENT_CATALOG, - toSQLTCHAR(val).data(), - SQLINTEGER(val.length() * sizeof(SQLTCHAR))); - } else if (opt.toUpper() == "SQL_ATTR_METADATA_ID"_L1) { - if (val.toUpper() == "SQL_TRUE"_L1) { + } else if (opt == "SQL_ATTR_CURRENT_CATALOG"_L1) { + r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_CURRENT_CATALOG, val); + } else if (opt == "SQL_ATTR_METADATA_ID"_L1) { + if (val == "SQL_TRUE"_L1) { v = SQL_TRUE; - } else if (val.toUpper() == "SQL_FALSE"_L1) { + } else if (val == "SQL_FALSE"_L1) { v = SQL_FALSE; } else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_METADATA_ID, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_PACKET_SIZE"_L1) { + } else if (opt == "SQL_ATTR_PACKET_SIZE"_L1) { v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_PACKET_SIZE, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_TRACEFILE"_L1) { - val.utf16(); // 0 terminate - r = SQLSetConnectAttr(hDbc, SQL_ATTR_TRACEFILE, - toSQLTCHAR(val).data(), - SQLINTEGER(val.length() * sizeof(SQLTCHAR))); - } else if (opt.toUpper() == "SQL_ATTR_TRACE"_L1) { - if (val.toUpper() == "SQL_OPT_TRACE_OFF"_L1) { + } else if (opt == "SQL_ATTR_TRACEFILE"_L1) { + r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_TRACEFILE, val); + } else if (opt == "SQL_ATTR_TRACE"_L1) { + if (val == "SQL_OPT_TRACE_OFF"_L1) { v = SQL_OPT_TRACE_OFF; - } else if (val.toUpper() == "SQL_OPT_TRACE_ON"_L1) { + } else if (val == "SQL_OPT_TRACE_ON"_L1) { v = SQL_OPT_TRACE_ON; } else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_TRACE, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CONNECTION_POOLING"_L1) { + } else if (opt == "SQL_ATTR_CONNECTION_POOLING"_L1) { if (val == "SQL_CP_OFF"_L1) v = SQL_CP_OFF; - else if (val.toUpper() == "SQL_CP_ONE_PER_DRIVER"_L1) + else if (val == "SQL_CP_ONE_PER_DRIVER"_L1) v = SQL_CP_ONE_PER_DRIVER; - else if (val.toUpper() == "SQL_CP_ONE_PER_HENV"_L1) + else if (val == "SQL_CP_ONE_PER_HENV"_L1) v = SQL_CP_ONE_PER_HENV; - else if (val.toUpper() == "SQL_CP_DEFAULT"_L1) + else if (val == "SQL_CP_DEFAULT"_L1) v = SQL_CP_DEFAULT; else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_CONNECTION_POOLING, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CP_MATCH"_L1) { - if (val.toUpper() == "SQL_CP_STRICT_MATCH"_L1) + } else if (opt == "SQL_ATTR_CP_MATCH"_L1) { + if (val == "SQL_CP_STRICT_MATCH"_L1) v = SQL_CP_STRICT_MATCH; - else if (val.toUpper() == "SQL_CP_RELAXED_MATCH"_L1) + else if (val == "SQL_CP_RELAXED_MATCH"_L1) v = SQL_CP_RELAXED_MATCH; - else if (val.toUpper() == "SQL_CP_MATCH_DEFAULT"_L1) + else if (val == "SQL_CP_MATCH_DEFAULT"_L1) v = SQL_CP_MATCH_DEFAULT; else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_CP_MATCH, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_ODBC_VERSION"_L1) { + } else if (opt == "SQL_ATTR_ODBC_VERSION"_L1) { // Already handled in QODBCDriver::open() continue; } else { - qWarning() << "QODBCDriver::open: Unknown connection attribute '" << opt << '\''; + qSqlWarning(("QODBCDriver::open: Unknown connection attribute '%1'"_L1) + .arg(opt), this); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) - qSqlWarning(QString::fromLatin1("QODBCDriver::open: Unable to set connection attribute'%1'").arg( - opt), this); + if (!SQL_SUCCEEDED(r)) + qSqlWarning(("QODBCDriver::open: Unable to set connection attribute '%1'"_L1) + .arg(opt), this); } return true; } -void QODBCDriverPrivate::splitTableQualifier(const QString & qualifier, QString &catalog, - QString &schema, QString &table) +void QODBCDriverPrivate::splitTableQualifier(const QString &qualifier, QString &catalog, + QString &schema, QString &table) const { + Q_Q(const QODBCDriver); + const auto adjustName = [&](const QString &name) { + if (q->isIdentifierEscaped(name, QSqlDriver::TableName)) + return q->stripDelimiters(name, QSqlDriver::TableName); + return adjustCase(name); + }; + catalog.clear(); + schema.clear(); + table.clear(); if (!useSchema) { - table = qualifier; + table = adjustName(qualifier); return; } - QStringList l = qualifier.split(u'.'); - if (l.count() > 3) - return; // can't possibly be a valid table qualifier - int i = 0, n = l.count(); - if (n == 1) { - table = qualifier; - } else { - for (QStringList::Iterator it = l.begin(); it != l.end(); ++it) { - if (n == 3) { - if (i == 0) { - catalog = *it; - } else if (i == 1) { - schema = *it; - } else if (i == 2) { - table = *it; - } - } else if (n == 2) { - if (i == 0) { - schema = *it; - } else if (i == 1) { - table = *it; - } - } - i++; - } + const QList<QStringView> l = QStringView(qualifier).split(u'.'); + switch (l.count()) { + case 1: + table = adjustName(qualifier); + break; + case 2: + schema = adjustName(l.at(0).toString()); + table = adjustName(l.at(1).toString()); + break; + case 3: + catalog = adjustName(l.at(0).toString()); + schema = adjustName(l.at(1).toString()); + table = adjustName(l.at(2).toString()); + break; + default: + qSqlWarning(("QODBCDriver::splitTableQualifier: Unable to split table qualifier '%1'"_L1) + .arg(qualifier), this); + break; } } -QODBCDriverPrivate::DefaultCase QODBCDriverPrivate::defaultCase() const +void QODBCDriverPrivate::checkDefaultCase() { - DefaultCase ret; + m_defaultCase = DefaultCase::Mixed; //arbitrary case if driver cannot be queried SQLUSMALLINT casing; - int r = SQLGetInfo(hDbc, - SQL_IDENTIFIER_CASE, - &casing, - sizeof(casing), - NULL); - if ( r != SQL_SUCCESS) - ret = Mixed;//arbitrary case if driver cannot be queried - else { + SQLRETURN r = SQLGetInfo(hDbc, + SQL_IDENTIFIER_CASE, + &casing, + sizeof(casing), + NULL); + if (r == SQL_SUCCESS) { switch (casing) { - case (SQL_IC_UPPER): - ret = Upper; - break; - case (SQL_IC_LOWER): - ret = Lower; - break; - case (SQL_IC_SENSITIVE): - ret = Sensitive; - break; - case (SQL_IC_MIXED): - default: - ret = Mixed; - break; + case SQL_IC_UPPER: + m_defaultCase = DefaultCase::Upper; + break; + case SQL_IC_LOWER: + m_defaultCase = DefaultCase::Lower; + break; + case SQL_IC_SENSITIVE: + m_defaultCase = DefaultCase::Sensitive; + break; + case SQL_IC_MIXED: + m_defaultCase = DefaultCase::Mixed; + break; } } - return ret; } /* @@ -911,20 +906,16 @@ QODBCDriverPrivate::DefaultCase QODBCDriverPrivate::defaultCase() const */ QString QODBCDriverPrivate::adjustCase(const QString &identifier) const { - QString ret = identifier; - switch(defaultCase()) { - case (Lower): - ret = identifier.toLower(); - break; - case (Upper): - ret = identifier.toUpper(); - break; - case(Mixed): - case(Sensitive): - default: - ret = identifier; + switch (m_defaultCase) { + case DefaultCase::Lower: + return identifier.toLower(); + case DefaultCase::Upper: + return identifier.toUpper(); + case DefaultCase::Mixed: + case DefaultCase::Sensitive: + break; } - return ret; + return identifier; } //////////////////////////////////////////////////////////////////////////// @@ -940,8 +931,7 @@ QODBCResult::~QODBCResult() if (d->hStmt && d->isStmtHandleValid() && driver() && driver()->isOpen()) { SQLRETURN r = SQLFreeHandle(SQL_HANDLE_STMT, d->hStmt); if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle "_L1 - + QString::number(r), d); + qSqlWarning(("QODBCResult: Unable to free statement handle "_L1), d); } } @@ -985,17 +975,20 @@ bool QODBCResult::reset (const QString& query) (SQLPOINTER)SQL_CURSOR_STATIC, SQL_IS_UINTEGER); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "QODBCResult::reset: Unable to set 'SQL_CURSOR_STATIC' as statement attribute. " "Please check your ODBC driver configuration"), QSqlError::StatementError, d)); return false; } - r = SQLExecDirect(d->hStmt, - toSQLTCHAR(query).data(), - (SQLINTEGER) query.length()); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r!= SQL_NO_DATA) { + { + auto encoded = toSQLTCHAR(query); + r = SQLExecDirect(d->hStmt, + encoded.data(), + SQLINTEGER(encoded.size())); + } + if (!SQL_SUCCEEDED(r) && r!= SQL_NO_DATA) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to execute statement"), QSqlError::StatementError, d)); return false; @@ -1003,14 +996,14 @@ bool QODBCResult::reset (const QString& query) SQLULEN isScrollable = 0; r = SQLGetStmtAttr(d->hStmt, SQL_ATTR_CURSOR_SCROLLABLE, &isScrollable, SQL_IS_INTEGER, 0); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) setForwardOnly(isScrollable == SQL_NONSCROLLABLE); SQLSMALLINT count = 0; SQLNumResultCols(d->hStmt, &count); if (count) { setSelect(true); - for (int i = 0; i < count; ++i) { + for (SQLSMALLINT i = 0; i < count; ++i) { d->rInf.append(qMakeFieldInfo(d, i)); } d->fieldCache.resize(count); @@ -1072,7 +1065,7 @@ bool QODBCResult::fetchNext() else r = SQLFetch(d->hStmt); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { if (r != SQL_NO_DATA) setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to fetch next"), QSqlError::ConnectionError, d)); @@ -1169,7 +1162,8 @@ QVariant QODBCResult::data(int field) { Q_D(QODBCResult); if (field >= d->rInf.count() || field < 0) { - qWarning() << "QODBCResult::data: column" << field << "out of range"; + qSqlWarning(("QODBCResult::data: column %1 out of range"_L1) + .arg(QString::number(field)), d); return QVariant(); } if (field < d->fieldCacheIdx) @@ -1205,7 +1199,7 @@ QVariant QODBCResult::data(int field) (SQLPOINTER)&dbuf, 0, &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (lengthIndicator != SQL_NULL_DATA)) + if (SQL_SUCCEEDED(r) && (lengthIndicator != SQL_NULL_DATA)) d->fieldCache[i] = QVariant(QDate(dbuf.year, dbuf.month, dbuf.day)); else d->fieldCache[i] = QVariant(QMetaType::fromType<QDate>()); @@ -1218,7 +1212,7 @@ QVariant QODBCResult::data(int field) (SQLPOINTER)&tbuf, 0, &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (lengthIndicator != SQL_NULL_DATA)) + if (SQL_SUCCEEDED(r) && (lengthIndicator != SQL_NULL_DATA)) d->fieldCache[i] = QVariant(QTime(tbuf.hour, tbuf.minute, tbuf.second)); else d->fieldCache[i] = QVariant(QMetaType::fromType<QTime>()); @@ -1231,7 +1225,7 @@ QVariant QODBCResult::data(int field) (SQLPOINTER)&dtbuf, 0, &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (lengthIndicator != SQL_NULL_DATA)) + if (SQL_SUCCEEDED(r) && (lengthIndicator != SQL_NULL_DATA)) d->fieldCache[i] = QVariant(QDateTime(QDate(dtbuf.year, dtbuf.month, dtbuf.day), QTime(dtbuf.hour, dtbuf.minute, dtbuf.second, dtbuf.fraction / 1000000))); else @@ -1261,7 +1255,7 @@ QVariant QODBCResult::data(int field) } break; default: - d->fieldCache[i] = QVariant(qGetStringData(d->hStmt, i, info.length(), false)); + d->fieldCache[i] = qGetStringData(d->hStmt, i, info.length(), false); break; } d->fieldCacheIdx = field + 1; @@ -1274,7 +1268,7 @@ bool QODBCResult::isNull(int field) Q_D(const QODBCResult); if (field < 0 || field >= d->fieldCache.size()) return true; - if (field <= d->fieldCacheIdx) { + if (field >= d->fieldCacheIdx) { // since there is no good way to find out whether the value is NULL // without fetching the field we'll fetch it here. // (data() also sets the NULL flag) @@ -1295,8 +1289,7 @@ int QODBCResult::numRowsAffected() SQLRETURN r = SQLRowCount(d->hStmt, &affectedRowCount); if (r == SQL_SUCCESS) return affectedRowCount; - else - qSqlWarning("QODBCResult::numRowsAffected: Unable to count affected rows"_L1, d); + qSqlWarning("QODBCResult::numRowsAffected: Unable to count affected rows"_L1, d); return -1; } @@ -1336,16 +1329,19 @@ bool QODBCResult::prepare(const QString& query) (SQLPOINTER)SQL_CURSOR_STATIC, SQL_IS_UINTEGER); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "QODBCResult::reset: Unable to set 'SQL_CURSOR_STATIC' as statement attribute. " "Please check your ODBC driver configuration"), QSqlError::StatementError, d)); return false; } - r = SQLPrepare(d->hStmt, - toSQLTCHAR(query).data(), - (SQLINTEGER) query.length()); + { + auto encoded = toSQLTCHAR(query); + r = SQLPrepare(d->hStmt, + encoded.data(), + SQLINTEGER(encoded.size())); + } if (r != SQL_SUCCESS) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", @@ -1373,14 +1369,12 @@ bool QODBCResult::exec() SQLCloseCursor(d->hStmt); QVariantList &values = boundValues(); - QByteArrayList tmpStorage(values.count(), QByteArray()); // holds temporary buffers - QVarLengthArray<SQLLEN, 32> indicators(values.count()); - memset(indicators.data(), 0, indicators.size() * sizeof(SQLLEN)); + QByteArrayList tmpStorage(values.count(), QByteArray()); // targets for SQLBindParameter() + QVarLengthArray<SQLLEN, 32> indicators(values.count(), 0); // bind parameters - only positional binding allowed - int i; SQLRETURN r; - for (i = 0; i < values.count(); ++i) { + for (qsizetype i = 0; i < values.count(); ++i) { if (bindValueType(i) & QSql::Out) values[i].detach(); const QVariant &val = values.at(i); @@ -1588,36 +1582,36 @@ bool QODBCResult::exec() case QMetaType::QString: if (d->unicode) { QByteArray &ba = tmpStorage[i]; - QString str = val.toString(); + { + const auto encoded = toSQLTCHAR(val.toString()); + ba = QByteArray(reinterpret_cast<const char *>(encoded.data()), + encoded.size() * sizeof(SQLTCHAR)); + } + if (*ind != SQL_NULL_DATA) - *ind = str.length() * sizeof(SQLTCHAR); - const qsizetype strSize = str.length() * sizeof(SQLTCHAR); + *ind = ba.size(); if (bindValueType(i) & QSql::Out) { - const QVarLengthArray<SQLTCHAR> a(toSQLTCHAR(str)); - ba = QByteArray((const char *)a.constData(), int(a.size() * sizeof(SQLTCHAR))); r = SQLBindParameter(d->hStmt, i + 1, qParamType[bindValueType(i) & QSql::InOut], SQL_C_TCHAR, - strSize > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, + ba.size() > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, 0, // god knows... don't change this! 0, - ba.data(), + const_cast<char *>(ba.constData()), // don't detach ba.size(), ind); break; } - ba = QByteArray(reinterpret_cast<const char *>(toSQLTCHAR(str).constData()), - int(strSize)); r = SQLBindParameter(d->hStmt, i + 1, qParamType[bindValueType(i) & QSql::InOut], SQL_C_TCHAR, - strSize > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, - strSize, + ba.size() > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, + ba.size(), 0, - const_cast<char *>(ba.constData()), + const_cast<char *>(ba.constData()), // don't detach ba.size(), ind); break; @@ -1660,15 +1654,15 @@ bool QODBCResult::exec() break; } } if (r != SQL_SUCCESS) { - qWarning() << "QODBCResult::exec: unable to bind variable:" << qODBCWarn(d); + qSqlWarning("QODBCResult::exec: unable to bind variable:"_L1, d); setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to bind variable"), QSqlError::StatementError, d)); return false; } } r = SQLExecute(d->hStmt); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r != SQL_NO_DATA) { - qWarning() << "QODBCResult::exec: Unable to execute statement:" << qODBCWarn(d); + if (!SQL_SUCCEEDED(r) && r != SQL_NO_DATA) { + qSqlWarning("QODBCResult::exec: Unable to execute statement:"_L1, d); setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to execute statement"), QSqlError::StatementError, d)); return false; @@ -1676,14 +1670,14 @@ bool QODBCResult::exec() SQLULEN isScrollable = 0; r = SQLGetStmtAttr(d->hStmt, SQL_ATTR_CURSOR_SCROLLABLE, &isScrollable, SQL_IS_INTEGER, 0); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) setForwardOnly(isScrollable == SQL_NONSCROLLABLE); SQLSMALLINT count = 0; SQLNumResultCols(d->hStmt, &count); if (count) { setSelect(true); - for (int i = 0; i < count; ++i) { + for (SQLSMALLINT i = 0; i < count; ++i) { d->rInf.append(qMakeFieldInfo(d, i)); } d->fieldCache.resize(count); @@ -1697,7 +1691,7 @@ bool QODBCResult::exec() if (!hasOutValues()) return true; - for (i = 0; i < values.count(); ++i) { + for (qsizetype i = 0; i < values.count(); ++i) { switch (values.at(i).typeId()) { case QMetaType::QDate: { DATE_STRUCT ds = *((DATE_STRUCT *)const_cast<char *>(tmpStorage.at(i).constData())); @@ -1728,10 +1722,11 @@ bool QODBCResult::exec() case QMetaType::QString: if (d->unicode) { if (bindValueType(i) & QSql::Out) { - const QByteArray &first = tmpStorage.at(i); - QVarLengthArray<SQLTCHAR> array; - array.append((const SQLTCHAR *)first.constData(), first.size()); - values[i] = fromSQLTCHAR(array, first.size()/sizeof(SQLTCHAR)); + const QByteArray &bytes = tmpStorage.at(i); + const auto strSize = bytes.size() / sizeof(SQLTCHAR); + QVarLengthArray<SQLTCHAR> string(strSize); + memcpy(string.data(), bytes.data(), strSize * sizeof(SQLTCHAR)); + values[i] = fromSQLTCHAR(string); } break; } @@ -1807,9 +1802,7 @@ bool QODBCResult::nextResult() SQLRETURN r = SQLMoreResults(d->hStmt); if (r != SQL_SUCCESS) { if (r == SQL_SUCCESS_WITH_INFO) { - int nativeCode = -1; - QString message = qODBCWarn(d, &nativeCode); - qWarning() << "QODBCResult::nextResult():" << message; + qSqlWarning("QODBCResult::nextResult:"_L1, d); } else { if (r != SQL_NO_DATA) setLastError(qMakeError(QCoreApplication::translate("QODBCResult", @@ -1822,7 +1815,7 @@ bool QODBCResult::nextResult() SQLNumResultCols(d->hStmt, &count); if (count) { setSelect(true); - for (int i = 0; i < count; ++i) { + for (SQLSMALLINT i = 0; i < count; ++i) { d->rInf.append(qMakeFieldInfo(d, i)); } d->fieldCache.resize(count); @@ -1928,6 +1921,18 @@ bool QODBCDriver::open(const QString & db, int, const QString& connOpts) { + const auto ensureEscaped = [](QString arg) -> QString { + QChar quoteChar; + if (arg.startsWith(u'"')) + quoteChar = u'\''; + else if (arg.startsWith(u'\'')) + quoteChar = u'"'; + else if (arg.contains(u';')) + quoteChar = u'"'; + else + return arg; + return quoteChar + arg + quoteChar; + }; Q_D(QODBCDriver); if (isOpen()) close(); @@ -1935,7 +1940,7 @@ bool QODBCDriver::open(const QString & db, r = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &d->hEnv); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { qSqlWarning("QODBCDriver::open: Unable to allocate environment"_L1, d); setOpenError(true); return false; @@ -1947,7 +1952,7 @@ bool QODBCDriver::open(const QString & db, r = SQLAllocHandle(SQL_HANDLE_DBC, d->hEnv, &d->hDbc); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { qSqlWarning("QODBCDriver::open: Unable to allocate connection"_L1, d); setOpenError(true); cleanup(); @@ -1963,31 +1968,31 @@ bool QODBCDriver::open(const QString & db, QString connQStr; // support the "DRIVER={SQL SERVER};SERVER=blah" syntax if (db.contains(".dsn"_L1, Qt::CaseInsensitive)) - connQStr = "FILEDSN="_L1 + db; + connQStr = "FILEDSN="_L1 + ensureEscaped(db); else if (db.contains("DRIVER="_L1, Qt::CaseInsensitive) || db.contains("SERVER="_L1, Qt::CaseInsensitive)) connQStr = db; else - connQStr = "DSN="_L1 + db; + connQStr = "DSN="_L1 + ensureEscaped(db); if (!user.isEmpty()) - connQStr += ";UID="_L1 + user; + connQStr += ";UID="_L1 + ensureEscaped(user); if (!password.isEmpty()) - connQStr += ";PWD="_L1 + password; + connQStr += ";PWD="_L1 + ensureEscaped(password); SQLSMALLINT cb; - QVarLengthArray<SQLTCHAR> connOut(1024); - memset(connOut.data(), 0, connOut.size() * sizeof(SQLTCHAR)); - r = SQLDriverConnect(d->hDbc, - NULL, - toSQLTCHAR(connQStr).data(), - (SQLSMALLINT)connQStr.length(), - connOut.data(), - 1024, - &cb, - /*SQL_DRIVER_NOPROMPT*/0); - - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + QVarLengthArray<SQLTCHAR, 1024> connOut(1024); + { + auto encoded = toSQLTCHAR(connQStr); + r = SQLDriverConnect(d->hDbc, + nullptr, + encoded.data(), SQLSMALLINT(encoded.size()), + connOut.data(), SQLSMALLINT(connOut.size()), + &cb, + /*SQL_DRIVER_NOPROMPT*/0); + } + + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(tr("Unable to connect"), QSqlError::ConnectionError, d)); setOpenError(true); cleanup(); @@ -2008,6 +2013,7 @@ bool QODBCDriver::open(const QString & db, d->checkHasSQLFetchScroll(); d->checkHasMultiResults(); d->checkDateTimePrecision(); + d->checkDefaultCase(); setOpen(true); setOpenError(false); if (d->dbmsType == MSSqlServer) { @@ -2066,7 +2072,7 @@ void QODBCDriverPrivate::checkUnicode() (SQLPOINTER)&fFunc, sizeof(fFunc), NULL); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (fFunc & SQL_CVT_WCHAR)) { + if (SQL_SUCCEEDED(r) && (fFunc & SQL_CVT_WCHAR)) { unicode = true; return; } @@ -2076,7 +2082,7 @@ void QODBCDriverPrivate::checkUnicode() (SQLPOINTER)&fFunc, sizeof(fFunc), NULL); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (fFunc & SQL_CVT_WVARCHAR)) { + if (SQL_SUCCEEDED(r) && (fFunc & SQL_CVT_WVARCHAR)) { unicode = true; return; } @@ -2086,74 +2092,86 @@ void QODBCDriverPrivate::checkUnicode() (SQLPOINTER)&fFunc, sizeof(fFunc), NULL); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (fFunc & SQL_CVT_WLONGVARCHAR)) { + if (SQL_SUCCEEDED(r) && (fFunc & SQL_CVT_WLONGVARCHAR)) { unicode = true; return; } - SQLHANDLE hStmt; - r = SQLAllocHandle(SQL_HANDLE_STMT, - hDbc, - &hStmt); - r = SQLExecDirect(hStmt, toSQLTCHAR("select 'test'"_L1).data(), SQL_NTS); + SqlStmtHandle hStmt(hDbc); + // for databases which do not return something useful in SQLGetInfo and are picky about a + // 'SELECT' statement without 'FROM' but support VALUE(foo) statement like e.g. DB2 or Oracle + const std::array<QStringView, 3> statements = { + u"select 'test'", + u"values('test')", + u"select 'test' from dual", + }; + for (const auto &statement : statements) { + auto encoded = toSQLTCHAR(statement); + r = SQLExecDirect(hStmt.handle(), encoded.data(), SQLINTEGER(encoded.size())); + if (r == SQL_SUCCESS) + break; + } if (r == SQL_SUCCESS) { - r = SQLFetch(hStmt); + r = SQLFetch(hStmt.handle()); if (r == SQL_SUCCESS) { - QVarLengthArray<SQLWCHAR> buffer(10); - r = SQLGetData(hStmt, 1, SQL_C_WCHAR, buffer.data(), buffer.size() * sizeof(SQLWCHAR), NULL); + QVarLengthArray<SQLWCHAR, 10> buffer(10); + r = SQLGetData(hStmt.handle(), 1, SQL_C_WCHAR, buffer.data(), + buffer.size() * sizeof(SQLWCHAR), NULL); if (r == SQL_SUCCESS && fromSQLTCHAR(buffer) == "test"_L1) { unicode = true; } } } - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); } bool QODBCDriverPrivate::checkDriver() const { #ifdef ODBC_CHECK_DRIVER - static const SQLUSMALLINT reqFunc[] = { + static constexpr SQLUSMALLINT reqFunc[] = { SQL_API_SQLDESCRIBECOL, SQL_API_SQLGETDATA, SQL_API_SQLCOLUMNS, SQL_API_SQLGETSTMTATTR, SQL_API_SQLGETDIAGREC, SQL_API_SQLEXECDIRECT, - SQL_API_SQLGETINFO, SQL_API_SQLTABLES, 0 + SQL_API_SQLGETINFO, SQL_API_SQLTABLES }; // these functions are optional - static const SQLUSMALLINT optFunc[] = { - SQL_API_SQLNUMRESULTCOLS, SQL_API_SQLROWCOUNT, 0 + static constexpr SQLUSMALLINT optFunc[] = { + SQL_API_SQLNUMRESULTCOLS, SQL_API_SQLROWCOUNT }; SQLRETURN r; SQLUSMALLINT sup; - int i; // check the required functions - for (i = 0; reqFunc[i] != 0; ++i) { + for (const SQLUSMALLINT func : reqFunc) { - r = SQLGetFunctions(hDbc, reqFunc[i], &sup); + r = SQLGetFunctions(hDbc, func, &sup); if (r != SQL_SUCCESS) { qSqlWarning("QODBCDriver::checkDriver: Cannot get list of supported functions"_L1, this); return false; } if (sup == SQL_FALSE) { - qWarning () << "QODBCDriver::open: Warning - Driver doesn't support all needed functionality (" << reqFunc[i] << - ").\nPlease look at the Qt SQL Module Driver documentation for more information."; + qSqlWarning(("QODBCDriver::checkDriver: Driver doesn't support all needed " + "functionality (func id %1).\nPlease look at the Qt SQL Module " + "Driver documentation for more information."_L1) + .arg(QString::number(func)), this); return false; } } // these functions are optional and just generate a warning - for (i = 0; optFunc[i] != 0; ++i) { + for (const SQLUSMALLINT func : optFunc) { - r = SQLGetFunctions(hDbc, optFunc[i], &sup); + r = SQLGetFunctions(hDbc, func, &sup); if (r != SQL_SUCCESS) { qSqlWarning("QODBCDriver::checkDriver: Cannot get list of supported functions"_L1, this); return false; } if (sup == SQL_FALSE) { - qWarning() << "QODBCDriver::checkDriver: Warning - Driver doesn't support some non-critical functions (" << optFunc[i] << ')'; + qSqlWarning(("QODBCDriver::checkDriver: Driver doesn't support some " + "non-critical functions (func id %1)."_L1) + .arg(QString::number(func)), this); return true; } } @@ -2172,23 +2190,22 @@ void QODBCDriverPrivate::checkSchemaUsage() (SQLPOINTER) &val, sizeof(val), NULL); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) useSchema = (val != 0); } void QODBCDriverPrivate::checkDBMS() { SQLRETURN r; - QVarLengthArray<SQLTCHAR> serverString(200); + QVarLengthArray<SQLTCHAR, 200> serverString(200); SQLSMALLINT t; - memset(serverString.data(), 0, serverString.size() * sizeof(SQLTCHAR)); r = SQLGetInfo(hDbc, SQL_DBMS_NAME, serverString.data(), SQLSMALLINT(serverString.size() * sizeof(SQLTCHAR)), &t); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { + if (SQL_SUCCEEDED(r)) { const QString serverType = fromSQLTCHAR(serverString, t / sizeof(SQLTCHAR)); if (serverType.contains("PostgreSQL"_L1, Qt::CaseInsensitive)) dbmsType = QSqlDriver::PostgreSQL; @@ -2206,7 +2223,7 @@ void QODBCDriverPrivate::checkDBMS() serverString.data(), SQLSMALLINT(serverString.size() * sizeof(SQLTCHAR)), &t); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { + if (SQL_SUCCEEDED(r)) { const QString serverType = fromSQLTCHAR(serverString, t / sizeof(SQLTCHAR)); isFreeTDSDriver = serverType.contains("tdsodbc"_L1, Qt::CaseInsensitive); unicode = unicode && !isFreeTDSDriver; @@ -2217,46 +2234,42 @@ void QODBCDriverPrivate::checkHasSQLFetchScroll() { SQLUSMALLINT sup; SQLRETURN r = SQLGetFunctions(hDbc, SQL_API_SQLFETCHSCROLL, &sup); - if ((r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) || sup != SQL_TRUE) { + if ((!SQL_SUCCEEDED(r)) || sup != SQL_TRUE) { hasSQLFetchScroll = false; - qWarning("QODBCDriver::checkHasSQLFetchScroll: Warning - Driver doesn't support scrollable result sets, use forward only mode for queries"); + qSqlWarning("QODBCDriver::checkHasSQLFetchScroll: Driver doesn't support " + "scrollable result sets, use forward only mode for queries"_L1, this); } } void QODBCDriverPrivate::checkHasMultiResults() { - QVarLengthArray<SQLTCHAR> driverResponse(2); + QVarLengthArray<SQLTCHAR, 2> driverResponse(2); SQLSMALLINT length; SQLRETURN r = SQLGetInfo(hDbc, SQL_MULT_RESULT_SETS, driverResponse.data(), SQLSMALLINT(driverResponse.size() * sizeof(SQLTCHAR)), &length); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) - hasMultiResultSets = fromSQLTCHAR(driverResponse, length/sizeof(SQLTCHAR)).startsWith(u'Y'); + if (SQL_SUCCEEDED(r)) + hasMultiResultSets = fromSQLTCHAR(driverResponse, length / sizeof(SQLTCHAR)).startsWith(u'Y'); } void QODBCDriverPrivate::checkDateTimePrecision() { SQLINTEGER columnSize; - SQLHANDLE hStmt; + SqlStmtHandle hStmt(hDbc); - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); - if (r != SQL_SUCCESS) { + if (!hStmt.isValid()) return; - } - r = SQLGetTypeInfo(hStmt, SQL_TIMESTAMP); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - r = SQLFetch(hStmt); - if ( r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO ) - { - if (SQLGetData(hStmt, 3, SQL_INTEGER, &columnSize, sizeof(columnSize), 0) == SQL_SUCCESS) { + SQLRETURN r = SQLGetTypeInfo(hStmt.handle(), SQL_TIMESTAMP); + if (SQL_SUCCEEDED(r)) { + r = SQLFetch(hStmt.handle()); + if (SQL_SUCCEEDED(r)) { + if (SQLGetData(hStmt.handle(), 3, SQL_INTEGER, &columnSize, sizeof(columnSize), 0) == SQL_SUCCESS) datetimePrecision = (int)columnSize; - } } } - SQLFreeHandle(SQL_HANDLE_STMT, hStmt); } QSqlResult *QODBCDriver::createResult() const @@ -2268,7 +2281,7 @@ bool QODBCDriver::beginTransaction() { Q_D(QODBCDriver); if (!isOpen()) { - qWarning("QODBCDriver::beginTransaction: Database not open"); + qSqlWarning("QODBCDriver::beginTransaction: Database not open"_L1, d); return false; } SQLUINTEGER ac(SQL_AUTOCOMMIT_OFF); @@ -2288,7 +2301,7 @@ bool QODBCDriver::commitTransaction() { Q_D(QODBCDriver); if (!isOpen()) { - qWarning("QODBCDriver::commitTransaction: Database not open"); + qSqlWarning("QODBCDriver::commitTransaction: Database not open"_L1, d); return false; } SQLRETURN r = SQLEndTran(SQL_HANDLE_DBC, @@ -2306,7 +2319,7 @@ bool QODBCDriver::rollbackTransaction() { Q_D(QODBCDriver); if (!isOpen()) { - qWarning("QODBCDriver::rollbackTransaction: Database not open"); + qSqlWarning("QODBCDriver::rollbackTransaction: Database not open"_L1, d); return false; } SQLRETURN r = SQLEndTran(SQL_HANDLE_DBC, @@ -2341,19 +2354,16 @@ QStringList QODBCDriver::tables(QSql::TableType type) const QStringList tl; if (!isOpen()) return tl; - SQLHANDLE hStmt; - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, - d->hDbc, - &hStmt); - if (r != SQL_SUCCESS) { + SqlStmtHandle hStmt(d->hDbc); + if (!hStmt.isValid()) { qSqlWarning("QODBCDriver::tables: Unable to allocate handle"_L1, d); return tl; } - r = SQLSetStmtAttr(hStmt, - SQL_ATTR_CURSOR_TYPE, - (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, - SQL_IS_UINTEGER); + SQLRETURN r = SQLSetStmtAttr(hStmt.handle(), + SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, + SQL_IS_UINTEGER); QStringList tableType; if (type & QSql::Tables) tableType += "TABLE"_L1; @@ -2364,48 +2374,32 @@ QStringList QODBCDriver::tables(QSql::TableType type) const if (tableType.isEmpty()) return tl; - QString joinedTableTypeString = tableType.join(u','); + { + auto joinedTableTypeString = toSQLTCHAR(tableType.join(u',')); - r = SQLTables(hStmt, - NULL, - 0, - NULL, - 0, - NULL, - 0, - toSQLTCHAR(joinedTableTypeString).data(), - joinedTableTypeString.length() /* characters, not bytes */); + r = SQLTables(hStmt.handle(), + nullptr, 0, + nullptr, 0, + nullptr, 0, + joinedTableTypeString.data(), joinedTableTypeString.size()); + } if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver::tables Unable to execute table list"_L1, d); + qSqlWarning("QODBCDriver::tables Unable to execute table list"_L1, + hStmt.handle()); - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); - - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r != SQL_NO_DATA) { - qWarning() << "QODBCDriver::tables failed to retrieve table/view list: (" << r << "," << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ")"; + r = d->sqlFetchNext(hStmt); + if (!SQL_SUCCEEDED(r) && r != SQL_NO_DATA) { + qSqlWarning("QODBCDriver::tables failed to retrieve table/view list"_L1, + hStmt.handle()); return QStringList(); } while (r == SQL_SUCCESS) { - QString fieldVal = qGetStringData(hStmt, 2, -1, d->unicode); - tl.append(fieldVal); - - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + tl.append(qGetStringData(hStmt.handle(), 2, -1, d->unicode).toString()); + r = d->sqlFetchNext(hStmt); } - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); - if (r!= SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle"_L1 + QString::number(r), d); return tl; } @@ -2418,98 +2412,69 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const bool usingSpecialColumns = false; QSqlRecord rec = record(tablename); - SQLHANDLE hStmt; - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, - d->hDbc, - &hStmt); - if (r != SQL_SUCCESS) { - qSqlWarning("QODBCDriver::primaryIndex: Unable to list primary key"_L1, d); + SqlStmtHandle hStmt(d->hDbc); + if (!hStmt.isValid()) { + qSqlWarning("QODBCDriver::primaryIndex: Unable to allocate handle"_L1, d); return index; } QString catalog, schema, table; - const_cast<QODBCDriverPrivate*>(d)->splitTableQualifier(tablename, catalog, schema, table); + d->splitTableQualifier(tablename, catalog, schema, table); - if (isIdentifierEscaped(catalog, QSqlDriver::TableName)) - catalog = stripDelimiters(catalog, QSqlDriver::TableName); - else - catalog = d->adjustCase(catalog); - - if (isIdentifierEscaped(schema, QSqlDriver::TableName)) - schema = stripDelimiters(schema, QSqlDriver::TableName); - else - schema = d->adjustCase(schema); - - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - else - table = d->adjustCase(table); - - r = SQLSetStmtAttr(hStmt, - SQL_ATTR_CURSOR_TYPE, - (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, - SQL_IS_UINTEGER); - r = SQLPrimaryKeys(hStmt, - catalog.length() == 0 ? NULL : toSQLTCHAR(catalog).data(), - catalog.length(), - schema.length() == 0 ? NULL : toSQLTCHAR(schema).data(), - schema.length(), - toSQLTCHAR(table).data(), - table.length() /* in characters, not in bytes */); + SQLRETURN r = SQLSetStmtAttr(hStmt.handle(), + SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, + SQL_IS_UINTEGER); + { + auto c = toSQLTCHAR(catalog); + auto s = toSQLTCHAR(schema); + auto t = toSQLTCHAR(table); + r = SQLPrimaryKeys(hStmt.handle(), + catalog.isEmpty() ? nullptr : c.data(), c.size(), + schema.isEmpty() ? nullptr : s.data(), s.size(), + t.data(), t.size()); + } // if the SQLPrimaryKeys() call does not succeed (e.g the driver // does not support it) - try an alternative method to get hold of // the primary index (e.g MS Access and FoxPro) if (r != SQL_SUCCESS) { - r = SQLSpecialColumns(hStmt, - SQL_BEST_ROWID, - catalog.length() == 0 ? NULL : toSQLTCHAR(catalog).data(), - catalog.length(), - schema.length() == 0 ? NULL : toSQLTCHAR(schema).data(), - schema.length(), - toSQLTCHAR(table).data(), - table.length(), - SQL_SCOPE_CURROW, - SQL_NULLABLE); + auto c = toSQLTCHAR(catalog); + auto s = toSQLTCHAR(schema); + auto t = toSQLTCHAR(table); + r = SQLSpecialColumns(hStmt.handle(), + SQL_BEST_ROWID, + catalog.isEmpty() ? nullptr : c.data(), c.size(), + schema.isEmpty() ? nullptr : s.data(), s.size(), + t.data(), t.size(), + SQL_SCOPE_CURROW, + SQL_NULLABLE); if (r != SQL_SUCCESS) { - qSqlWarning("QODBCDriver::primaryIndex: Unable to execute primary key list"_L1, d); + qSqlWarning("QODBCDriver::primaryIndex: Unable to execute primary key list"_L1, + hStmt.handle()); } else { usingSpecialColumns = true; } } - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + r = d->sqlFetchNext(hStmt); int fakeId = 0; QString cName, idxName; // Store all fields in a StringList because some drivers can't detail fields in this FETCH loop while (r == SQL_SUCCESS) { if (usingSpecialColumns) { - cName = qGetStringData(hStmt, 1, -1, d->unicode); // column name + cName = qGetStringData(hStmt.handle(), 1, -1, d->unicode).toString(); // column name idxName = QString::number(fakeId++); // invent a fake index name } else { - cName = qGetStringData(hStmt, 3, -1, d->unicode); // column name - idxName = qGetStringData(hStmt, 5, -1, d->unicode); // pk index name + cName = qGetStringData(hStmt.handle(), 3, -1, d->unicode).toString(); // column name + idxName = qGetStringData(hStmt.handle(), 5, -1, d->unicode).toString(); // pk index name } index.append(rec.field(cName)); index.setName(idxName); - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); - + r = d->sqlFetchNext(hStmt); } - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); - if (r!= SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle"_L1 + QString::number(r), d); return index; } @@ -2520,72 +2485,39 @@ QSqlRecord QODBCDriver::record(const QString& tablename) const if (!isOpen()) return fil; - SQLHANDLE hStmt; - QString catalog, schema, table; - const_cast<QODBCDriverPrivate*>(d)->splitTableQualifier(tablename, catalog, schema, table); - - if (isIdentifierEscaped(catalog, QSqlDriver::TableName)) - catalog = stripDelimiters(catalog, QSqlDriver::TableName); - else - catalog = d->adjustCase(catalog); - - if (isIdentifierEscaped(schema, QSqlDriver::TableName)) - schema = stripDelimiters(schema, QSqlDriver::TableName); - else - schema = d->adjustCase(schema); - - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - else - table = d->adjustCase(table); - - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, - d->hDbc, - &hStmt); - if (r != SQL_SUCCESS) { + SqlStmtHandle hStmt(d->hDbc); + if (!hStmt.isValid()) { qSqlWarning("QODBCDriver::record: Unable to allocate handle"_L1, d); return fil; } - r = SQLSetStmtAttr(hStmt, - SQL_ATTR_CURSOR_TYPE, - (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, - SQL_IS_UINTEGER); - r = SQLColumns(hStmt, - catalog.length() == 0 ? NULL : toSQLTCHAR(catalog).data(), - catalog.length(), - schema.length() == 0 ? NULL : toSQLTCHAR(schema).data(), - schema.length(), - toSQLTCHAR(table).data(), - table.length(), - NULL, - 0); - if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver::record: Unable to execute column list"_L1, d); - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + QString catalog, schema, table; + d->splitTableQualifier(tablename, catalog, schema, table); + + SQLRETURN r = SQLSetStmtAttr(hStmt.handle(), + SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, + SQL_IS_UINTEGER); + { + auto c = toSQLTCHAR(catalog); + auto s = toSQLTCHAR(schema); + auto t = toSQLTCHAR(table); + r = SQLColumns(hStmt.handle(), + catalog.isEmpty() ? nullptr : c.data(), c.size(), + schema.isEmpty() ? nullptr : s.data(), s.size(), + t.data(), t.size(), + nullptr, + 0); + } + if (r != SQL_SUCCESS) + qSqlWarning("QODBCDriver::record: Unable to execute column list"_L1, hStmt.handle()); + r = d->sqlFetchNext(hStmt); // Store all fields in a StringList because some drivers can't detail fields in this FETCH loop while (r == SQL_SUCCESS) { - - fil.append(qMakeFieldInfo(hStmt, d)); - - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + fil.append(qMakeFieldInfo(hStmt.handle(), d)); + r = d->sqlFetchNext(hStmt); } - - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); - if (r!= SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle "_L1 + QString::number(r), d); - return fil; } @@ -2597,9 +2529,10 @@ QString QODBCDriver::formatValue(const QSqlField &field, r = "NULL"_L1; } else if (field.metaType().id() == QMetaType::QDateTime) { // Use an escape sequence for the datetime fields - if (field.value().toDateTime().isValid()){ - QDate dt = field.value().toDateTime().date(); - QTime tm = field.value().toDateTime().time(); + const QDateTime dateTime = field.value().toDateTime(); + if (dateTime.isValid()) { + const QDate dt = dateTime.date(); + const QTime tm = dateTime.time(); // Dateformat has to be "yyyy-MM-dd hh:mm:ss", with leading zeroes if month or day < 10 r = "{ ts '"_L1 + QString::number(dt.year()) + u'-' + @@ -2612,15 +2545,14 @@ QString QODBCDriver::formatValue(const QSqlField &field, } else r = "NULL"_L1; } else if (field.metaType().id() == QMetaType::QByteArray) { - QByteArray ba = field.value().toByteArray(); - QString res; - static const char hexchars[] = "0123456789abcdef"; - for (int i = 0; i < ba.size(); ++i) { - uchar s = (uchar) ba[i]; - res += QLatin1Char(hexchars[s >> 4]); - res += QLatin1Char(hexchars[s & 0x0f]); + const QByteArray ba = field.value().toByteArray(); + r.reserve((ba.size() + 1) * 2); + r = "0x"_L1; + for (const char c : ba) { + const uchar s = uchar(c); + r += QLatin1Char(QtMiscUtils::toHexLower(s >> 4)); + r += QLatin1Char(QtMiscUtils::toHexLower(s & 0x0f)); } - r = "0x"_L1 + res; } else { r = QSqlDriver::formatValue(field, trimStrings); } @@ -2639,9 +2571,10 @@ QString QODBCDriver::escapeIdentifier(const QString &identifier, IdentifierType) QChar quote = const_cast<QODBCDriverPrivate*>(d)->quoteChar(); QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(quote) && !identifier.endsWith(quote) ) { - res.replace(quote, QString(quote)+QString(quote)); - res.prepend(quote).append(quote); - res.replace(u'.', QString(quote) + u'.' +QString(quote)); + const QString quoteStr(quote); + res.replace(quote, quoteStr + quoteStr); + res.replace(u'.', quoteStr + u'.' + quoteStr); + res = quote + res + quote; } return res; } diff --git a/src/plugins/sqldrivers/psql/CMakeLists.txt b/src/plugins/sqldrivers/psql/CMakeLists.txt index d3a6a8588e..2f55ab4950 100644 --- a/src/plugins/sqldrivers/psql/CMakeLists.txt +++ b/src/plugins/sqldrivers/psql/CMakeLists.txt @@ -1,6 +1,7 @@ -# Generated from psql.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -qt_find_package(PostgreSQL) # special case +qt_find_package(PostgreSQL) ##################################################################### ## QPSQLDriverPlugin Plugin: @@ -15,6 +16,7 @@ qt_internal_add_plugin(QPSQLDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES PostgreSQL::PostgreSQL Qt::Core @@ -22,9 +24,6 @@ qt_internal_add_plugin(QPSQLDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:psql.pro:<TRUE>: -# OTHER_FILES = "psql.json" - # PostgreSQL delivers header files that are not a part of PostgreSQL itself. When precompiled # headers are processed, MinGW uses 'pthread.h' from the PostgreSQL installation directory. # As result, we disable precompile headers for the plugin. diff --git a/src/plugins/sqldrivers/psql/qsql_psql.cpp b/src/plugins/sqldrivers/psql/qsql_psql.cpp index 7365d71695..7b9521fec6 100644 --- a/src/plugins/sqldrivers/psql/qsql_psql.cpp +++ b/src/plugins/sqldrivers/psql/qsql_psql.cpp @@ -6,6 +6,7 @@ #include <qcoreapplication.h> #include <qvariant.h> #include <qdatetime.h> +#include <qloggingcategory.h> #include <qregularexpression.h> #include <qsqlerror.h> #include <qsqlfield.h> @@ -65,6 +66,8 @@ Q_DECLARE_METATYPE(PGresult*) QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcPsql, "qt.sql.postgresql") + using namespace Qt::StringLiterals; inline void qPQfreemem(void *buffer) @@ -74,11 +77,11 @@ inline void qPQfreemem(void *buffer) /* Missing declaration of PGRES_SINGLE_TUPLE for PSQL below 9.2 */ #if !defined PG_VERSION_NUM || PG_VERSION_NUM-0 < 90200 -static const int PGRES_SINGLE_TUPLE = 9; +static constexpr int PGRES_SINGLE_TUPLE = 9; #endif typedef int StatementId; -static const StatementId InvalidStatementId = 0; +static constexpr StatementId InvalidStatementId = 0; class QPSQLResultPrivate; @@ -122,10 +125,9 @@ public: QSocketNotifier *sn = nullptr; QPSQLDriver::Protocol pro = QPSQLDriver::Version6; StatementId currentStmtId = InvalidStatementId; - int stmtCount = 0; + StatementId stmtCount = InvalidStatementId; mutable bool pendingNotifyCheck = false; bool hasBackslashEscape = false; - bool isUtf8 = false; void appendTables(QStringList &tl, QSqlQuery &t, QChar type); PGresult *exec(const char *stmt); @@ -141,6 +143,7 @@ public: bool setEncodingUtf8(); void setDatestyle(); void setByteaOutput(); + void setUtcTimeZone(); void detectBackslashEscape(); mutable QHash<int, QString> oidToTable; }; @@ -175,7 +178,7 @@ PGresult *QPSQLDriverPrivate::exec(const char *stmt) PGresult *QPSQLDriverPrivate::exec(const QString &stmt) { - return exec((isUtf8 ? stmt.toUtf8() : stmt.toLocal8Bit()).constData()); + return exec(stmt.toUtf8().constData()); } StatementId QPSQLDriverPrivate::sendQuery(const QString &stmt) @@ -183,8 +186,7 @@ StatementId QPSQLDriverPrivate::sendQuery(const QString &stmt) // Discard any prior query results that the application didn't eat. // This is required for PQsendQuery() discardResults(); - const int result = PQsendQuery(connection, - (isUtf8 ? stmt.toUtf8() : stmt.toLocal8Bit()).constData()); + const int result = PQsendQuery(connection, stmt.toUtf8().constData()); currentStmtId = result ? generateStatementId() : InvalidStatementId; return currentStmtId; } @@ -209,8 +211,8 @@ PGresult *QPSQLDriverPrivate::getResult(StatementId stmtId) const if (stmtId != currentStmtId) { // If you change the following warning, remember to update it // on sql-driver.html page too. - qWarning("QPSQLDriver::getResult: Query results lost - " - "probably discarded on executing another SQL query."); + qCWarning(lcPsql, "QPSQLDriver::getResult: Query results lost - " + "probably discarded on executing another SQL query."); return nullptr; } PGresult *result = PQgetResult(connection); @@ -234,7 +236,7 @@ void QPSQLDriverPrivate::discardResults() const StatementId QPSQLDriverPrivate::generateStatementId() { - int stmtId = ++stmtCount; + StatementId stmtId = ++stmtCount; if (stmtId <= 0) stmtId = stmtCount = 1; return stmtId; @@ -245,18 +247,18 @@ void QPSQLDriverPrivate::checkPendingNotifications() const Q_Q(const QPSQLDriver); if (seid.size() && !pendingNotifyCheck) { pendingNotifyCheck = true; - QMetaObject::invokeMethod(const_cast<QPSQLDriver*>(q), "_q_handleNotification", Qt::QueuedConnection); + QMetaObject::invokeMethod(const_cast<QPSQLDriver*>(q), &QPSQLDriver::_q_handleNotification, Qt::QueuedConnection); } } -class QPSQLResultPrivate : public QSqlResultPrivate +class QPSQLResultPrivate final : public QSqlResultPrivate { Q_DECLARE_PUBLIC(QPSQLResult) public: Q_DECLARE_SQLDRIVER_PRIVATE(QPSQLDriver) using QSqlResultPrivate::QSqlResultPrivate; - QString fieldSerial(int i) const override { return u'$' + QString::number(i + 1); } + QString fieldSerial(qsizetype i) const override { return QString("$%1"_L1).arg(i + 1); } void deallocatePreparedStmt(); std::queue<PGresult*> nextResultSets; @@ -274,7 +276,7 @@ static QSqlError qMakeError(const QString &err, QSqlError::ErrorType type, const QPSQLDriverPrivate *p, PGresult *result = nullptr) { const char *s = PQerrorMessage(p->connection); - QString msg = p->isUtf8 ? QString::fromUtf8(s) : QString::fromLocal8Bit(s); + QString msg = QString::fromUtf8(s); QString errorCode; if (result) { errorCode = QString::fromLatin1(PQresultErrorField(result, PG_DIAG_SQLSTATE)); @@ -382,8 +384,10 @@ void QPSQLResultPrivate::deallocatePreparedStmt() const QString stmt = QStringLiteral("DEALLOCATE ") + preparedStmtId; PGresult *result = drv_d_func()->exec(stmt); - if (PQresultStatus(result) != PGRES_COMMAND_OK) - qWarning("Unable to free statement: %s", PQerrorMessage(drv_d_func()->connection)); + if (PQresultStatus(result) != PGRES_COMMAND_OK) { + const QString msg = QString::fromUtf8(PQerrorMessage(drv_d_func()->connection)); + qCWarning(lcPsql, "Unable to free statement: %ls.", qUtf16Printable(msg)); + } PQclear(result); } preparedStmtId.clear(); @@ -593,7 +597,7 @@ QVariant QPSQLResult::data(int i) { Q_D(const QPSQLResult); if (i >= PQnfields(d->result)) { - qWarning("QPSQLResult::data: column %d out of range", i); + qCWarning(lcPsql, "QPSQLResult::data: column %d out of range.", i); return QVariant(); } const int currentRow = isForwardOnly() ? 0 : at(); @@ -606,7 +610,7 @@ QVariant QPSQLResult::data(int i) case QMetaType::Bool: return QVariant((bool)(val[0] == 't')); case QMetaType::QString: - return d->drv_d_func()->isUtf8 ? QString::fromUtf8(val) : QString::fromLatin1(val); + return QString::fromUtf8(val); case QMetaType::LongLong: if (val[0] == '-') return QByteArray::fromRawData(val, qstrlen(val)).toLongLong(); @@ -641,23 +645,21 @@ QVariant QPSQLResult::data(int i) } return dbl; } - case QMetaType::QDate: #if QT_CONFIG(datestring) + case QMetaType::QDate: return QVariant(QDate::fromString(QString::fromLatin1(val), Qt::ISODate)); -#else - return QVariant(QString::fromLatin1(val)); -#endif case QMetaType::QTime: -#if QT_CONFIG(datestring) return QVariant(QTime::fromString(QString::fromLatin1(val), Qt::ISODate)); + case QMetaType::QDateTime: { + QString tzString(QString::fromLatin1(val)); + if (!tzString.endsWith(u'Z')) + tzString.append(u'Z'); // make UTC + return QVariant(QDateTime::fromString(tzString, Qt::ISODate)); + } #else - return QVariant(QString::fromLatin1(val)); -#endif + case QMetaType::QDate: + case QMetaType::QTime: case QMetaType::QDateTime: -#if QT_CONFIG(datestring) - return QVariant(QDateTime::fromString(QString::fromLatin1(val), - Qt::ISODate).toLocalTime()); -#else return QVariant(QString::fromLatin1(val)); #endif case QMetaType::QByteArray: { @@ -668,7 +670,7 @@ QVariant QPSQLResult::data(int i) return QVariant(ba); } default: - qWarning("QPSQLResult::data: unknown data type"); + qCWarning(lcPsql, "QPSQLResult::data: unhandled data type %d.", type.id()); } return QVariant(); } @@ -747,10 +749,7 @@ QSqlRecord QPSQLResult::record() const int count = PQnfields(d->result); QSqlField f; for (int i = 0; i < count; ++i) { - if (d->drv_d_func()->isUtf8) - f.setName(QString::fromUtf8(PQfname(d->result, i))); - else - f.setName(QString::fromLocal8Bit(PQfname(d->result, i))); + f.setName(QString::fromUtf8(PQfname(d->result, i))); const int tableOid = PQftable(d->result, i); // WARNING: We cannot execute any other SQL queries on // the same db connection while forward-only mode is active @@ -801,7 +800,6 @@ QSqlRecord QPSQLResult::record() const f.setLength(len); f.setPrecision(precision); - f.setSqlType(ptype); info.append(f); } return info; @@ -835,7 +833,7 @@ static QString qCreateParamString(const QList<QVariant> &boundValues, const QSql QString qMakePreparedStmtId() { - static QBasicAtomicInt qPreparedStmtCount = Q_BASIC_ATOMIC_INITIALIZER(0); + Q_CONSTINIT static QBasicAtomicInt qPreparedStmtCount = Q_BASIC_ATOMIC_INITIALIZER(0); QString id = QStringLiteral("qpsqlpstmt_") + QString::number(qPreparedStmtCount.fetchAndAddRelaxed(1) + 1, 16); return id; } @@ -918,7 +916,7 @@ void QPSQLDriverPrivate::setDatestyle() PGresult *result = exec("SET DATESTYLE TO 'ISO'"); int status = PQresultStatus(result); if (status != PGRES_COMMAND_OK) - qWarning("%s", PQerrorMessage(connection)); + qCWarning(lcPsql) << QString::fromUtf8(PQerrorMessage(connection)); PQclear(result); } @@ -931,11 +929,20 @@ void QPSQLDriverPrivate::setByteaOutput() PGresult *result = exec("SET bytea_output TO escape"); int status = PQresultStatus(result); if (status != PGRES_COMMAND_OK) - qWarning("%s", PQerrorMessage(connection)); + qCWarning(lcPsql) << QString::fromUtf8(PQerrorMessage(connection)); PQclear(result); } } +void QPSQLDriverPrivate::setUtcTimeZone() +{ + PGresult *result = exec("SET TIME ZONE 'UTC'"); + int status = PQresultStatus(result); + if (status != PGRES_COMMAND_OK) + qCWarning(lcPsql) << QString::fromUtf8(PQerrorMessage(connection)); + PQclear(result); +} + void QPSQLDriverPrivate::detectBackslashEscape() { // standard_conforming_strings option introduced in 8.2 @@ -1069,16 +1076,16 @@ QPSQLDriver::Protocol QPSQLDriverPrivate::getPSQLVersion() if (serverVersion == QPSQLDriver::VersionUnknown) { serverVersion = clientVersion; if (serverVersion != QPSQLDriver::VersionUnknown) - qWarning("The server version of this PostgreSQL is unknown, falling back to the client version."); + qCWarning(lcPsql, "The server version of this PostgreSQL is unknown, " + "falling back to the client version."); } // Keep the old behavior unchanged if (serverVersion == QPSQLDriver::VersionUnknown) serverVersion = QPSQLDriver::Version6; - if (serverVersion < QPSQLDriver::Version7_3) { - qWarning("This version of PostgreSQL is not supported and may not work."); - } + if (serverVersion < QPSQLDriver::Version7_3) + qCWarning(lcPsql, "This version of PostgreSQL is not supported and may not work."); return serverVersion; } @@ -1104,8 +1111,7 @@ QPSQLDriver::QPSQLDriver(PGconn *conn, QObject *parent) QPSQLDriver::~QPSQLDriver() { Q_D(QPSQLDriver); - if (d->connection) - PQfinish(d->connection); + PQfinish(d->connection); } QVariant QPSQLDriver::handle() const @@ -1125,6 +1131,7 @@ bool QPSQLDriver::hasFeature(DriverFeature f) const case EventNotifications: case MultipleResultSets: case BLOB: + case Unicode: return true; case PreparedQueries: case PositionalPlaceholders: @@ -1135,8 +1142,6 @@ bool QPSQLDriver::hasFeature(DriverFeature f) const case FinishQuery: case CancelQuery: return false; - case Unicode: - return d->isUtf8; } return false; } @@ -1194,9 +1199,16 @@ bool QPSQLDriver::open(const QString &db, d->pro = d->getPSQLVersion(); d->detectBackslashEscape(); - d->isUtf8 = d->setEncodingUtf8(); + if (!d->setEncodingUtf8()) { + setLastError(qMakeError(tr("Unable to set client encoding to 'UNICODE'"), QSqlError::ConnectionError, d)); + setOpenError(true); + PQfinish(d->connection); + d->connection = nullptr; + return false; + } d->setDatestyle(); d->setByteaOutput(); + d->setUtcTimeZone(); setOpen(true); setOpenError(false); @@ -1209,13 +1221,12 @@ void QPSQLDriver::close() d->seid.clear(); if (d->sn) { - disconnect(d->sn, SIGNAL(activated(QSocketDescriptor)), this, SLOT(_q_handleNotification())); + disconnect(d->sn, &QSocketNotifier::activated, this, &QPSQLDriver::_q_handleNotification); delete d->sn; d->sn = nullptr; } - if (d->connection) - PQfinish(d->connection); + PQfinish(d->connection); d->connection = nullptr; setOpen(false); setOpenError(false); @@ -1230,7 +1241,7 @@ bool QPSQLDriver::beginTransaction() { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::beginTransaction: Database not open"); + qCWarning(lcPsql, "QPSQLDriver::beginTransaction: Database not open."); return false; } PGresult *res = d->exec("BEGIN"); @@ -1248,7 +1259,7 @@ bool QPSQLDriver::commitTransaction() { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::commitTransaction: Database not open"); + qCWarning(lcPsql, "QPSQLDriver::commitTransaction: Database not open."); return false; } PGresult *res = d->exec("COMMIT"); @@ -1277,7 +1288,7 @@ bool QPSQLDriver::rollbackTransaction() { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::rollbackTransaction: Database not open"); + qCWarning(lcPsql, "QPSQLDriver::rollbackTransaction: Database not open."); return false; } PGresult *res = d->exec("ROLLBACK"); @@ -1414,7 +1425,6 @@ QSqlRecord QPSQLDriver::record(const QString &tablename) const f.setLength(len); f.setPrecision(precision); f.setDefaultValue(defVal); - f.setSqlType(query.value(1).toInt()); info.append(f); } @@ -1439,32 +1449,28 @@ QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const r = nullStr(); } else { switch (field.metaType().id()) { - case QMetaType::QDateTime: -#if QT_CONFIG(datestring) - if (field.value().toDateTime().isValid()) { + case QMetaType::QDateTime: { + const auto dt = field.value().toDateTime(); + if (dt.isValid()) { // we force the value to be considered with a timezone information, and we force it to be UTC // this is safe since postgresql stores only the UTC value and not the timezone offset (only used // while parsing), so we have correct behavior in both case of with timezone and without tz r = QStringLiteral("TIMESTAMP WITH TIME ZONE ") + u'\'' + - QLocale::c().toString(field.value().toDateTime().toUTC(), u"yyyy-MM-ddThh:mm:ss.zzz") + + QLocale::c().toString(dt.toUTC(), u"yyyy-MM-ddThh:mm:ss.zzz") + u'Z' + u'\''; } else { r = nullStr(); } -#else - r = nullStr(); -#endif // datestring break; - case QMetaType::QTime: -#if QT_CONFIG(datestring) - if (field.value().toTime().isValid()) { - r = u'\'' + field.value().toTime().toString(u"hh:mm:ss.zzz") + u'\''; - } else -#endif - { + } + case QMetaType::QTime: { + const auto t = field.value().toTime(); + if (t.isValid()) + r = u'\'' + QLocale::c().toString(t, u"hh:mm:ss.zzz") + u'\''; + else r = nullStr(); - } break; + } case QMetaType::QString: r = QSqlDriver::formatValue(field, trimStrings); if (d->hasBackslashEscape) @@ -1516,8 +1522,8 @@ QString QPSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"') ) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } @@ -1538,7 +1544,7 @@ bool QPSQLDriver::subscribeToNotification(const QString &name) { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::subscribeToNotificationImplementation: database not open."); + qCWarning(lcPsql, "QPSQLDriver::subscribeToNotification: Database not open."); return false; } @@ -1563,11 +1569,12 @@ bool QPSQLDriver::subscribeToNotification(const QString &name) PQclear(result); if (!d->sn) { - d->sn = new QSocketNotifier(socket, QSocketNotifier::Read); - connect(d->sn, SIGNAL(activated(QSocketDescriptor)), this, SLOT(_q_handleNotification())); + d->sn = new QSocketNotifier(socket, QSocketNotifier::Read, this); + connect(d->sn, &QSocketNotifier::activated, this, &QPSQLDriver::_q_handleNotification); } } else { - qWarning("QPSQLDriver::subscribeToNotificationImplementation: PQsocket didn't return a valid socket to listen on"); + qCWarning(lcPsql, "QPSQLDriver::subscribeToNotificationImplementation: " + "PQsocket didn't return a valid socket to listen on."); return false; } @@ -1578,13 +1585,13 @@ bool QPSQLDriver::unsubscribeFromNotification(const QString &name) { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::unsubscribeFromNotificationImplementation: database not open."); + qCWarning(lcPsql, "QPSQLDriver::unsubscribeFromNotification: Database not open."); return false; } if (!d->seid.contains(name)) { - qWarning("QPSQLDriver::unsubscribeFromNotificationImplementation: not subscribed to '%s'.", - qPrintable(name)); + qCWarning(lcPsql, "QPSQLDriver::unsubscribeFromNotification: not subscribed to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1600,7 +1607,7 @@ bool QPSQLDriver::unsubscribeFromNotification(const QString &name) d->seid.removeAll(name); if (d->seid.isEmpty()) { - disconnect(d->sn, SIGNAL(activated(QSocketDescriptor)), this, SLOT(_q_handleNotification())); + disconnect(d->sn, &QSocketNotifier::activated, this, &QPSQLDriver::_q_handleNotification); delete d->sn; d->sn = nullptr; } @@ -1627,17 +1634,19 @@ void QPSQLDriver::_q_handleNotification() QString payload; #if defined PG_VERSION_NUM && PG_VERSION_NUM-0 >= 70400 if (notify->extra) - payload = d->isUtf8 ? QString::fromUtf8(notify->extra) : QString::fromLatin1(notify->extra); + payload = QString::fromUtf8(notify->extra); #endif QSqlDriver::NotificationSource source = (notify->be_pid == PQbackendPID(d->connection)) ? QSqlDriver::SelfSource : QSqlDriver::OtherSource; emit notification(name, source, payload); } else - qWarning("QPSQLDriver: received notification for '%s' which isn't subscribed to.", - qPrintable(name)); + qCWarning(lcPsql, "QPSQLDriver: received notification for '%ls' which isn't subscribed to.", + qUtf16Printable(name)); qPQfreemem(notify); } } QT_END_NAMESPACE + +#include "moc_qsql_psql_p.cpp" diff --git a/src/plugins/sqldrivers/qt_cmdline.cmake b/src/plugins/sqldrivers/qt_cmdline.cmake index 93991fb2a9..2bb1fc64eb 100644 --- a/src/plugins/sqldrivers/qt_cmdline.cmake +++ b/src/plugins/sqldrivers/qt_cmdline.cmake @@ -1,6 +1,9 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_commandline_option(mysql_config TYPE string) qt_commandline_option(psql_config TYPE string) -qt_commandline_option(sqlite TYPE enum NAME system-sqlite MAPPING qt no system yes) +qt_commandline_option(sqlite CONTROLS_FEATURE TYPE enum NAME system-sqlite MAPPING qt no system yes) qt_commandline_option(sql-db2 TYPE boolean) qt_commandline_option(sql-ibase TYPE boolean) qt_commandline_option(sql-mysql TYPE boolean) @@ -8,6 +11,7 @@ qt_commandline_option(sql-oci TYPE boolean) qt_commandline_option(sql-odbc TYPE boolean) qt_commandline_option(sql-psql TYPE boolean) qt_commandline_option(sql-sqlite TYPE boolean) +qt_commandline_option(sql-mimer TYPE boolean) qt_commandline_option(plugin-sql-db2 TYPE void NAME sql-db2) qt_commandline_option(plugin-sql-ibase TYPE void NAME sql-ibase) qt_commandline_option(plugin-sql-mysql TYPE void NAME sql-mysql) @@ -15,3 +19,4 @@ qt_commandline_option(plugin-sql-oci TYPE void NAME sql-oci) qt_commandline_option(plugin-sql-odbc TYPE void NAME sql-odbc) qt_commandline_option(plugin-sql-psql TYPE void NAME sql-psql) qt_commandline_option(plugin-sql-sqlite TYPE void NAME sql-sqlite) +qt_commandline_option(plugin-sql-mimer TYPE void NAME sql-mimer) diff --git a/src/plugins/sqldrivers/sqlite/CMakeLists.txt b/src/plugins/sqldrivers/sqlite/CMakeLists.txt index aea1c14a42..4203a5c437 100644 --- a/src/plugins/sqldrivers/sqlite/CMakeLists.txt +++ b/src/plugins/sqldrivers/sqlite/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from sqlite.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QSQLiteDriverPlugin Plugin: @@ -9,20 +10,18 @@ qt_internal_add_plugin(QSQLiteDriverPlugin PLUGIN_TYPE sqldrivers SOURCES qsql_sqlite.cpp qsql_sqlite_p.h + qsql_sqlite_vfs.cpp qsql_sqlite_vfs_p.h smain.cpp DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES Qt::Core Qt::CorePrivate Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:sqlite.pro:<TRUE>: -# OTHER_FILES = "sqlite.json" -# QT_FOR_CONFIG = "sqldrivers-private" - ## Scopes: ##################################################################### @@ -31,7 +30,6 @@ qt_internal_extend_target(QSQLiteDriverPlugin CONDITION QT_FEATURE_system_sqlite SQLite::SQLite3 ) -# special case begin if (NOT QT_FEATURE_system_sqlite) # On newer compilers compiling sqlite.c produces warnings qt_disable_warnings(QSQLiteDriverPlugin) @@ -40,7 +38,11 @@ endif() if(QT_FEATURE_system_sqlite) qt_internal_force_macos_intel_arch(QSQLiteDriverPlugin) endif() -# special case end + +qt_internal_extend_target(QSQLiteDriverPlugin CONDITION NOT QT_FEATURE_system_sqlite AND VXWORKS + DEFINES + SQLITE_OS_UNIX=1 +) qt_internal_extend_target(QSQLiteDriverPlugin CONDITION NOT QT_FEATURE_system_sqlite SOURCES @@ -49,8 +51,11 @@ qt_internal_extend_target(QSQLiteDriverPlugin CONDITION NOT QT_FEATURE_system_sq SQLITE_ENABLE_COLUMN_METADATA SQLITE_ENABLE_FTS3 SQLITE_ENABLE_FTS3_PARENTHESIS + SQLITE_ENABLE_FTS4 SQLITE_ENABLE_FTS5 + SQLITE_ENABLE_GEOPOLY SQLITE_ENABLE_JSON1 + SQLITE_ENABLE_MATH_FUNCTIONS SQLITE_ENABLE_RTREE SQLITE_OMIT_COMPLETE INCLUDE_DIRECTORIES diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp index 51a3908867..c574772fd7 100644 --- a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp @@ -7,6 +7,7 @@ #include <qdatetime.h> #include <qdebug.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qsqlerror.h> #include <qsqlfield.h> #include <qsqlindex.h> @@ -38,23 +39,9 @@ Q_DECLARE_METATYPE(sqlite3_stmt*) QT_BEGIN_NAMESPACE -using namespace Qt::StringLiterals; +static Q_LOGGING_CATEGORY(lcSqlite, "qt.sql.sqlite") -static QString _q_escapeIdentifier(const QString &identifier, QSqlDriver::IdentifierType type) -{ - QString res = identifier; - // If it contains [ and ] then we assume it to be escaped properly already as this indicates - // the syntax is exactly how it should be - if (identifier.contains(u'[') && identifier.contains(u']')) - return res; - if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"')) { - res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); - if (type == QSqlDriver::TableName) - res.replace(u'.', "\".\""_L1); - } - return res; -} +using namespace Qt::StringLiterals; static int qGetColumnType(const QString &tpName) { @@ -114,11 +101,64 @@ class QSQLiteDriverPrivate : public QSqlDriverPrivate public: inline QSQLiteDriverPrivate() : QSqlDriverPrivate(QSqlDriver::SQLite) {} + bool isIdentifierEscaped(QStringView identifier) const; + QSqlIndex getTableInfo(QSqlQuery &query, const QString &tableName, + bool onlyPIndex = false) const; + sqlite3 *access = nullptr; QList<QSQLiteResult *> results; QStringList notificationid; }; +bool QSQLiteDriverPrivate::isIdentifierEscaped(QStringView identifier) const +{ + return identifier.size() > 2 + && ((identifier.startsWith(u'"') && identifier.endsWith(u'"')) + || (identifier.startsWith(u'`') && identifier.endsWith(u'`')) + || (identifier.startsWith(u'[') && identifier.endsWith(u']'))); +} + +QSqlIndex QSQLiteDriverPrivate::getTableInfo(QSqlQuery &query, const QString &tableName, + bool onlyPIndex) const +{ + Q_Q(const QSQLiteDriver); + QString schema; + QString table = q->escapeIdentifier(tableName, QSqlDriver::TableName); + const auto indexOfSeparator = table.indexOf(u'.'); + if (indexOfSeparator > -1) { + auto leftName = QStringView{table}.first(indexOfSeparator); + auto rightName = QStringView{table}.sliced(indexOfSeparator + 1); + if (isIdentifierEscaped(leftName) && isIdentifierEscaped(rightName)) { + schema = leftName.toString() + u'.'; + table = rightName.toString(); + } + } + + query.exec("PRAGMA "_L1 + schema + "table_info ("_L1 + table + u')'); + QSqlIndex ind; + while (query.next()) { + bool isPk = query.value(5).toInt(); + if (onlyPIndex && !isPk) + continue; + QString typeName = query.value(2).toString().toLower(); + QString defVal = query.value(4).toString(); + if (!defVal.isEmpty() && defVal.at(0) == u'\'') { + const int end = defVal.lastIndexOf(u'\''); + if (end > 0) + defVal = defVal.mid(1, end - 1); + } + + QSqlField fld(query.value(1).toString(), QMetaType(qGetColumnType(typeName)), tableName); + if (isPk && (typeName == "integer"_L1)) + // INTEGER PRIMARY KEY fields are auto-generated in sqlite + // INT PRIMARY KEY is not the same as INTEGER PRIMARY KEY! + fld.setAutoValue(true); + fld.setRequired(query.value(3).toInt() != 0); + fld.setDefaultValue(defVal); + ind.append(fld); + } + return ind; +} class QSQLiteResultPrivate : public QSqlCachedResultPrivate { @@ -158,7 +198,7 @@ void QSQLiteResultPrivate::finalize() return; sqlite3_finalize(stmt); - stmt = 0; + stmt = nullptr; } void QSQLiteResultPrivate::initColumns(bool emptyResultset) @@ -210,7 +250,6 @@ void QSQLiteResultPrivate::initColumns(bool emptyResultset) } QSqlField fld(colName, QMetaType(fieldType), tableName); - fld.setSqlType(stp); rInf.append(fld); } } @@ -223,7 +262,7 @@ bool QSQLiteResultPrivate::fetchNext(QSqlCachedResult::ValueCache &values, int i // already fetched Q_ASSERT(!initialFetch); skipRow = false; - for(int i=0;i<firstRow.count();i++) + for(int i=0;i<firstRow.size();i++) values[i]=firstRow[i]; return skippedStatus; } @@ -382,10 +421,10 @@ bool QSQLiteResult::execBatch(bool arrayBind) Q_D(QSqlResult); QScopedValueRollback<QList<QVariant>> valuesScope(d->values); QList<QVariant> values = d->values; - if (values.count() == 0) + if (values.size() == 0) return false; - for (int i = 0; i < values.at(0).toList().count(); ++i) { + for (int i = 0; i < values.at(0).toList().size(); ++i) { d->values.clear(); QScopedValueRollback<QHash<QString, QList<int>>> indexesScope(d->indexes); auto it = d->indexes.constBegin(); @@ -419,16 +458,16 @@ bool QSQLiteResult::exec() } int paramCount = sqlite3_bind_parameter_count(d->stmt); - bool paramCountIsValid = paramCount == values.count(); + bool paramCountIsValid = paramCount == values.size(); #if (SQLITE_VERSION_NUMBER >= 3003011) // In the case of the reuse of a named placeholder // We need to check explicitly that paramCount is greater than or equal to 1, as sqlite // can end up in a case where for virtual tables it returns 0 even though it // has parameters - if (paramCount >= 1 && paramCount < values.count()) { + if (paramCount >= 1 && paramCount < values.size()) { const auto countIndexes = [](int counter, const QList<int> &indexList) { - return counter + indexList.length(); + return counter + indexList.size(); }; const int bindParamCount = std::accumulate(d->indexes.cbegin(), @@ -436,7 +475,7 @@ bool QSQLiteResult::exec() 0, countIndexes); - paramCountIsValid = bindParamCount == values.count(); + paramCountIsValid = bindParamCount == values.size(); // When using named placeholders, it will reuse the index for duplicated // placeholders. So we need to ensure the QList has only one instance of // each value as SQLite will do the rest for us. @@ -625,6 +664,30 @@ static void _q_regexp_cleanup(void *cache) } #endif +static void _q_lower(sqlite3_context* context, int argc, sqlite3_value** argv) +{ + if (Q_UNLIKELY(argc != 1)) { + sqlite3_result_text(context, nullptr, 0, nullptr); + return; + } + const QString lower = QString::fromUtf8( + reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))).toLower(); + const QByteArray ba = lower.toUtf8(); + sqlite3_result_text(context, ba.data(), ba.size(), SQLITE_TRANSIENT); +} + +static void _q_upper(sqlite3_context* context, int argc, sqlite3_value** argv) +{ + if (Q_UNLIKELY(argc != 1)) { + sqlite3_result_text(context, nullptr, 0, nullptr); + return; + } + const QString upper = QString::fromUtf8( + reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))).toUpper(); + const QByteArray ba = upper.toUtf8(); + sqlite3_result_text(context, ba.data(), ba.size(), SQLITE_TRANSIENT); +} + QSQLiteDriver::QSQLiteDriver(QObject * parent) : QSqlDriver(*new QSQLiteDriverPrivate, parent) { @@ -691,13 +754,16 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c bool openReadOnlyOption = false; bool openUriOption = false; bool useExtendedResultCodes = true; + bool useQtVfs = false; + bool useQtCaseFolding = false; + bool openNoFollow = false; #if QT_CONFIG(regularexpression) static const auto regexpConnectOption = "QSQLITE_ENABLE_REGEXP"_L1; bool defineRegexp = false; int regexpCacheSize = 25; #endif - const auto opts = QStringView{conOpts}.split(u';'); + const auto opts = QStringView{conOpts}.split(u';', Qt::SkipEmptyParts); for (auto option : opts) { option = option.trimmed(); if (option.startsWith("QSQLITE_BUSY_TIMEOUT"_L1)) { @@ -708,6 +774,8 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c if (ok) timeOut = nt; } + } else if (option == "QSQLITE_USE_QT_VFS"_L1) { + useQtVfs = true; } else if (option == "QSQLITE_OPEN_READONLY"_L1) { openReadOnlyOption = true; } else if (option == "QSQLITE_OPEN_URI"_L1) { @@ -716,6 +784,10 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c sharedCache = true; } else if (option == "QSQLITE_NO_USE_EXTENDED_RESULT_CODES"_L1) { useExtendedResultCodes = false; + } else if (option == "QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING"_L1) { + useQtCaseFolding = true; + } else if (option == "QSQLITE_OPEN_NOFOLLOW"_L1) { + openNoFollow = true; } #if QT_CONFIG(regularexpression) else if (option.startsWith(regexpConnectOption)) { @@ -733,16 +805,25 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c } } #endif + else + qCWarning(lcSqlite, "Unsupported option '%ls'", qUtf16Printable(option.toString())); } int openMode = (openReadOnlyOption ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)); openMode |= (sharedCache ? SQLITE_OPEN_SHAREDCACHE : SQLITE_OPEN_PRIVATECACHE); if (openUriOption) openMode |= SQLITE_OPEN_URI; + if (openNoFollow) { +#if defined(SQLITE_OPEN_NOFOLLOW) + openMode |= SQLITE_OPEN_NOFOLLOW; +#else + qCWarning(lcSqlite, "SQLITE_OPEN_NOFOLLOW not supported with the SQLite version %s", sqlite3_libversion()); +#endif + } openMode |= SQLITE_OPEN_NOMUTEX; - const int res = sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, nullptr); + const int res = sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, useQtVfs ? "QtVFS" : nullptr); if (res == SQLITE_OK) { sqlite3_busy_timeout(d->access, timeOut); @@ -757,6 +838,12 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c nullptr, &_q_regexp_cleanup); } #endif + if (useQtCaseFolding) { + sqlite3_create_function_v2(d->access, "lower", 1, SQLITE_UTF8, nullptr, + &_q_lower, nullptr, nullptr, nullptr); + sqlite3_create_function_v2(d->access, "upper", 1, SQLITE_UTF8, nullptr, + &_q_upper, nullptr, nullptr, nullptr); + } return true; } else { setLastError(qMakeError(d->access, tr("Error opening database"), @@ -765,7 +852,7 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c if (d->access) { sqlite3_close(d->access); - d->access = 0; + d->access = nullptr; } return false; @@ -776,10 +863,10 @@ void QSQLiteDriver::close() { Q_D(QSQLiteDriver); if (isOpen()) { - for (QSQLiteResult *result : qAsConst(d->results)) + for (QSQLiteResult *result : std::as_const(d->results)) result->d_func()->finalize(); - if (d->access && (d->notificationid.count() > 0)) { + if (d->access && (d->notificationid.size() > 0)) { d->notificationid.clear(); sqlite3_update_hook(d->access, nullptr, nullptr); } @@ -788,7 +875,7 @@ void QSQLiteDriver::close() if (res != SQLITE_OK) setLastError(qMakeError(d->access, tr("Error closing database"), QSqlError::ConnectionError, res)); - d->access = 0; + d->access = nullptr; setOpen(false); setOpenError(false); } @@ -877,79 +964,26 @@ QStringList QSQLiteDriver::tables(QSql::TableType type) const return res; } -static QSqlIndex qGetTableInfo(QSqlQuery &q, const QString &tableName, bool onlyPIndex = false) -{ - QString schema; - QString table(tableName); - const qsizetype indexOfSeparator = tableName.indexOf(u'.'); - if (indexOfSeparator > -1) { - const qsizetype indexOfCloseBracket = tableName.indexOf(u']'); - if (indexOfCloseBracket != tableName.size() - 1) { - // Handles a case like databaseName.tableName - schema = tableName.left(indexOfSeparator + 1); - table = tableName.mid(indexOfSeparator + 1); - } else { - const qsizetype indexOfOpenBracket = tableName.lastIndexOf(u'[', indexOfCloseBracket); - if (indexOfOpenBracket > 0) { - // Handles a case like databaseName.[tableName] - schema = tableName.left(indexOfOpenBracket); - table = tableName.mid(indexOfOpenBracket); - } - } - } - q.exec("PRAGMA "_L1 + schema + "table_info ("_L1 + - _q_escapeIdentifier(table, QSqlDriver::TableName) + u')'); - QSqlIndex ind; - while (q.next()) { - bool isPk = q.value(5).toInt(); - if (onlyPIndex && !isPk) - continue; - QString typeName = q.value(2).toString().toLower(); - QString defVal = q.value(4).toString(); - if (!defVal.isEmpty() && defVal.at(0) == u'\'') { - const int end = defVal.lastIndexOf(u'\''); - if (end > 0) - defVal = defVal.mid(1, end - 1); - } - - QSqlField fld(q.value(1).toString(), QMetaType(qGetColumnType(typeName)), tableName); - if (isPk && (typeName == "integer"_L1)) - // INTEGER PRIMARY KEY fields are auto-generated in sqlite - // INT PRIMARY KEY is not the same as INTEGER PRIMARY KEY! - fld.setAutoValue(true); - fld.setRequired(q.value(3).toInt() != 0); - fld.setDefaultValue(defVal); - ind.append(fld); - } - return ind; -} - -QSqlIndex QSQLiteDriver::primaryIndex(const QString &tblname) const +QSqlIndex QSQLiteDriver::primaryIndex(const QString &tablename) const { + Q_D(const QSQLiteDriver); if (!isOpen()) return QSqlIndex(); - QString table = tblname; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - QSqlQuery q(createResult()); q.setForwardOnly(true); - return qGetTableInfo(q, table, true); + return d->getTableInfo(q, tablename, true); } -QSqlRecord QSQLiteDriver::record(const QString &tbl) const +QSqlRecord QSQLiteDriver::record(const QString &tablename) const { + Q_D(const QSQLiteDriver); if (!isOpen()) return QSqlRecord(); - QString table = tbl; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - QSqlQuery q(createResult()); q.setForwardOnly(true); - return qGetTableInfo(q, table); + return d->getTableInfo(q, tablename); } QVariant QSQLiteDriver::handle() const @@ -960,7 +994,52 @@ QVariant QSQLiteDriver::handle() const QString QSQLiteDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const { - return _q_escapeIdentifier(identifier, type); + Q_D(const QSQLiteDriver); + if (identifier.isEmpty() || isIdentifierEscaped(identifier, type)) + return identifier; + + const auto indexOfSeparator = identifier.indexOf(u'.'); + if (indexOfSeparator > -1) { + auto leftName = QStringView{identifier}.first(indexOfSeparator); + auto rightName = QStringView{identifier}.sliced(indexOfSeparator + 1); + const QStringView leftEnclose = d->isIdentifierEscaped(leftName) ? u"" : u"\""; + const QStringView rightEnclose = d->isIdentifierEscaped(rightName) ? u"" : u"\""; + if (leftEnclose.isEmpty() || rightEnclose.isEmpty()) + return (leftEnclose + leftName + leftEnclose + u'.' + rightEnclose + rightName + + rightEnclose); + } + return u'"' + identifier + u'"'; +} + +bool QSQLiteDriver::isIdentifierEscaped(const QString &identifier, IdentifierType type) const +{ + Q_D(const QSQLiteDriver); + Q_UNUSED(type); + return d->isIdentifierEscaped(QStringView{identifier}); +} + +QString QSQLiteDriver::stripDelimiters(const QString &identifier, IdentifierType type) const +{ + Q_D(const QSQLiteDriver); + const auto indexOfSeparator = identifier.indexOf(u'.'); + if (indexOfSeparator > -1) { + auto leftName = QStringView{identifier}.first(indexOfSeparator); + auto rightName = QStringView{identifier}.sliced(indexOfSeparator + 1); + const auto leftEscaped = d->isIdentifierEscaped(leftName); + const auto rightEscaped = d->isIdentifierEscaped(rightName); + if (leftEscaped || rightEscaped) { + if (leftEscaped) + leftName = leftName.sliced(1).chopped(1); + if (rightEscaped) + rightName = rightName.sliced(1).chopped(1); + return leftName + u'.' + rightName; + } + } + + if (isIdentifierEscaped(identifier, type)) + return identifier.mid(1, identifier.size() - 2); + + return identifier; } static void handle_sqlite_callback(void *qobj,int aoperation, char const *adbname, char const *atablename, @@ -979,18 +1058,19 @@ bool QSQLiteDriver::subscribeToNotification(const QString &name) { Q_D(QSQLiteDriver); if (!isOpen()) { - qWarning("Database not open."); + qCWarning(lcSqlite, "QSQLiteDriver::subscribeToNotification: Database not open."); return false; } if (d->notificationid.contains(name)) { - qWarning("Already subscribing to '%s'.", qPrintable(name)); + qCWarning(lcSqlite, "QSQLiteDriver::subscribeToNotification: Already subscribing to '%ls'.", + qUtf16Printable(name)); return false; } //sqlite supports only one notification callback, so only the first is registered d->notificationid << name; - if (d->notificationid.count() == 1) + if (d->notificationid.size() == 1) sqlite3_update_hook(d->access, &handle_sqlite_callback, reinterpret_cast<void *> (this)); return true; @@ -1000,12 +1080,13 @@ bool QSQLiteDriver::unsubscribeFromNotification(const QString &name) { Q_D(QSQLiteDriver); if (!isOpen()) { - qWarning("Database not open."); + qCWarning(lcSqlite, "QSQLiteDriver::unsubscribeFromNotification: Database not open."); return false; } if (!d->notificationid.contains(name)) { - qWarning("Not subscribed to '%s'.", qPrintable(name)); + qCWarning(lcSqlite, "QSQLiteDriver::unsubscribeFromNotification: Not subscribed to '%ls'.", + qUtf16Printable(name)); return false; } diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h b/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h index 53ffb45f96..db6b76cb69 100644 --- a/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h @@ -54,9 +54,12 @@ public: QStringList tables(QSql::TableType) const override; QSqlRecord record(const QString& tablename) const override; - QSqlIndex primaryIndex(const QString &table) const override; + QSqlIndex primaryIndex(const QString &tablename) const override; QVariant handle() const override; - QString escapeIdentifier(const QString &identifier, IdentifierType) const override; + + QString escapeIdentifier(const QString &identifier, IdentifierType type) const override; + bool isIdentifierEscaped(const QString &identifier, IdentifierType type) const override; + QString stripDelimiters(const QString &identifier, IdentifierType type) const override; bool subscribeToNotification(const QString &name) override; bool unsubscribeFromNotification(const QString &name) override; diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp new file mode 100644 index 0000000000..bbba3cd14f --- /dev/null +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp @@ -0,0 +1,258 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsql_sqlite_vfs_p.h" + +#include <QFile> + +#include <limits.h> // defines PATH_MAX on unix +#include <sqlite3.h> +#include <stdio.h> // defines FILENAME_MAX everywhere + +#ifndef PATH_MAX +# define PATH_MAX FILENAME_MAX +#endif +#if SQLITE_VERSION_NUMBER < 3040000 +typedef const char *sqlite3_filename; +#endif + +namespace { +struct Vfs : sqlite3_vfs { + sqlite3_vfs *pVfs; + sqlite3_io_methods ioMethods; +}; + +struct File : sqlite3_file { + class QtFile : public QFile { + public: + QtFile(const QString &name, bool removeOnClose) + : QFile(name) + , removeOnClose(removeOnClose) + {} + + ~QtFile() override + { + if (removeOnClose) + remove(); + } + private: + bool removeOnClose; + }; + QtFile *pFile; +}; + + +int xClose(sqlite3_file *sfile) +{ + auto file = static_cast<File *>(sfile); + delete file->pFile; + file->pFile = nullptr; + return SQLITE_OK; +} + +int xRead(sqlite3_file *sfile, void *ptr, int iAmt, sqlite3_int64 iOfst) +{ + auto file = static_cast<File *>(sfile); + if (!file->pFile->seek(iOfst)) + return SQLITE_IOERR_READ; + + auto sz = file->pFile->read(static_cast<char *>(ptr), iAmt); + if (sz < iAmt) { + memset(static_cast<char *>(ptr) + sz, 0, size_t(iAmt - sz)); + return SQLITE_IOERR_SHORT_READ; + } + return SQLITE_OK; +} + +int xWrite(sqlite3_file *sfile, const void *data, int iAmt, sqlite3_int64 iOfst) +{ + auto file = static_cast<File *>(sfile); + if (!file->pFile->seek(iOfst)) + return SQLITE_IOERR_SEEK; + return file->pFile->write(reinterpret_cast<const char*>(data), iAmt) == iAmt ? SQLITE_OK : SQLITE_IOERR_WRITE; +} + +int xTruncate(sqlite3_file *sfile, sqlite3_int64 size) +{ + auto file = static_cast<File *>(sfile); + return file->pFile->resize(size) ? SQLITE_OK : SQLITE_IOERR_TRUNCATE; +} + +int xSync(sqlite3_file *sfile, int /*flags*/) +{ + static_cast<File *>(sfile)->pFile->flush(); + return SQLITE_OK; +} + +int xFileSize(sqlite3_file *sfile, sqlite3_int64 *pSize) +{ + auto file = static_cast<File *>(sfile); + *pSize = file->pFile->size(); + return SQLITE_OK; +} + +// No lock/unlock for QFile, QLockFile doesn't work for me + +int xLock(sqlite3_file *, int) { return SQLITE_OK; } + +int xUnlock(sqlite3_file *, int) { return SQLITE_OK; } + +int xCheckReservedLock(sqlite3_file *, int *pResOut) +{ + *pResOut = 0; + return SQLITE_OK; +} + +int xFileControl(sqlite3_file *, int, void *) { return SQLITE_NOTFOUND; } + +int xSectorSize(sqlite3_file *) +{ + return 4096; +} + +int xDeviceCharacteristics(sqlite3_file *) +{ + return 0; // no SQLITE_IOCAP_XXX +} + +int xOpen(sqlite3_vfs *svfs, sqlite3_filename zName, sqlite3_file *sfile, + int flags, int *pOutFlags) +{ + auto vfs = static_cast<Vfs *>(svfs); + auto file = static_cast<File *>(sfile); + memset(file, 0, sizeof(File)); + QIODeviceBase::OpenMode mode = QIODeviceBase::NotOpen; + if (!zName || (flags & SQLITE_OPEN_MEMORY)) + return SQLITE_PERM; + if ((flags & SQLITE_OPEN_READONLY) && + !(flags & SQLITE_OPEN_READWRITE) && + !(flags & SQLITE_OPEN_CREATE) && + !(flags & SQLITE_OPEN_DELETEONCLOSE)) { + mode |= QIODeviceBase::OpenModeFlag::ReadOnly; + } else { + /* + ** ^The [SQLITE_OPEN_EXCLUSIVE] flag is always used in conjunction + ** with the [SQLITE_OPEN_CREATE] flag, which are both directly + ** analogous to the O_EXCL and O_CREAT flags of the POSIX open() + ** API. The SQLITE_OPEN_EXCLUSIVE flag, when paired with the + ** SQLITE_OPEN_CREATE, is used to indicate that file should always + ** be created, and that it is an error if it already exists. + ** It is <i>not</i> used to indicate the file should be opened + ** for exclusive access. + */ + if ((flags & SQLITE_OPEN_CREATE) && (flags & SQLITE_OPEN_EXCLUSIVE)) + mode |= QIODeviceBase::OpenModeFlag::NewOnly; + + if (flags & SQLITE_OPEN_READWRITE) + mode |= QIODeviceBase::OpenModeFlag::ReadWrite; + } + + file->pMethods = &vfs->ioMethods; + file->pFile = new File::QtFile(QString::fromUtf8(zName), bool(flags & SQLITE_OPEN_DELETEONCLOSE)); + if (!file->pFile->open(mode)) + return SQLITE_CANTOPEN; + if (pOutFlags) + *pOutFlags = flags; + + return SQLITE_OK; +} + +int xDelete(sqlite3_vfs *, const char *zName, int) +{ + return QFile::remove(QString::fromUtf8(zName)) ? SQLITE_OK : SQLITE_ERROR; +} + +int xAccess(sqlite3_vfs */*svfs*/, const char *zName, int flags, int *pResOut) +{ + *pResOut = 0; + switch (flags) { + case SQLITE_ACCESS_EXISTS: + case SQLITE_ACCESS_READ: + *pResOut = QFile::exists(QString::fromUtf8(zName)); + break; + default: + break; + } + return SQLITE_OK; +} + +int xFullPathname(sqlite3_vfs *, const char *zName, int nOut, char *zOut) +{ + if (!zName) + return SQLITE_ERROR; + + int i = 0; + for (;zName[i] && i < nOut; ++i) + zOut[i] = zName[i]; + + if (i >= nOut) + return SQLITE_ERROR; + + zOut[i] = '\0'; + return SQLITE_OK; +} + +int xRandomness(sqlite3_vfs *svfs, int nByte, char *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xRandomness(vfs, nByte, zOut); +} + +int xSleep(sqlite3_vfs *svfs, int microseconds) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xSleep(vfs, microseconds); +} + +int xCurrentTime(sqlite3_vfs *svfs, double *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xCurrentTime(vfs, zOut); +} + +int xGetLastError(sqlite3_vfs *, int, char *) +{ + return 0; +} + +int xCurrentTimeInt64(sqlite3_vfs *svfs, sqlite3_int64 *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xCurrentTimeInt64(vfs, zOut); +} +} // namespace { + +void register_qt_vfs() +{ + static Vfs vfs; + memset(&vfs, 0, sizeof(Vfs)); + vfs.iVersion = 1; + vfs.szOsFile = sizeof(File); + vfs.mxPathname = PATH_MAX; + vfs.zName = "QtVFS"; + vfs.xOpen = &xOpen; + vfs.xDelete = &xDelete; + vfs.xAccess = &xAccess; + vfs.xFullPathname = &xFullPathname; + vfs.xRandomness = &xRandomness; + vfs.xSleep = &xSleep; + vfs.xCurrentTime = &xCurrentTime; + vfs.xGetLastError = &xGetLastError; + vfs.xCurrentTimeInt64 = &xCurrentTimeInt64; + vfs.pVfs = sqlite3_vfs_find(nullptr); + vfs.ioMethods.iVersion = 1; + vfs.ioMethods.xClose = &xClose; + vfs.ioMethods.xRead = &xRead; + vfs.ioMethods.xWrite = &xWrite; + vfs.ioMethods.xTruncate = &xTruncate; + vfs.ioMethods.xSync = &xSync; + vfs.ioMethods.xFileSize = &xFileSize; + vfs.ioMethods.xLock = &xLock; + vfs.ioMethods.xUnlock = &xUnlock; + vfs.ioMethods.xCheckReservedLock = &xCheckReservedLock; + vfs.ioMethods.xFileControl = &xFileControl; + vfs.ioMethods.xSectorSize = &xSectorSize; + vfs.ioMethods.xDeviceCharacteristics = &xDeviceCharacteristics; + + sqlite3_vfs_register(&vfs, 0); +} diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h new file mode 100644 index 0000000000..56024b3ecb --- /dev/null +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSQL_SQLITE_VFS_H +#define QSQL_SQLITE_VFS_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +void register_qt_vfs(); + + +#endif // QSQL_SQLITE_VFS_H diff --git a/src/plugins/sqldrivers/sqlite/smain.cpp b/src/plugins/sqldrivers/sqlite/smain.cpp index 9223cca850..0d201c38d3 100644 --- a/src/plugins/sqldrivers/sqlite/smain.cpp +++ b/src/plugins/sqldrivers/sqlite/smain.cpp @@ -4,6 +4,7 @@ #include <qsqldriverplugin.h> #include <qstringlist.h> #include "qsql_sqlite_p.h" +#include "qsql_sqlite_vfs_p.h" QT_BEGIN_NAMESPACE @@ -23,6 +24,7 @@ public: QSQLiteDriverPlugin::QSQLiteDriverPlugin() : QSqlDriverPlugin() { + register_qt_vfs(); } QSqlDriver* QSQLiteDriverPlugin::create(const QString &name) @@ -31,7 +33,8 @@ QSqlDriver* QSQLiteDriverPlugin::create(const QString &name) QSQLiteDriver* driver = new QSQLiteDriver(); return driver; } - return 0; + + return nullptr; } QT_END_NAMESPACE diff --git a/src/plugins/styles/CMakeLists.txt b/src/plugins/styles/CMakeLists.txt index 73329915f6..b809042c14 100644 --- a/src/plugins/styles/CMakeLists.txt +++ b/src/plugins/styles/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from styles.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause if(QT_FEATURE_style_android) add_subdirectory(android) @@ -7,5 +8,5 @@ if(QT_FEATURE_style_mac) add_subdirectory(mac) endif() if(QT_FEATURE_style_windowsvista) - add_subdirectory(windowsvista) + add_subdirectory(modernwindows) endif() diff --git a/src/plugins/styles/android/CMakeLists.txt b/src/plugins/styles/android/CMakeLists.txt index 3dc0ed1e2e..b8bda6fd5f 100644 --- a/src/plugins/styles/android/CMakeLists.txt +++ b/src/plugins/styles/android/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from android.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QAndroidStylePlugin Plugin: @@ -15,6 +16,3 @@ qt_internal_add_plugin(QAndroidStylePlugin Qt::Gui Qt::WidgetsPrivate ) - -#### Keys ignored in scope 1:.:.:android.pro:<TRUE>: -# DISTFILES = "androidstyle.json" diff --git a/src/plugins/styles/android/qandroidstyle.cpp b/src/plugins/styles/android/qandroidstyle.cpp index 5acdb07520..64940ed4d8 100644 --- a/src/plugins/styles/android/qandroidstyle.cpp +++ b/src/plugins/styles/android/qandroidstyle.cpp @@ -1134,7 +1134,7 @@ QAndroidStyle::AndroidStateDrawable::AndroidStateDrawable(const QVariantMap &dra QAndroidStyle::AndroidStateDrawable::~AndroidStateDrawable() { - for (const StateType &type : qAsConst(m_states)) + for (const StateType &type : std::as_const(m_states)) delete type.second; } @@ -1209,7 +1209,7 @@ const QAndroidStyle::AndroidDrawable * QAndroidStyle::AndroidStateDrawable::best int QAndroidStyle::AndroidStateDrawable::extractState(const QVariantMap &value) { - QStyle::State state = QStyle::State_Enabled | QStyle::State_Active;; + QStyle::State state = QStyle::State_Enabled | QStyle::State_Active; for (auto it = value.cbegin(), end = value.cend(); it != end; ++it) { const QString &key = it.key(); bool val = it.value().toString() == QLatin1String("true"); @@ -1259,7 +1259,7 @@ int QAndroidStyle::AndroidStateDrawable::extractState(const QVariantMap &value) void QAndroidStyle::AndroidStateDrawable::setPaddingLeftToSizeWidth() { - for (const StateType &type : qAsConst(m_states)) + for (const StateType &type : std::as_const(m_states)) const_cast<AndroidDrawable *>(type.second)->setPaddingLeftToSizeWidth(); } @@ -1285,7 +1285,7 @@ QAndroidStyle::AndroidLayerDrawable::AndroidLayerDrawable(const QVariantMap &dra QAndroidStyle::AndroidLayerDrawable::~AndroidLayerDrawable() { - for (const LayerType &layer : qAsConst(m_layers)) + for (const LayerType &layer : std::as_const(m_layers)) delete layer.second; } diff --git a/src/plugins/styles/mac/CMakeLists.txt b/src/plugins/styles/mac/CMakeLists.txt index 98d6791d82..9dc8d01cc7 100644 --- a/src/plugins/styles/mac/CMakeLists.txt +++ b/src/plugins/styles/mac/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from mac.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QMacStylePlugin Plugin: @@ -18,12 +19,7 @@ qt_internal_add_plugin(QMacStylePlugin Qt::WidgetsPrivate ) -#### Keys ignored in scope 1:.:.:mac.pro:<TRUE>: -# DISTFILES = "macstyle.json" - -# special case begin set_target_properties(QMacStylePlugin PROPERTIES DISABLE_PRECOMPILE_HEADERS ON ) -# special case end diff --git a/src/plugins/styles/mac/main.mm b/src/plugins/styles/mac/main.mm index 15808754ac..5f4fbd9eb8 100644 --- a/src/plugins/styles/mac/main.mm +++ b/src/plugins/styles/mac/main.mm @@ -4,6 +4,8 @@ #include <QtWidgets/qstyleplugin.h> #include "qmacstyle_mac_p.h" +#include <QtCore/private/qcore_mac_p.h> + QT_BEGIN_NAMESPACE class QMacStylePlugin : public QStylePlugin diff --git a/src/plugins/styles/mac/qmacstyle_mac.mm b/src/plugins/styles/mac/qmacstyle_mac.mm index bc62d50d6d..3f57f284e6 100644 --- a/src/plugins/styles/mac/qmacstyle_mac.mm +++ b/src/plugins/styles/mac/qmacstyle_mac.mm @@ -160,46 +160,7 @@ const int pushButtonBevelRectOffsets[3] = { QVector<QPointer<QObject> > QMacStylePrivate::scrollBars; -bool isDarkMode() { return QGuiApplicationPrivate::platformTheme()->appearance() == QPlatformTheme::Appearance::Dark; } - -// Title bar gradient colors for Lion were determined by inspecting PSDs exported -// using CoreUI's CoreThemeDocument; there is no public API to retrieve them - -static QLinearGradient titlebarGradientActive() -{ - static QLinearGradient darkGradient = [](){ - QLinearGradient gradient; - // FIXME: colors are chosen somewhat arbitrarily and could be fine-tuned, - // or ideally determined by calling a native API. - gradient.setColorAt(0, QColor(47, 47, 47)); - return gradient; - }(); - static QLinearGradient lightGradient = [](){ - QLinearGradient gradient; - gradient.setColorAt(0, QColor(235, 235, 235)); - gradient.setColorAt(0.5, QColor(210, 210, 210)); - gradient.setColorAt(0.75, QColor(195, 195, 195)); - gradient.setColorAt(1, QColor(180, 180, 180)); - return gradient; - }(); - return isDarkMode() ? darkGradient : lightGradient; -} - -static QLinearGradient titlebarGradientInactive() -{ - static QLinearGradient darkGradient = [](){ - QLinearGradient gradient; - gradient.setColorAt(1, QColor(42, 42, 42)); - return gradient; - }(); - static QLinearGradient lightGradient = [](){ - QLinearGradient gradient; - gradient.setColorAt(0, QColor(250, 250, 250)); - gradient.setColorAt(1, QColor(225, 225, 225)); - return gradient; - }(); - return isDarkMode() ? darkGradient : lightGradient; -} +bool isDarkMode() { return QGuiApplicationPrivate::platformTheme()->colorScheme() == Qt::ColorScheme::Dark; } #if QT_CONFIG(tabwidget) /* @@ -385,7 +346,6 @@ class AppearanceSync { public: AppearanceSync() { -#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_14) if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSMojave && !isDarkMode()) { auto requiredAppearanceName = NSApplication.sharedApplication.effectiveAppearance.name; @@ -394,7 +354,6 @@ public: NSAppearance.currentAppearance = [NSAppearance appearanceNamed:requiredAppearanceName]; } } -#endif // QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_14) } ~AppearanceSync() @@ -431,10 +390,7 @@ static bool setupSlider(NSSlider *slider, const QStyleOptionSlider *sl) if (sl->minimum >= sl->maximum) return false; - // NSSlider seems to cache values based on tracking and the last layout of the - // NSView, resulting in incorrect knob rects that break the interaction with - // multiple sliders. So completely reinitialize the slider. - [slider initWithFrame:sl->rect.toCGRect()]; + slider.frame = sl->rect.toCGRect(); slider.minValue = sl->minimum; slider.maxValue = sl->maximum; @@ -465,14 +421,6 @@ static bool setupSlider(NSSlider *slider, const QStyleOptionSlider *sl) // the cell for its metrics and to draw itself. [slider layoutSubtreeIfNeeded]; - if (sl->state & QStyle::State_Sunken) { - const CGRect knobRect = [slider.cell knobRectFlipped:slider.isFlipped]; - CGPoint pressPoint; - pressPoint.x = CGRectGetMidX(knobRect); - pressPoint.y = CGRectGetMidY(knobRect); - [slider.cell startTrackingAt:pressPoint inView:slider]; - } - return true; } @@ -1794,10 +1742,6 @@ QRectF QMacStylePrivate::comboboxEditBounds(const QRectF &outerBounds, const Coc QMacStylePrivate::QMacStylePrivate() : backingStoreNSView(nil) { - if (auto *ssf = QGuiApplicationPrivate::platformTheme()->font(QPlatformTheme::SmallFont)) - smallSystemFont = *ssf; - if (auto *msf = QGuiApplicationPrivate::platformTheme()->font(QPlatformTheme::MiniFont)) - miniSystemFont = *msf; } QMacStylePrivate::~QMacStylePrivate() @@ -1815,13 +1759,9 @@ NSView *QMacStylePrivate::cocoaControl(CocoaControl widget) const || widget.size == QStyleHelper::SizeDefault) return nil; - if (widget.type == Box) { - if (__builtin_available(macOS 10.14, *)) { - if (isDarkMode()) { - // See render code in drawPrimitive(PE_FrameTabWidget) - widget.type = Box_Dark; - } - } + if (widget.type == Box && isDarkMode()) { + // See render code in drawPrimitive(PE_FrameTabWidget) + widget.type = Box_Dark; } NSView *bv = cocoaControls.value(widget, nil); @@ -2066,7 +2006,6 @@ QMacStyle::QMacStyle() QCoreApplication::sendEvent(o, &event); }); -#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_14) Q_D(QMacStyle); // FIXME: Tie this logic into theme change, or even polish/unpolish if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSMojave) { @@ -2077,7 +2016,6 @@ QMacStyle::QMacStyle() d->cocoaControls.clear(); }); } -#endif } QMacStyle::~QMacStyle() @@ -2098,6 +2036,14 @@ void QMacStyle::unpolish(QApplication *) void QMacStyle::polish(QWidget* w) { + Q_D(QMacStyle); + if (!d->smallSystemFont && QGuiApplicationPrivate::platformTheme()) { + if (auto *ssf = QGuiApplicationPrivate::platformTheme()->font(QPlatformTheme::SmallFont)) + d->smallSystemFont = *ssf; + else + d->smallSystemFont = QFont(); + } + if (false #if QT_CONFIG(menu) || qobject_cast<QMenu*>(w) @@ -2210,25 +2156,6 @@ int QMacStyle::pixelMetric(PixelMetric metric, const QStyleOption *opt, const QW case PM_FocusFrameHMargin: ret = qt_mac_aqua_get_metric(FocusRectOutset); break; - case PM_DialogButtonsSeparator: - ret = -5; - break; - case PM_DialogButtonsButtonHeight: { - QSize sz; - ret = d->aquaSizeConstrain(opt, 0, QStyle::CT_PushButton, QSize(-1, -1), &sz); - if (sz == QSize(-1, -1)) - ret = 32; - else - ret = sz.height(); - break; } - case PM_DialogButtonsButtonWidth: { - QSize sz; - ret = d->aquaSizeConstrain(opt, 0, QStyle::CT_PushButton, QSize(-1, -1), &sz); - if (sz == QSize(-1, -1)) - ret = 70; - else - ret = sz.width(); - break; } case PM_MenuBarHMargin: ret = 8; @@ -2555,10 +2482,13 @@ int QMacStyle::pixelMetric(PixelMetric metric, const QStyleOption *opt, const QW case PM_ToolBarFrameWidth: ret = 1; break; - case PM_ScrollView_ScrollBarOverlap: - ret = [NSScroller preferredScrollerStyle] == NSScrollerStyleOverlay ? - pixelMetric(PM_ScrollBarExtent, opt, widget) : 0; + case PM_ScrollView_ScrollBarOverlap: { + const QStyle *realStyle = widget ? widget->style() : proxy(); + ret = realStyle->styleHint(SH_ScrollBar_Transient, opt, widget) + ? realStyle->pixelMetric(PM_ScrollBarExtent, opt, widget) + : 0; break; + } default: ret = QCommonStyle::pixelMetric(metric, opt, widget); break; @@ -3286,7 +3216,7 @@ void QMacStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPai NSButtonCell *triangleCell = static_cast<NSButtonCell *>(d->cocoaCell(cw)); [triangleCell setState:(opt->state & State_Open) ? NSControlStateValueOn : NSControlStateValueOff]; bool viewHasFocus = (w && w->hasFocus()) || (opt->state & State_HasFocus); - [triangleCell setBackgroundStyle:((opt->state & State_Selected) && viewHasFocus) ? NSBackgroundStyleDark : NSBackgroundStyleLight]; + [triangleCell setBackgroundStyle:((opt->state & State_Selected) && viewHasFocus) ? NSBackgroundStyleEmphasized : NSBackgroundStyleNormal]; d->setupNSGraphicsContext(cg, NO); @@ -3423,17 +3353,7 @@ void QMacStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPai } break; #endif // QT_CONFIG(tabbar) case PE_PanelStatusBar: { - // Fill the status bar with the titlebar gradient. - QLinearGradient linearGrad; - if (w ? qt_macWindowMainWindow(w->window()) : (opt->state & QStyle::State_Active)) { - linearGrad = titlebarGradientActive(); - } else { - linearGrad = titlebarGradientInactive(); - } - - linearGrad.setStart(0, opt->rect.top()); - linearGrad.setFinalStop(0, opt->rect.bottom()); - p->fillRect(opt->rect, linearGrad); + p->fillRect(opt->rect, opt->palette.window()); // Draw the black separator line at the top of the status bar. if (w ? qt_macWindowMainWindow(w->window()) : (opt->state & QStyle::State_Active)) @@ -3914,6 +3834,7 @@ void QMacStyle::drawControl(ControlElement ce, const QStyleOption *opt, QPainter frameRect = frameRect.adjusted(-1, 0, 1, 0); } break; + case QStyleOptionTab::Moving: // Moving tab treated like End case QStyleOptionTab::End: // Pressed state hack: tweak adjustments in preparation for flip below if (isSelected || tabDirection == QMacStylePrivate::West) @@ -3941,8 +3862,13 @@ void QMacStyle::drawControl(ControlElement ce, const QStyleOption *opt, QPainter pb.enabled = isEnabled; [pb highlight:isPressed]; + // Set off state when inactive. See needsInactiveHack for when it's selected - pb.state = (isActive && isSelected && !isPressed) ? NSControlStateValueOn : NSControlStateValueOff; + // On macOS 12, don't set the Off state for selected tabs as it draws a gray backgorund even when highlighted + if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) + pb.state = (isActive && isSelected) ? NSControlStateValueOn : NSControlStateValueOff; + else + pb.state = (isActive && isSelected && !isPressed) ? NSControlStateValueOn : NSControlStateValueOff; const auto drawBezelBlock = ^(CGContextRef ctx, const CGRect &r) { CGContextClipToRect(ctx, opt->rect.toCGRect()); @@ -4109,12 +4035,7 @@ void QMacStyle::drawControl(ControlElement ce, const QStyleOption *opt, QPainter } // fill title bar background - QLinearGradient linearGrad; - linearGrad.setStart(QPointF(0, 0)); - linearGrad.setFinalStop(QPointF(0, 2 * effectiveRect.height())); - linearGrad.setColorAt(0, opt->palette.button().color()); - linearGrad.setColorAt(1, opt->palette.dark().color()); - p->fillRect(effectiveRect, linearGrad); + p->fillRect(effectiveRect, opt->palette.window()); // draw horizontal line at bottom p->setPen(opt->palette.dark().color()); @@ -4129,7 +4050,7 @@ void QMacStyle::drawControl(ControlElement ce, const QStyleOption *opt, QPainter titleRect.width()); const auto text = p->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width()); - proxy()->drawItemText(p, titleRect, Qt::AlignCenter, dwOpt->palette, + proxy()->drawItemText(p, titleRect, Qt::AlignCenter | Qt::TextHideMnemonic, dwOpt->palette, dwOpt->state & State_Enabled, text, QPalette::WindowText); } p->restore(); @@ -5189,7 +5110,8 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex const auto cocoaSize = d->effectiveAquaSizeConstrain(opt, widget); const CGFloat maxExpandScale = expandedKnobWidths[cocoaSize] / knobWidths[cocoaSize]; - const bool isTransient = proxy()->styleHint(SH_ScrollBar_Transient, opt, widget); + const QStyle *realStyle = widget ? widget->style() : proxy(); + const bool isTransient = realStyle->styleHint(SH_ScrollBar_Transient, opt, widget); if (!isTransient) d->stopAnimation(opt->styleObject); bool wasActive = false; @@ -5378,6 +5300,15 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex const CGRect knobRect = [slider.cell knobRectFlipped:slider.isFlipped]; pressPoint.x = CGRectGetMidX(knobRect); pressPoint.y = CGRectGetMidY(knobRect); + + // The only way to tell a NSSlider/NSSliderCell to render as pressed + // is to start tracking. But this API has some weird behaviors that + // we have to account for. First of all, the pressed state will not + // be visually reflected unless we start tracking twice. And secondly + // if we don't track twice, the state of one render-pass will affect + // the render pass of other sliders, even if we set up the shared + // NSSlider with a new slider value. + [slider.cell startTrackingAt:pressPoint inView:slider]; [slider.cell startTrackingAt:pressPoint inView:slider]; } @@ -5482,8 +5413,12 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex } }); - if (isPressed) + if (isPressed) { + // We stop twice to be on the safe side, even if one seems to be enough. + // See startTracking above for why we do this. + [slider.cell stopTracking:pressPoint at:pressPoint inView:slider mouseIsUp:NO]; [slider.cell stopTracking:pressPoint at:pressPoint inView:slider mouseIsUp:NO]; + } } break; #if QT_CONFIG(spinbox) @@ -5545,7 +5480,7 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex const auto cw = QMacStylePrivate::CocoaControl(ct, cs); auto *cc = static_cast<NSControl *>(d->cocoaControl(cw)); cc.enabled = isEnabled; - QRectF frameRect = cw.adjustedControlFrame(combo->rect);; + QRectF frameRect = cw.adjustedControlFrame(combo->rect); if (cw.type == QMacStylePrivate::Button_PopupButton) { // Non-editable QComboBox auto *pb = static_cast<NSPopUpButton *>(cc); @@ -5618,16 +5553,7 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex const auto frameAdjust = 1.0 / p->device()->devicePixelRatio(); const auto innerFrameRect = outerFrameRect.adjusted(frameAdjust, frameAdjust, -frameAdjust, 0); QPainterPath innerFramePath = d->windowPanelPath(innerFrameRect); - if (isActive) { - QLinearGradient g; - g.setStart(QPointF(0, 0)); - g.setFinalStop(QPointF(0, 2 * opt->rect.height())); - g.setColorAt(0, opt->palette.button().color()); - g.setColorAt(1, opt->palette.dark().color()); - p->fillPath(innerFramePath, g); - } else { - p->fillPath(innerFramePath, opt->palette.button()); - } + p->fillPath(innerFramePath, opt->palette.button()); if (titlebar->subControls & (SC_TitleBarCloseButton | SC_TitleBarMaxButton @@ -5690,8 +5616,8 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex const bool rtl = groupBox.direction == Qt::RightToLeft; const int alignment = Qt::TextHideMnemonic | (rtl ? Qt::AlignRight : Qt::AlignLeft); const QFont savedFont = p->font(); - if (!flat) - p->setFont(d->smallSystemFont); + if (!flat && d->smallSystemFont) + p->setFont(*d->smallSystemFont); proxy()->drawItemText(p, rect, alignment, groupBox.palette, groupBox.state & State_Enabled, groupBox.text, QPalette::WindowText); if (!flat) p->setFont(savedFont); @@ -5974,7 +5900,7 @@ QRect QMacStyle::subControlRect(ComplexControl cc, const QStyleOptionComplex *op qreal controlsSpacing = lastButtonRect.right() + titleBarButtonSpacing; if (!titlebar->icon.isNull()) { const auto iconSize = proxy()->pixelMetric(PM_SmallIconSize); - const auto actualIconSize = titlebar->icon.actualSize(QSize(iconSize, iconSize)).width();; + const auto actualIconSize = titlebar->icon.actualSize(QSize(iconSize, iconSize)).width(); controlsSpacing += actualIconSize + titleBarIconTitleSpacing; } @@ -6055,7 +5981,9 @@ QRect QMacStyle::subControlRect(ComplexControl cc, const QStyleOptionComplex *op const int margin = flat || hasNoText ? 0 : 9; ret = groupBox->rect.adjusted(margin, 0, -margin, 0); - const QFontMetricsF fm = flat || fontIsSet ? QFontMetricsF(groupBox->fontMetrics) : QFontMetricsF(d->smallSystemFont); + const QFontMetricsF fm = flat || fontIsSet || !d->smallSystemFont + ? QFontMetricsF(groupBox->fontMetrics) + : QFontMetricsF(*d->smallSystemFont); const QSizeF s = fm.size(Qt::AlignHCenter | Qt::AlignVCenter, qt_mac_removeMnemonics(groupBox->text), 0, nullptr); const int tw = qCeil(s.width()); const int h = qCeil(fm.height()); diff --git a/src/plugins/styles/mac/qmacstyle_mac_p_p.h b/src/plugins/styles/mac/qmacstyle_mac_p_p.h index 8dc8d659e7..0dcfce2f0c 100644 --- a/src/plugins/styles/mac/qmacstyle_mac_p_p.h +++ b/src/plugins/styles/mac/qmacstyle_mac_p_p.h @@ -12,7 +12,6 @@ #include <QtCore/qlist.h> #include <QtCore/qmap.h> #include <QtCore/qmath.h> -#include <QtCore/qpair.h> #include <QtCore/qpointer.h> #include <QtCore/qtextstream.h> @@ -262,8 +261,7 @@ public: mutable QHash<CocoaControl, NSView *> cocoaControls; mutable QHash<CocoaControl, NSCell *> cocoaCells; - QFont smallSystemFont; - QFont miniSystemFont; + std::optional<QFont> smallSystemFont; QMacKeyValueObserver appearanceObserver; }; diff --git a/src/plugins/styles/windowsvista/CMakeLists.txt b/src/plugins/styles/modernwindows/CMakeLists.txt index efd552ab08..985bce3a2d 100644 --- a/src/plugins/styles/windowsvista/CMakeLists.txt +++ b/src/plugins/styles/modernwindows/CMakeLists.txt @@ -1,26 +1,27 @@ -# Generated from windowsvista.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QWindowsVistaStylePlugin Plugin: ##################################################################### -qt_internal_add_plugin(QWindowsVistaStylePlugin - OUTPUT_NAME qwindowsvistastyle +qt_internal_add_plugin(QModernWindowsStylePlugin + OUTPUT_NAME qmodernwindowsstyle PLUGIN_TYPE styles SOURCES main.cpp qwindowsvistastyle.cpp qwindowsvistastyle_p.h + qwindows11style.cpp qwindows11style_p.h qwindowsvistastyle_p_p.h - qwindowsxpstyle.cpp qwindowsxpstyle_p.h - qwindowsxpstyle_p_p.h + qwindowsvistaanimation.cpp qwindowsvistaanimation_p.h + qwindowsthemedata.cpp qwindowsthemedata_p.h + LIBRARIES gdi32 user32 uxtheme Qt::Core Qt::Gui + Qt::GuiPrivate Qt::WidgetsPrivate ) - -#### Keys ignored in scope 1:.:.:windowsvista.pro:<TRUE>: -# DISTFILES = "windowsvistastyle.json" diff --git a/src/plugins/styles/modernwindows/main.cpp b/src/plugins/styles/modernwindows/main.cpp new file mode 100644 index 0000000000..a4d8e60385 --- /dev/null +++ b/src/plugins/styles/modernwindows/main.cpp @@ -0,0 +1,36 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtWidgets/private/qtwidgetsglobal_p.h> +#include <QtWidgets/qstyleplugin.h> +#include <QtCore/qoperatingsystemversion.h> +#include "qwindowsvistastyle_p.h" +#include "qwindows11style_p.h" + +QT_BEGIN_NAMESPACE + +class QModernWindowsStylePlugin : public QStylePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QStyleFactoryInterface" FILE "modernwindowsstyles.json") +public: + QStyle *create(const QString &key) override; +}; + +QStyle *QModernWindowsStylePlugin::create(const QString &key) +{ + bool isWin11OrAbove = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11; + if (isWin11OrAbove && key.compare(QLatin1String("windows11"), Qt::CaseInsensitive) == 0) { + return new QWindows11Style(); + } else if (!isWin11OrAbove && key.compare(QLatin1String("windows11"), Qt::CaseInsensitive) == 0) { + qWarning("QWindows11Style: Style is only supported on Windows11 and above"); + return new QWindowsVistaStyle(); + } else if (key.compare(QLatin1String("windowsvista"), Qt::CaseInsensitive) == 0) { + return new QWindowsVistaStyle(); + } + return nullptr; +} + +QT_END_NAMESPACE + +#include "main.moc" diff --git a/src/plugins/styles/modernwindows/modernwindowsstyles.json b/src/plugins/styles/modernwindows/modernwindowsstyles.json new file mode 100644 index 0000000000..e9cef558e8 --- /dev/null +++ b/src/plugins/styles/modernwindows/modernwindowsstyles.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "windowsvista", "windows11" ] +} diff --git a/src/plugins/styles/modernwindows/qwindows11style.cpp b/src/plugins/styles/modernwindows/qwindows11style.cpp new file mode 100644 index 0000000000..0501e54ee0 --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindows11style.cpp @@ -0,0 +1,2158 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindows11style_p.h" +#include <qstylehints.h> +#include <private/qstyleanimation_p.h> +#include <private/qstylehelper_p.h> +#include <private/qapplication_p.h> +#include <qstyleoption.h> +#include <qpainter.h> +#include <QGraphicsDropShadowEffect> +#include <QtWidgets/qcombobox.h> +#include <QtWidgets/qcommandlinkbutton.h> +#include <QtWidgets/qgraphicsview.h> +#include <QtWidgets/qlistview.h> +#include <QtWidgets/qmenu.h> +#if QT_CONFIG(mdiarea) +#include <QtWidgets/qmdiarea.h> +#endif +#include <QtWidgets/qtextedit.h> +#include <QtWidgets/qtreeview.h> + +#include "qdrawutil.h" +#include <chrono> + +QT_BEGIN_NAMESPACE + +const static int topLevelRoundingRadius = 8; //Radius for toplevel items like popups for round corners +const static int secondLevelRoundingRadius = 4; //Radius for second level items like hovered menu item round corners + +enum WINUI3Color { + subtleHighlightColor, //Subtle highlight based on alpha used for hovered elements + subtlePressedColor, //Subtle highlight based on alpha used for pressed elements + frameColorLight, //Color of frame around flyouts and controls except for Checkbox and Radiobutton + frameColorStrong, //Color of frame around Checkbox and Radiobuttons + controlStrongFill, //Color of controls with strong filling such as the right side of a slider + controlStrokeSecondary, + controlStrokePrimary, + controlFillTertiary, //Color of filled sunken controls + controlFillSecondary, //Color of filled hovered controls + menuPanelFill, //Color of menu panel + textOnAccentPrimary, //Color of text on controls filled in accent color + textOnAccentSecondary, //Color of text of sunken controls in accent color + controlTextSecondary, //Color of text of sunken controls + controlStrokeOnAccentSecondary, //Color of frame around Buttons in accent color + controlFillSolid, //Color for solid fill + surfaceStroke, //Color of MDI window frames +}; + +const static QColor WINUI3ColorsLight [] { + QColor(0x00,0x00,0x00,0x09), //subtleHighlightColor + QColor(0x00,0x00,0x00,0x06), //subtlePressedColor + QColor(0x00,0x00,0x00,0x0F), //frameColorLight + QColor(0x00,0x00,0x00,0x9c), //frameColorStrong + QColor(0x00,0x00,0x00,0x72), //controlStrongFill + QColor(0x00,0x00,0x00,0x29), //controlStrokeSecondary + QColor(0x00,0x00,0x00,0x14), //controlStrokePrimary + QColor(0xF9,0xF9,0xF9,0x00), //controlFillTertiary + QColor(0xF9,0xF9,0xF9,0x80), //controlFillSecondary + QColor(0xFF,0xFF,0xFF,0xFF), //menuPanelFill + QColor(0xFF,0xFF,0xFF,0xFF), //textOnAccentPrimary + QColor(0xFF,0xFF,0xFF,0x7F), //textOnAccentSecondary + QColor(0x00,0x00,0x00,0x7F), //controlTextSecondary + QColor(0x00,0x00,0x00,0x66), //controlStrokeOnAccentSecondary + QColor(0xFF,0xFF,0xFF,0xFF), //controlFillSolid + QColor(0x75,0x75,0x75,0x66), //surfaceStroke +}; + +const static QColor WINUI3ColorsDark[] { + QColor(0xFF,0xFF,0xFF,0x0F), //subtleHighlightColor + QColor(0xFF,0xFF,0xFF,0x0A), //subtlePressedColor + QColor(0xFF,0xFF,0xFF,0x12), //frameColorLight + QColor(0xFF,0xFF,0xFF,0x8B), //frameColorStrong + QColor(0xFF,0xFF,0xFF,0x8B), //controlStrongFill + QColor(0xFF,0xFF,0xFF,0x18), //controlStrokeSecondary + QColor(0xFF,0xFF,0xFF,0x12), //controlStrokePrimary + QColor(0xF9,0xF9,0xF9,0x00), //controlFillTertiary + QColor(0xF9,0xF9,0xF9,0x80), //controlFillSecondary + QColor(0x0F,0x0F,0x0F,0xFF), //menuPanelFill + QColor(0x00,0x00,0x00,0xFF), //textOnAccentPrimary + QColor(0x00,0x00,0x00,0x80), //textOnAccentSecondary + QColor(0xFF,0xFF,0xFF,0x87), //controlTextSecondary + QColor(0xFF,0xFF,0xFF,0x14), //controlStrokeOnAccentSecondary + QColor(0x45,0x45,0x45,0xFF), //controlFillSolid + QColor(0x75,0x75,0x75,0x66), //surfaceStroke +}; + +const static QColor* WINUI3Colors[] { + WINUI3ColorsLight, + WINUI3ColorsDark +}; + +const QColor shellCloseButtonColor(0xC4,0x2B,0x1C,0xFF); //Color of close Button in Titlebar + +#if QT_CONFIG(toolbutton) +static void drawArrow(const QStyle *style, const QStyleOptionToolButton *toolbutton, + const QRect &rect, QPainter *painter, const QWidget *widget = nullptr) +{ + QStyle::PrimitiveElement pe; + switch (toolbutton->arrowType) { + case Qt::LeftArrow: + pe = QStyle::PE_IndicatorArrowLeft; + break; + case Qt::RightArrow: + pe = QStyle::PE_IndicatorArrowRight; + break; + case Qt::UpArrow: + pe = QStyle::PE_IndicatorArrowUp; + break; + case Qt::DownArrow: + pe = QStyle::PE_IndicatorArrowDown; + break; + default: + return; + } + QStyleOption arrowOpt = *toolbutton; + arrowOpt.rect = rect; + style->drawPrimitive(pe, &arrowOpt, painter, widget); +} +#endif // QT_CONFIG(toolbutton) +/*! + \class QWindows11Style + \brief The QWindows11Style class provides a look and feel suitable for applications on Microsoft Windows 11. + \since 6.6 + \ingroup appearance + \inmodule QtWidgets + \internal + + \warning This style is only available on the Windows 11 platform and above. + + \sa QWindows11Style QWindowsVistaStyle, QMacStyle, QFusionStyle +*/ + +/*! + Constructs a QWindows11Style object. +*/ +QWindows11Style::QWindows11Style() : QWindowsVistaStyle(*new QWindows11StylePrivate) +{ + highContrastTheme = QGuiApplicationPrivate::styleHints->colorScheme() == Qt::ColorScheme::Unknown; + colorSchemeIndex = QGuiApplicationPrivate::styleHints->colorScheme() == Qt::ColorScheme::Light ? 0 : 1; +} + +/*! + \internal + Constructs a QWindows11Style object. +*/ +QWindows11Style::QWindows11Style(QWindows11StylePrivate &dd) : QWindowsVistaStyle(dd) +{ + highContrastTheme = QGuiApplicationPrivate::styleHints->colorScheme() == Qt::ColorScheme::Unknown; + colorSchemeIndex = QGuiApplicationPrivate::styleHints->colorScheme() == Qt::ColorScheme::Light ? 0 : 1; +} + +/*! + Destructor. +*/ +QWindows11Style::~QWindows11Style() = default; + +/*! + \internal + see drawPrimitive for comments on the animation support + + */ +void QWindows11Style::drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, + QPainter *painter, const QWidget *widget) const +{ + QWindows11StylePrivate *d = const_cast<QWindows11StylePrivate*>(d_func()); + + State state = option->state; + SubControls sub = option->subControls; + State flags = option->state; + if (widget && widget->testAttribute(Qt::WA_UnderMouse) && widget->isActiveWindow()) + flags |= State_MouseOver; + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + if (d->transitionsEnabled()) { + if (control == CC_Slider) { + if (const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + QObject *styleObject = option->styleObject; // Can be widget or qquickitem + + QRectF thumbRect = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); + auto center = thumbRect.center(); + const qreal outerRadius = qMin(8.0, (slider->orientation == Qt::Horizontal ? thumbRect.height() / 2.0 : thumbRect.width() / 2.0) - 1); + + thumbRect.setWidth(outerRadius); + thumbRect.setHeight(outerRadius); + thumbRect.moveCenter(center); + QPointF cursorPos = widget ? widget->mapFromGlobal(QCursor::pos()) : QPointF(); + bool isInsideHandle = thumbRect.contains(cursorPos); + + bool oldIsInsideHandle = styleObject->property("_q_insidehandle").toBool(); + int oldState = styleObject->property("_q_stylestate").toInt(); + int oldActiveControls = styleObject->property("_q_stylecontrols").toInt(); + + QRectF oldRect = styleObject->property("_q_stylerect").toRect(); + styleObject->setProperty("_q_insidehandle", isInsideHandle); + styleObject->setProperty("_q_stylestate", int(option->state)); + styleObject->setProperty("_q_stylecontrols", int(option->activeSubControls)); + styleObject->setProperty("_q_stylerect", option->rect); + if (option->styleObject->property("_q_end_radius").isNull()) + option->styleObject->setProperty("_q_end_radius", outerRadius * 0.43); + + bool doTransition = (((state & State_Sunken) != (oldState & State_Sunken) + || ((oldIsInsideHandle) != (isInsideHandle)) + || oldActiveControls != int(option->activeSubControls)) + && state & State_Enabled); + + if (oldRect != option->rect) { + doTransition = false; + d->stopAnimation(styleObject); + styleObject->setProperty("_q_inner_radius", outerRadius * 0.43); + } + + if (doTransition) { + QNumberStyleAnimation *t = new QNumberStyleAnimation(styleObject); + t->setStartValue(styleObject->property("_q_inner_radius").toFloat()); + if (state & State_Sunken) + t->setEndValue(outerRadius * 0.29); + else if (isInsideHandle) + t->setEndValue(outerRadius * 0.71); + else + t->setEndValue(outerRadius * 0.43); + + styleObject->setProperty("_q_end_radius", t->endValue()); + + t->setStartTime(d->animationTime()); + t->setDuration(150); + d->startAnimation(t); + } + } + } + } + + switch (control) { +#if QT_CONFIG(spinbox) + case CC_SpinBox: + if (const QStyleOptionSpinBox *sb = qstyleoption_cast<const QStyleOptionSpinBox *>(option)) { + if (sb->frame && (sub & SC_SpinBoxFrame)) { + painter->save(); + QRegion clipRegion = option->rect; + clipRegion -= option->rect.adjusted(2, 2, -2, -2); + painter->setClipRegion(clipRegion); + QColor lineColor = state & State_HasFocus ? option->palette.accent().color() : QColor(0,0,0); + painter->setPen(QPen(lineColor)); + painter->drawLine(option->rect.bottomLeft() + QPointF(7,-0.5), option->rect.bottomRight() + QPointF(-7,-0.5)); + painter->restore(); + } + QRectF frameRect = option->rect; + frameRect.adjust(0.5,0.5,-0.5,-0.5); + QBrush fillColor = option->palette.brush(QPalette::Base); + painter->setBrush(fillColor); + painter->setPen(QPen(highContrastTheme == true ? sb->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight])); + painter->drawRoundedRect(frameRect.adjusted(2,2,-2,-2), secondLevelRoundingRadius, secondLevelRoundingRadius); + QPoint mousePos = widget ? widget->mapFromGlobal(QCursor::pos()) : QPoint(); + QColor hoverColor = WINUI3Colors[colorSchemeIndex][subtleHighlightColor]; + if (sub & SC_SpinBoxEditField) { + QRect rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxEditField, widget).adjusted(0, 0, 0, 1); + if (rect.contains(mousePos) && !(state & State_HasFocus)) { + QBrush fillColor = QBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + painter->setBrush(fillColor); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(option->rect.adjusted(2,2,-2,-2), secondLevelRoundingRadius, secondLevelRoundingRadius); + } + } + if (sub & SC_SpinBoxUp) { + QRect rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxUp, widget).adjusted(0, 0, 0, 1); + float scale = rect.width() >= 16 ? 1.0 : rect.width()/16.0; + if (rect.contains(mousePos)) { + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(hoverColor)); + painter->drawRoundedRect(rect.adjusted(1,1,-1,-1),secondLevelRoundingRadius, secondLevelRoundingRadius); + } + painter->save(); + painter->translate(rect.center()); + painter->scale(scale,scale); + painter->translate(-rect.center()); + painter->setFont(assetFont); + painter->setPen(sb->palette.buttonText().color()); + painter->setBrush(Qt::NoBrush); + painter->drawText(rect,"\uE018", Qt::AlignVCenter | Qt::AlignHCenter); + painter->restore(); + } + if (sub & SC_SpinBoxDown) { + QRect rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxDown, widget); + float scale = rect.width() >= 16 ? 1.0 : rect.width()/16.0; + if (rect.contains(mousePos)) { + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(hoverColor)); + painter->drawRoundedRect(rect.adjusted(1,1,-1,-1), secondLevelRoundingRadius, secondLevelRoundingRadius); + } + painter->save(); + painter->translate(rect.center()); + painter->scale(scale,scale); + painter->translate(-rect.center()); + painter->setFont(assetFont); + painter->setPen(sb->palette.buttonText().color()); + painter->setBrush(Qt::NoBrush); + painter->drawText(rect,"\uE019", Qt::AlignVCenter | Qt::AlignHCenter); + painter->restore(); + } + } + break; +#endif // QT_CONFIG(spinbox) +#if QT_CONFIG(slider) + case CC_Slider: + if (const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + QRectF slrect = slider->rect; + QRegion tickreg = slrect.toRect(); + + if (sub & SC_SliderGroove) { + QRectF rect = proxy()->subControlRect(CC_Slider, option, SC_SliderGroove, widget); + QRectF handleRect = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); + QPointF handlePos = handleRect.center(); + QRectF leftRect; + QRectF rightRect; + + if (slider->orientation == Qt::Horizontal) { + rect = QRect(slrect.left(), rect.center().y() - 2, slrect.width() - 5, 4); + leftRect = QRect(rect.left(), rect.top(), (handlePos.x() - rect.left()), rect.height()); + rightRect = QRect(handlePos.x(), rect.top(), (rect.width() - handlePos.x()), rect.height()); + } else { + rect = QRect(rect.center().x() - 2, slrect.top(), 4, slrect.height() - 5); + rightRect = QRect(rect.left(), rect.top(), rect.width(), (handlePos.y() - rect.top())); + leftRect = QRect(rect.left(), handlePos.y(), rect.width(), (rect.height() - handlePos.y())); + } + + painter->setPen(Qt::NoPen); + painter->setBrush(option->palette.accent()); + painter->drawRoundedRect(leftRect,1,1); + painter->setBrush(QBrush(WINUI3Colors[colorSchemeIndex][controlStrongFill])); + painter->drawRoundedRect(rightRect,1,1); + + painter->setPen(QPen(highContrastTheme == true ? slider->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight])); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(leftRect,1.5,1.5); + painter->drawRoundedRect(rightRect,1.5,1.5); + + tickreg -= rect.toRect(); + } + if (sub & SC_SliderTickmarks) { + int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget); + int ticks = slider->tickPosition; + int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget); + int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + int interval = slider->tickInterval; + if (interval <= 0) { + interval = slider->singleStep; + if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, + available) + - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + 0, available) < 3) + interval = slider->pageStep; + } + if (!interval) + interval = 1; + int fudge = len / 2; + int pos; + int bothOffset = (ticks & QSlider::TicksAbove && ticks & QSlider::TicksBelow) ? 1 : 0; + painter->setPen(slider->palette.text().color()); + QVarLengthArray<QLine, 32> lines; + int v = slider->minimum; + while (v <= slider->maximum + 1) { + if (v == slider->maximum + 1 && interval == 1) + break; + const int v_ = qMin(v, slider->maximum); + int tickLength = (v_ == slider->minimum || v_ >= slider->maximum) ? 4 : 3; + pos = QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + v_, available) + fudge; + if (slider->orientation == Qt::Horizontal) { + if (ticks & QSlider::TicksAbove) { + lines.append(QLine(pos, tickOffset - 1 - bothOffset, + pos, tickOffset - 1 - bothOffset - tickLength)); + } + + if (ticks & QSlider::TicksBelow) { + lines.append(QLine(pos, tickOffset + thickness + bothOffset, + pos, tickOffset + thickness + bothOffset + tickLength)); + } + } else { + if (ticks & QSlider::TicksAbove) { + lines.append(QLine(tickOffset - 1 - bothOffset, pos, + tickOffset - 1 - bothOffset - tickLength, pos)); + } + + if (ticks & QSlider::TicksBelow) { + lines.append(QLine(tickOffset + thickness + bothOffset, pos, + tickOffset + thickness + bothOffset + tickLength, pos)); + } + } + // in the case where maximum is max int + int nextInterval = v + interval; + if (nextInterval < v) + break; + v = nextInterval; + } + if (!lines.isEmpty()) { + painter->save(); + painter->translate(slrect.topLeft()); + painter->drawLines(lines.constData(), lines.size()); + painter->restore(); + } + } + if (sub & SC_SliderHandle) { + if (const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + const QRectF rect = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); + const QPointF center = rect.center(); + + const QNumberStyleAnimation* animation = qobject_cast<QNumberStyleAnimation*>(d->animation(option->styleObject)); + + if (animation != nullptr) + option->styleObject->setProperty("_q_inner_radius", animation->currentValue()); + else + option->styleObject->setProperty("_q_inner_radius", option->styleObject->property("_q_end_radius")); + + const qreal outerRadius = qMin(8.0,(slider->orientation == Qt::Horizontal ? rect.height() / 2.0 : rect.width() / 2.0) - 1); + const float innerRadius = option->styleObject->property("_q_inner_radius").toFloat(); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(WINUI3Colors[colorSchemeIndex][controlFillSolid])); + painter->drawEllipse(center, outerRadius, outerRadius); + painter->setBrush(option->palette.accent()); + painter->drawEllipse(center, innerRadius, innerRadius); + + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][controlStrokeSecondary])); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(center, outerRadius + 0.5, outerRadius + 0.5); + painter->drawEllipse(center, innerRadius + 0.5, innerRadius + 0.5); + } + } + if (slider->state & State_HasFocus) { + QStyleOptionFocusRect fropt; + fropt.QStyleOption::operator=(*slider); + fropt.rect = subElementRect(SE_SliderFocusRect, slider, widget); + proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); + } + } + break; +#endif +#if QT_CONFIG(combobox) + case CC_ComboBox: + if (const QStyleOptionComboBox *combobox = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { + QBrush fillColor = combobox->palette.brush(QPalette::Base); + QRectF rect = option->rect.adjusted(2,2,-2,-2); + painter->setBrush(fillColor); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + // In case the QComboBox is hovered overdraw the background with a alpha mask to + // highlight the QComboBox. + if (state & State_MouseOver) { + fillColor = QBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + painter->setBrush(fillColor); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + } + + rect.adjust(0.5,0.5,-0.5,-0.5); + painter->setBrush(Qt::NoBrush); + painter->setPen(highContrastTheme == true ? combobox->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + if (sub & SC_ComboBoxArrow) { + QRectF rect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxArrow, widget).adjusted(-4, 0, -4, 1); + painter->setFont(assetFont); + painter->setPen(combobox->palette.text().color()); + painter->drawText(rect,"\uE019", Qt::AlignVCenter | Qt::AlignHCenter); + } + if (combobox->editable) { + QColor lineColor = state & State_HasFocus ? option->palette.accent().color() : QColor(0,0,0); + painter->setPen(QPen(lineColor)); + painter->drawLine(rect.bottomLeft() + QPoint(2,1), rect.bottomRight() + QPoint(-2,1)); + if (state & State_HasFocus) + painter->drawLine(rect.bottomLeft() + QPoint(3,2), rect.bottomRight() + QPoint(-3,2)); + } + } + break; +#endif // QT_CONFIG(combobox) + case QStyle::CC_ScrollBar: + if (const QStyleOptionSlider *scrollbar = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + QRectF rect = scrollbar->rect; + QPointF center = rect.center(); + + if (scrollbar->orientation == Qt::Vertical && rect.width()>24) + rect.marginsRemoved(QMargins(0,2,2,2)); + else if (scrollbar->orientation == Qt::Horizontal && rect.height()>24) + rect.marginsRemoved(QMargins(2,0,2,2)); + + if (state & State_MouseOver) { + if (scrollbar->orientation == Qt::Vertical && rect.width()>24) + rect.setWidth(rect.width()/2); + else if (scrollbar->orientation == Qt::Horizontal && rect.height()>24) + rect.setHeight(rect.height()/2); + rect.moveCenter(center); + painter->setBrush(scrollbar->palette.base()); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(rect, topLevelRoundingRadius, topLevelRoundingRadius); + + painter->setBrush(Qt::NoBrush); + painter->setPen(WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->drawRoundedRect(rect.marginsRemoved(QMarginsF(0.5,0.5,0.5,0.5)), topLevelRoundingRadius + 0.5, topLevelRoundingRadius + 0.5); + } + if (sub & SC_ScrollBarSlider) { + QRectF rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); + QPointF center = rect.center(); + if (flags & State_MouseOver) { + if (scrollbar->orientation == Qt::Vertical) + rect.setWidth(rect.width()/2); + else + rect.setHeight(rect.height()/2); + } + else { + if (scrollbar->orientation == Qt::Vertical) + rect.setWidth(1); + else + rect.setHeight(1); + + } + rect.moveCenter(center); + painter->setBrush(Qt::gray); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + } + if (sub & SC_ScrollBarAddLine) { + QRectF rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddLine, widget); + if (flags & State_MouseOver) { + painter->setFont(QFont("Segoe Fluent Icons",6)); + painter->setPen(Qt::gray); + if (scrollbar->orientation == Qt::Vertical) + painter->drawText(rect,"\uEDDC", Qt::AlignVCenter | Qt::AlignHCenter); + else + painter->drawText(rect,"\uEDDA", Qt::AlignVCenter | Qt::AlignHCenter); + } + } + if (sub & SC_ScrollBarSubLine) { + QRectF rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubLine, widget); + if (flags & State_MouseOver) { + painter->setPen(Qt::gray); + if (scrollbar->orientation == Qt::Vertical) + painter->drawText(rect,"\uEDDB", Qt::AlignVCenter | Qt::AlignHCenter); + else + painter->drawText(rect,"\uEDD9", Qt::AlignVCenter | Qt::AlignHCenter); + } + } + } + break; + case CC_TitleBar: + if (const auto* titlebar = qstyleoption_cast<const QStyleOptionTitleBar*>(option)) { + painter->setPen(Qt::NoPen); + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][surfaceStroke])); + painter->setBrush(titlebar->palette.button()); + painter->drawRect(titlebar->rect); + + // draw title + QRect textRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarLabel, widget); + painter->setPen(titlebar->palette.text().color()); + // Note workspace also does elliding but it does not use the correct font + QString title = painter->fontMetrics().elidedText(titlebar->text, Qt::ElideRight, textRect.width() - 14); + painter->drawText(textRect.adjusted(1, 1, 1, 1), title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter)); + + QFont buttonFont = QFont(assetFont); + buttonFont.setPointSize(8); + // min button + if ((titlebar->subControls & SC_TitleBarMinButton) && (titlebar->titleBarFlags & Qt::WindowMinimizeButtonHint) && + !(titlebar->titleBarState& Qt::WindowMinimized)) { + const QRect minButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarMinButton, widget); + if (minButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarMinButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(minButtonRect,WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + const QString textToDraw("\uE921"); + painter->setPen(QPen(titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(minButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + // max button + if ((titlebar->subControls & SC_TitleBarMaxButton) && (titlebar->titleBarFlags & Qt::WindowMaximizeButtonHint) && + !(titlebar->titleBarState & Qt::WindowMaximized)) { + const QRectF maxButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarMaxButton, widget); + if (maxButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarMaxButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(maxButtonRect,WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + const QString textToDraw("\uE922"); + painter->setPen(QPen(titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(maxButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + + // close button + if ((titlebar->subControls & SC_TitleBarCloseButton) && (titlebar->titleBarFlags & Qt::WindowSystemMenuHint)) { + const QRect closeButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarCloseButton, widget); + if (closeButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarCloseButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(closeButtonRect,shellCloseButtonColor); + const QString textToDraw("\uE8BB"); + painter->setPen(QPen(hover ? titlebar->palette.highlightedText().color() : titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(closeButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + + // normalize button + if ((titlebar->subControls & SC_TitleBarNormalButton) && + (((titlebar->titleBarFlags & Qt::WindowMinimizeButtonHint) && + (titlebar->titleBarState & Qt::WindowMinimized)) || + ((titlebar->titleBarFlags & Qt::WindowMaximizeButtonHint) && + (titlebar->titleBarState & Qt::WindowMaximized)))) { + const QRect normalButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarNormalButton, widget); + if (normalButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarNormalButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(normalButtonRect,WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + const QString textToDraw("\uE923"); + painter->setPen(QPen(titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(normalButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + + // context help button + if (titlebar->subControls & SC_TitleBarContextHelpButton + && (titlebar->titleBarFlags & Qt::WindowContextHelpButtonHint)) { + const QRect contextHelpButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarContextHelpButton, widget); + if (contextHelpButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarCloseButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(contextHelpButtonRect,WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + const QString textToDraw("\uE897"); + painter->setPen(QPen(titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(contextHelpButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + + // shade button + if (titlebar->subControls & SC_TitleBarShadeButton && (titlebar->titleBarFlags & Qt::WindowShadeButtonHint)) { + const QRect shadeButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarShadeButton, widget); + if (shadeButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarShadeButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(shadeButtonRect,WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + const QString textToDraw("\uE010"); + painter->setPen(QPen(titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(shadeButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + + // unshade button + if (titlebar->subControls & SC_TitleBarUnshadeButton && (titlebar->titleBarFlags & Qt::WindowShadeButtonHint)) { + const QRect unshadeButtonRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarUnshadeButton, widget); + if (unshadeButtonRect.isValid()) { + bool hover = (titlebar->activeSubControls & SC_TitleBarUnshadeButton) && (titlebar->state & State_MouseOver); + if (hover) + painter->fillRect(unshadeButtonRect,WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + const QString textToDraw("\uE011"); + painter->setPen(QPen(titlebar->palette.text().color())); + painter->setFont(buttonFont); + painter->drawText(unshadeButtonRect, Qt::AlignVCenter | Qt::AlignHCenter, textToDraw); + } + } + + // window icon for system menu + if ((titlebar->subControls & SC_TitleBarSysMenu) && (titlebar->titleBarFlags & Qt::WindowSystemMenuHint)) { + const QRect iconRect = proxy()->subControlRect(CC_TitleBar, titlebar, SC_TitleBarSysMenu, widget); + if (iconRect.isValid()) { + if (!titlebar->icon.isNull()) { + titlebar->icon.paint(painter, iconRect); + } else { + QStyleOption tool = *titlebar; + QPixmap pm = proxy()->standardIcon(SP_TitleBarMenuButton, &tool, widget).pixmap(16, 16); + tool.rect = iconRect; + painter->save(); + proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pm); + painter->restore(); + } + } + } + } + break; + default: + QWindowsVistaStyle::drawComplexControl(control, option, painter, widget); + } + painter->restore(); +} + +void QWindows11Style::drawPrimitive(PrimitiveElement element, const QStyleOption *option, + QPainter *painter, + const QWidget *widget) const { + QWindows11StylePrivate *d = const_cast<QWindows11StylePrivate*>(d_func()); + + int state = option->state; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + if (d->transitionsEnabled() && (element == PE_IndicatorCheckBox || element == PE_IndicatorRadioButton)) { + QObject *styleObject = option->styleObject; // Can be widget or qquickitem + if (styleObject) { + int oldState = styleObject->property("_q_stylestate").toInt(); + styleObject->setProperty("_q_stylestate", int(option->state)); + styleObject->setProperty("_q_stylerect", option->rect); + bool doTransition = (((state & State_Sunken) != (oldState & State_Sunken) + || ((state & State_MouseOver) != (oldState & State_MouseOver)) + || (state & State_On) != (oldState & State_On)) + && state & State_Enabled); + if (doTransition) { + if (element == PE_IndicatorRadioButton) { + QNumberStyleAnimation *t = new QNumberStyleAnimation(styleObject); + t->setStartValue(styleObject->property("_q_inner_radius").toFloat()); + t->setEndValue(7.0f); + if (option->state & State_Sunken) + t->setEndValue(2.0f); + else if (option->state & State_MouseOver && !(option->state & State_On)) + t->setEndValue(7.0f); + else if (option->state & State_MouseOver && (option->state & State_On)) + t->setEndValue(5.0f); + else if (option->state & State_On) + t->setEndValue(4.0f); + styleObject->setProperty("_q_end_radius", t->endValue()); + t->setStartTime(d->animationTime()); + t->setDuration(150); + d->startAnimation(t); + } + else if (element == PE_IndicatorCheckBox) { + if ((oldState & State_Off && state & State_On) || (oldState & State_NoChange && state & State_On)) { + QNumberStyleAnimation *t = new QNumberStyleAnimation(styleObject); + t->setStartValue(0.0f); + t->setEndValue(1.0f); + t->setStartTime(d->animationTime()); + t->setDuration(150); + d->startAnimation(t); + } + } + } + } + } else if (!d->transitionsEnabled() && element == PE_IndicatorRadioButton) { + QObject *styleObject = option->styleObject; // Can be widget or qquickitem + if (styleObject) { + styleObject->setProperty("_q_end_radius",7.0); + if (option->state & State_Sunken) + styleObject->setProperty("_q_end_radius",2.0); + else if (option->state & State_MouseOver && !(option->state & State_On)) + styleObject->setProperty("_q_end_radius",7.0); + else if (option->state & State_MouseOver && (option->state & State_On)) + styleObject->setProperty("_q_end_radius",5.0); + else if (option->state & State_On) + styleObject->setProperty("_q_end_radius",4.0); + } + } + + switch (element) { + case PE_PanelTipLabel: { + QRectF tipRect = option->rect.marginsRemoved(QMargins(1,1,1,1)); + painter->setPen(Qt::NoPen); + painter->setBrush(option->palette.toolTipBase()); + painter->drawRoundedRect(tipRect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + painter->setPen(highContrastTheme == true ? option->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(tipRect.marginsAdded(QMarginsF(0.5,0.5,0.5,0.5)), secondLevelRoundingRadius, secondLevelRoundingRadius); + break; + } + case PE_FrameTabWidget: + if (const QStyleOptionTabWidgetFrame *frame = qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option)) { + QRectF frameRect = frame->rect.marginsRemoved(QMargins(0,0,0,0)); + painter->setPen(Qt::NoPen); + painter->setBrush(frame->palette.base()); + painter->drawRoundedRect(frameRect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + painter->setPen(highContrastTheme == true ? frame->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(frameRect.marginsRemoved(QMarginsF(0.5,0.5,0.5,0.5)), secondLevelRoundingRadius, secondLevelRoundingRadius); + } + break; + case PE_FrameGroupBox: + if (const QStyleOptionFrame *frame = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + QRectF frameRect = frame->rect; + frameRect.adjust(0.5,0.5,-0.5,-0.5); + painter->setPen(highContrastTheme == true ? frame->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorStrong]); + painter->setBrush(Qt::NoBrush); + if (frame->features & QStyleOptionFrame::Flat) { + QRect fr = frame->rect; + QPoint p1(fr.x(), fr.y() + 1); + QPoint p2(fr.x() + fr.width(), p1.y()); + painter->drawLine(p1,p2); + } else { + painter->drawRoundedRect(frameRect.marginsRemoved(QMargins(1,1,1,1)), secondLevelRoundingRadius, secondLevelRoundingRadius); + } + } + break; + case PE_IndicatorCheckBox: + { + QNumberStyleAnimation* animation = qobject_cast<QNumberStyleAnimation*>(d->animation(option->styleObject)); + QFontMetrics fm(assetFont); + + QRectF rect = option->rect; + QPointF center = QPointF(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2); + rect.setWidth(15); + rect.setHeight(15); + rect.moveCenter(center); + + float clipWidth = animation != nullptr ? animation->currentValue() : 1.0f; + QRectF clipRect = fm.boundingRect("\uE001"); + clipRect.moveCenter(center); + clipRect.setLeft(rect.x() + (rect.width() - clipRect.width()) / 2.0); + clipRect.setWidth(clipWidth * clipRect.width()); + + + QBrush fillBrush = (option->state & State_On || option->state & State_NoChange) ? option->palette.accent() : option->palette.window(); + if (state & State_MouseOver && (option->state & State_On || option->state & State_NoChange)) + fillBrush.setColor(fillBrush.color().lighter(107)); + else if (state & State_MouseOver && !(option->state & State_On || option->state & State_NoChange)) + fillBrush.setColor(fillBrush.color().darker(107)); + painter->setPen(Qt::NoPen); + painter->setBrush(fillBrush); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius, Qt::AbsoluteSize); + + painter->setPen(QPen(highContrastTheme == true ? option->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorStrong])); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(rect, secondLevelRoundingRadius + 0.5, secondLevelRoundingRadius + 0.5, Qt::AbsoluteSize); + + painter->setFont(assetFont); + painter->setPen(option->palette.highlightedText().color()); + painter->setBrush(option->palette.highlightedText().color()); + if (option->state & State_On) + painter->drawText(clipRect, Qt::AlignVCenter | Qt::AlignLeft,"\uE001"); + else if (option->state & State_NoChange) + painter->drawText(rect, Qt::AlignVCenter | Qt::AlignHCenter,"\uE108"); + } + break; + + case PE_IndicatorRadioButton: + { + if (option->styleObject->property("_q_end_radius").isNull()) + option->styleObject->setProperty("_q_end_radius", option->state & State_On ? 4.0f :7.0f); + QNumberStyleAnimation* animation = qobject_cast<QNumberStyleAnimation*>(d->animation(option->styleObject)); + if (animation != nullptr) + option->styleObject->setProperty("_q_inner_radius", animation->currentValue()); + else + option->styleObject->setProperty("_q_inner_radius", option->styleObject->property("_q_end_radius")); + int innerRadius = option->styleObject->property("_q_inner_radius").toFloat(); + + QRectF rect = option->rect; + QPointF center = QPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2); + rect.setWidth(15); + rect.setHeight(15); + rect.moveCenter(center); + QRectF innerRect = rect; + innerRect.setWidth(8); + innerRect.setHeight(8); + innerRect.moveCenter(center); + + painter->setPen(Qt::NoPen); + painter->setBrush(option->palette.accent()); + if (option->state & State_MouseOver && option->state & State_Enabled) + painter->setBrush(QBrush(option->palette.accent().color().lighter(107))); + painter->drawEllipse(center, 7, 7); + + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][frameColorStrong])); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(center, 7.5, 7.5); + + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(option->palette.window())); + painter->drawEllipse(center,innerRadius, innerRadius); + + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][frameColorStrong])); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(center,innerRadius + 0.5, innerRadius + 0.5); + + painter->setPen(Qt::NoPen); + painter->setBrush(QBrush(option->palette.window())); + if (option->state & State_MouseOver && option->state & State_Enabled) + painter->setBrush(QBrush(option->palette.window().color().darker(107))); + painter->drawEllipse(center,innerRadius, innerRadius); + } + break; + case PE_PanelButtonBevel:{ + QRectF rect = option->rect.marginsRemoved(QMargins(2,2,2,2)); + rect.adjust(-0.5,-0.5,0.5,0.5); + painter->setBrush(Qt::NoBrush); + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][controlStrokePrimary])); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + rect = option->rect.marginsRemoved(QMargins(2,2,2,2)); + painter->setPen(Qt::NoPen); + if (!(state & (State_Raised))) + painter->setBrush(WINUI3Colors[colorSchemeIndex][controlFillTertiary]); + else if (state & State_MouseOver) + painter->setBrush(WINUI3Colors[colorSchemeIndex][controlFillSecondary]); + else + painter->setBrush(option->palette.button()); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][controlStrokeSecondary])); + if (state & State_Raised) + painter->drawLine(rect.bottomLeft() + QPoint(2,1), rect.bottomRight() + QPoint(-2,1)); + } + break; + case PE_FrameDefaultButton: + painter->setPen(option->palette.accent().color()); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(option->rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + break; + case QStyle::PE_FrameMenu: + break; + case QStyle::PE_PanelMenu: { + QRect rect = option->rect; + QPen pen(WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->save(); + painter->setPen(pen); + painter->setBrush(QBrush(WINUI3Colors[colorSchemeIndex][menuPanelFill])); + painter->setRenderHint(QPainter::Antialiasing); + painter->drawRoundedRect(rect.marginsRemoved(QMargins(2,2,12,2)), topLevelRoundingRadius, topLevelRoundingRadius); + painter->restore(); + break; + } + case PE_PanelLineEdit: + if (widget && widget->objectName() == "qt_spinbox_lineedit") + break; + if (const auto *panel = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + QRectF frameRect = option->rect; + frameRect.adjust(0.5,0.5,-0.5,-0.5); + QBrush fillColor = option->palette.brush(QPalette::Base); + painter->setBrush(fillColor); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(frameRect, secondLevelRoundingRadius, secondLevelRoundingRadius); + // In case the QLineEdit is hovered overdraw the background with a alpha mask to + // highlight the QLineEdit. + if (state & State_MouseOver && !(state & State_HasFocus)) { + fillColor = QBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + painter->setBrush(fillColor); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(frameRect, secondLevelRoundingRadius, secondLevelRoundingRadius); + } + if (panel->lineWidth > 0) + proxy()->drawPrimitive(PE_FrameLineEdit, panel, painter, widget); + } + break; + case PE_FrameLineEdit: { + painter->setBrush(Qt::NoBrush); + painter->setPen(highContrastTheme == true ? option->palette.buttonText().color() : QPen(WINUI3Colors[colorSchemeIndex][frameColorLight])); + painter->drawRoundedRect(option->rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + QRegion clipRegion = option->rect; + clipRegion -= option->rect.adjusted(2, 2, -2, -2); + painter->setClipRegion(clipRegion); + QColor lineColor = state & State_HasFocus ? option->palette.accent().color() : QColor(0,0,0); + painter->setPen(QPen(lineColor)); + painter->drawLine(option->rect.bottomLeft() + QPointF(1,0.5), option->rect.bottomRight() + QPointF(-1,0.5)); + } + break; + case PE_Frame: { + if (const auto *frame = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + if (frame->frameShape == QFrame::NoFrame) + break; + QRectF rect = option->rect.adjusted(1,1,-1,-1); + if (widget && widget->inherits("QComboBoxPrivateContainer")) { + painter->setPen(Qt::NoPen); + painter->setBrush(WINUI3Colors[colorSchemeIndex][menuPanelFill]); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + } + painter->setBrush(option->palette.base()); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + painter->setBrush(Qt::NoBrush); + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][frameColorLight])); + painter->drawRoundedRect(rect.marginsRemoved(QMarginsF(0.5,0.5,0.5,0.5)), secondLevelRoundingRadius, secondLevelRoundingRadius); + + if (qobject_cast<const QTextEdit *>(widget)) { + QRegion clipRegion = option->rect; + QColor lineColor = state & State_HasFocus ? option->palette.accent().color() : QColor(0,0,0,255); + painter->setPen(QPen(lineColor)); + painter->drawLine(option->rect.bottomLeft() + QPoint(1,-1), option->rect.bottomRight() + QPoint(-1,-1)); + } + } + break; + } + case QStyle::PE_PanelItemViewRow: + if (const QStyleOptionViewItem *vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { + if ((vopt->state & State_Selected || vopt->state & State_MouseOver) && vopt->showDecorationSelected) { + painter->setBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(vopt->rect.marginsRemoved(QMargins(0,2,-2,2)),2,2); + const int offset = qobject_cast<const QTreeView *>(widget) ? 2 : 0; + if (vopt->viewItemPosition == QStyleOptionViewItem::Beginning && option->state & State_Selected) { + painter->setPen(QPen(option->palette.accent().color())); + painter->drawLine(option->rect.x(),option->rect.y()+offset,option->rect.x(),option->rect.y() + option->rect.height()-2); + painter->drawLine(option->rect.x()+1,option->rect.y()+2,option->rect.x()+1,option->rect.y() + option->rect.height()-2); + } + } + } + break; + case QStyle::PE_Widget: { +#if QT_CONFIG(dialogbuttonbox) + const QDialogButtonBox *buttonBox = nullptr; + if (qobject_cast<const QMessageBox *> (widget)) + buttonBox = widget->findChild<const QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); +#if QT_CONFIG(inputdialog) + else if (qobject_cast<const QInputDialog *> (widget)) + buttonBox = widget->findChild<const QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); +#endif // QT_CONFIG(inputdialog) + if (buttonBox) { + painter->fillRect(option->rect,option->palette.window()); + } +#endif + break; + } + case QStyle::PE_FrameWindow: + if (const auto *frm = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + + QRectF rect= option->rect; + int fwidth = int((frm->lineWidth + frm->midLineWidth) / QWindowsStylePrivate::nativeMetricScaleFactor(widget)); + + QRectF bottomLeftCorner = QRectF(rect.left() + 1.0, + rect.bottom() - 1.0 - secondLevelRoundingRadius, + secondLevelRoundingRadius, + secondLevelRoundingRadius); + QRectF bottomRightCorner = QRectF(rect.right() - 1.0 - secondLevelRoundingRadius, + rect.bottom() - 1.0 - secondLevelRoundingRadius, + secondLevelRoundingRadius, + secondLevelRoundingRadius); + + //Draw Mask + if (widget != nullptr) { + QBitmap mask(widget->width(), widget->height()); + mask.clear(); + + QPainter maskPainter(&mask); + maskPainter.setRenderHint(QPainter::Antialiasing); + maskPainter.setBrush(Qt::color1); + maskPainter.setPen(Qt::NoPen); + maskPainter.drawRoundedRect(option->rect,secondLevelRoundingRadius,secondLevelRoundingRadius); + const_cast<QWidget*>(widget)->setMask(mask); + } + + //Draw Window + painter->setPen(QPen(frm->palette.base(), fwidth)); + painter->drawLine(QPointF(rect.left(), rect.top()), + QPointF(rect.left(), rect.bottom() - fwidth)); + painter->drawLine(QPointF(rect.left() + fwidth, rect.bottom()), + QPointF(rect.right() - fwidth, rect.bottom())); + painter->drawLine(QPointF(rect.right(), rect.top()), + QPointF(rect.right(), rect.bottom() - fwidth)); + + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][surfaceStroke])); + painter->drawLine(QPointF(rect.left() + 0.5, rect.top() + 0.5), + QPointF(rect.left() + 0.5, rect.bottom() - 0.5 - secondLevelRoundingRadius)); + painter->drawLine(QPointF(rect.left() + 0.5 + secondLevelRoundingRadius, rect.bottom() - 0.5), + QPointF(rect.right() - 0.5 - secondLevelRoundingRadius, rect.bottom() - 0.5)); + painter->drawLine(QPointF(rect.right() - 0.5, rect.top() + 1.5), + QPointF(rect.right() - 0.5, rect.bottom() - 0.5 - secondLevelRoundingRadius)); + + painter->setPen(Qt::NoPen); + painter->setBrush(frm->palette.base()); + painter->drawPie(bottomRightCorner.marginsAdded(QMarginsF(2.5,2.5,0.0,0.0)), + 270 * 16,90 * 16); + painter->drawPie(bottomLeftCorner.marginsAdded(QMarginsF(0.0,2.5,2.5,0.0)), + -90 * 16,-90 * 16); + + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][surfaceStroke])); + painter->setBrush(Qt::NoBrush); + painter->drawArc(bottomRightCorner, + 0 * 16,-90 * 16); + painter->drawArc(bottomLeftCorner, + -90 * 16,-90 * 16); + } + break; + default: + QWindowsVistaStyle::drawPrimitive(element, option, painter, widget); + } + painter->restore(); +} + +/*! + \internal +*/ +void QWindows11Style::drawControl(ControlElement element, const QStyleOption *option, + QPainter *painter, const QWidget *widget) const +{ + Q_D(const QWindows11Style); + QRect rect(option->rect); + State flags = option->state; + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + switch (element) { + case QStyle::CE_TabBarTabShape: + if (const QStyleOptionTab *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) { + QRectF tabRect = tab->rect.marginsRemoved(QMargins(2,2,0,0)); + painter->setPen(Qt::NoPen); + painter->setBrush(tab->palette.base()); + if (tab->state & State_MouseOver){ + painter->setBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + } else if (tab->state & State_Selected) { + painter->setBrush(tab->palette.base()); + } else { + painter->setBrush(tab->palette.window()); + } + painter->drawRoundedRect(tabRect,2,2); + + painter->setBrush(Qt::NoBrush); + painter->setPen(highContrastTheme == true ? tab->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->drawRoundedRect(tabRect.adjusted(0.5,0.5,-0.5,-0.5),2,2); + + } + break; + case CE_ToolButtonLabel: + if (const QStyleOptionToolButton *toolbutton + = qstyleoption_cast<const QStyleOptionToolButton *>(option)) { + QRect rect = toolbutton->rect; + int shiftX = 0; + int shiftY = 0; + if (toolbutton->state & (State_Sunken | State_On)) { + shiftX = proxy()->pixelMetric(PM_ButtonShiftHorizontal, toolbutton, widget); + shiftY = proxy()->pixelMetric(PM_ButtonShiftVertical, toolbutton, widget); + } + // Arrow type always overrules and is always shown + bool hasArrow = toolbutton->features & QStyleOptionToolButton::Arrow; + if (((!hasArrow && toolbutton->icon.isNull()) && !toolbutton->text.isEmpty()) + || toolbutton->toolButtonStyle == Qt::ToolButtonTextOnly) { + int alignment = Qt::AlignCenter | Qt::TextShowMnemonic; + if (!proxy()->styleHint(SH_UnderlineShortcut, toolbutton, widget)) + alignment |= Qt::TextHideMnemonic; + rect.translate(shiftX, shiftY); + painter->setFont(toolbutton->font); + const QString text = d->toolButtonElideText(toolbutton, rect, alignment); + if (toolbutton->state & State_Raised || toolbutton->palette.isBrushSet(QPalette::Current, QPalette::ButtonText)) + painter->setPen(QPen(toolbutton->palette.buttonText().color())); + else + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][controlTextSecondary])); + proxy()->drawItemText(painter, rect, alignment, toolbutton->palette, + toolbutton->state & State_Enabled, text); + } else { + QPixmap pm; + QSize pmSize = toolbutton->iconSize; + if (!toolbutton->icon.isNull()) { + QIcon::State state = toolbutton->state & State_On ? QIcon::On : QIcon::Off; + QIcon::Mode mode; + if (!(toolbutton->state & State_Enabled)) + mode = QIcon::Disabled; + else if ((toolbutton->state & State_MouseOver) && (toolbutton->state & State_AutoRaise)) + mode = QIcon::Active; + else + mode = QIcon::Normal; + pm = toolbutton->icon.pixmap(toolbutton->rect.size().boundedTo(toolbutton->iconSize), painter->device()->devicePixelRatio(), + mode, state); + pmSize = pm.size() / pm.devicePixelRatio(); + } + + if (toolbutton->toolButtonStyle != Qt::ToolButtonIconOnly) { + painter->setFont(toolbutton->font); + QRect pr = rect, + tr = rect; + int alignment = Qt::TextShowMnemonic; + if (!proxy()->styleHint(SH_UnderlineShortcut, toolbutton, widget)) + alignment |= Qt::TextHideMnemonic; + + if (toolbutton->toolButtonStyle == Qt::ToolButtonTextUnderIcon) { + pr.setHeight(pmSize.height() + 4); //### 4 is currently hardcoded in QToolButton::sizeHint() + tr.adjust(0, pr.height() - 1, 0, -1); + pr.translate(shiftX, shiftY); + if (!hasArrow) { + proxy()->drawItemPixmap(painter, pr, Qt::AlignCenter, pm); + } else { + drawArrow(proxy(), toolbutton, pr, painter, widget); + } + alignment |= Qt::AlignCenter; + } else { + pr.setWidth(pmSize.width() + 4); //### 4 is currently hardcoded in QToolButton::sizeHint() + tr.adjust(pr.width(), 0, 0, 0); + pr.translate(shiftX, shiftY); + if (!hasArrow) { + proxy()->drawItemPixmap(painter, QStyle::visualRect(toolbutton->direction, rect, pr), Qt::AlignCenter, pm); + } else { + drawArrow(proxy(), toolbutton, pr, painter, widget); + } + alignment |= Qt::AlignLeft | Qt::AlignVCenter; + } + tr.translate(shiftX, shiftY); + const QString text = d->toolButtonElideText(toolbutton, tr, alignment); + if (toolbutton->state & State_Raised || toolbutton->palette.isBrushSet(QPalette::Current, QPalette::ButtonText)) + painter->setPen(QPen(toolbutton->palette.buttonText().color())); + else + painter->setPen(QPen(WINUI3Colors[colorSchemeIndex][controlTextSecondary])); + proxy()->drawItemText(painter, QStyle::visualRect(toolbutton->direction, rect, tr), alignment, toolbutton->palette, + toolbutton->state & State_Enabled, text); + } else { + rect.translate(shiftX, shiftY); + if (hasArrow) { + drawArrow(proxy(), toolbutton, rect, painter, widget); + } else { + proxy()->drawItemPixmap(painter, rect, Qt::AlignCenter, pm); + } + } + } + } + break; + case QStyle::CE_ShapedFrame: + if (const QStyleOptionFrame *f = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + int frameShape = f->frameShape; + int frameShadow = QFrame::Plain; + if (f->state & QStyle::State_Sunken) + frameShadow = QFrame::Sunken; + else if (f->state & QStyle::State_Raised) + frameShadow = QFrame::Raised; + + int lw = f->lineWidth; + int mlw = f->midLineWidth; + + switch (frameShape) { + case QFrame::Box: + if (frameShadow == QFrame::Plain) + qDrawPlainRoundedRect(painter, f->rect, secondLevelRoundingRadius, secondLevelRoundingRadius, highContrastTheme == true ? f->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorStrong], lw); + else + qDrawShadeRect(painter, f->rect, f->palette, frameShadow == QFrame::Sunken, lw, mlw); + break; + case QFrame::Panel: + if (frameShadow == QFrame::Plain) + qDrawPlainRoundedRect(painter, f->rect, secondLevelRoundingRadius, secondLevelRoundingRadius, highContrastTheme == true ? f->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorStrong], lw); + else + qDrawShadePanel(painter, f->rect, f->palette, frameShadow == QFrame::Sunken, lw); + break; + default: + QWindowsVistaStyle::drawControl(element, option, painter, widget); + } + } + break; + case QStyle::CE_ProgressBarGroove:{ + if (const QStyleOptionProgressBar* progbaropt = qstyleoption_cast<const QStyleOptionProgressBar*>(option)) { + QRect rect = subElementRect(SE_ProgressBarContents, progbaropt, widget); + QPointF center = rect.center(); + if (progbaropt->state & QStyle::State_Horizontal) { + rect.setHeight(1); + rect.moveTop(center.y()); + } else { + rect.setWidth(1); + rect.moveLeft(center.x()); + } + painter->setPen(Qt::NoPen); + painter->setBrush(Qt::gray); + painter->drawRect(rect); + } + break; + } + case QStyle::CE_ProgressBarContents: + if (const QStyleOptionProgressBar* progbaropt = qstyleoption_cast<const QStyleOptionProgressBar*>(option)) { + const qreal progressBarThickness = 3; + const qreal progressBarHalfThickness = progressBarThickness / 2.0; + QRectF rect = subElementRect(SE_ProgressBarContents, progbaropt, widget); + QRectF originalRect = rect; + QPointF center = rect.center(); + bool isIndeterminate = progbaropt->maximum == 0 && progbaropt->minimum == 0; + float fillPercentage = 0; + const Qt::Orientation orientation = (progbaropt->state & QStyle::State_Horizontal) ? Qt::Horizontal : Qt::Vertical; + const qreal offset = (orientation == Qt::Horizontal && int(rect.height()) % 2 == 0) + || (orientation == Qt::Vertical && int(rect.width()) % 2 == 0) ? 0.5 : 0.0; + + if (!isIndeterminate) { + fillPercentage = ((float(progbaropt->progress) - float(progbaropt->minimum)) / (float(progbaropt->maximum) - float(progbaropt->minimum))); + if (orientation == Qt::Horizontal) { + rect.setHeight(progressBarThickness); + rect.moveTop(center.y() - progressBarHalfThickness - offset); + rect.setWidth(rect.width() * fillPercentage); + } else { + float oldHeight = rect.height(); + rect.setWidth(progressBarThickness); + rect.moveLeft(center.x() - progressBarHalfThickness - offset); + rect.moveTop(oldHeight * (1.0f - fillPercentage)); + rect.setHeight(oldHeight * fillPercentage); + } + } else { + auto elapsedTime = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now()); + fillPercentage = (elapsedTime.time_since_epoch().count() % 5000)/(5000.0f*0.75); + if (orientation == Qt::Horizontal) { + float barBegin = qMin(qMax(fillPercentage-0.25,0.0) * rect.width(), float(rect.width())); + float barEnd = qMin(fillPercentage * rect.width(), float(rect.width())); + rect = QRect(QPoint(rect.left() + barBegin, rect.top()), QPoint(rect.left() + barEnd, rect.bottom())); + rect.setHeight(progressBarThickness); + rect.moveTop(center.y() - progressBarHalfThickness - offset); + } else { + float barBegin = qMin(qMax(fillPercentage-0.25,0.0) * rect.height(), float(rect.height())); + float barEnd = qMin(fillPercentage * rect.height(), float(rect.height())); + rect = QRect(QPoint(rect.left(), rect.bottom() - barEnd), QPoint(rect.right(), rect.bottom() - barBegin)); + rect.setWidth(progressBarThickness); + rect.moveLeft(center.x() - progressBarHalfThickness - offset); + } + const_cast<QWidget*>(widget)->update(); + } + if (progbaropt->invertedAppearance && orientation == Qt::Horizontal) + rect.moveLeft(originalRect.width() * (1.0 - fillPercentage)); + else if (progbaropt->invertedAppearance && orientation == Qt::Vertical) + rect.moveBottom(originalRect.height() * fillPercentage); + painter->setPen(Qt::NoPen); + painter->setBrush(progbaropt->palette.accent()); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + } + break; + case QStyle::CE_ProgressBarLabel: + if (const QStyleOptionProgressBar* progbaropt = qstyleoption_cast<const QStyleOptionProgressBar*>(option)) { + QRect rect = subElementRect(SE_ProgressBarLabel, progbaropt, widget); + painter->setPen(progbaropt->palette.text().color()); + painter->drawText(rect, progbaropt->text,Qt::AlignVCenter|Qt::AlignLeft); + } + break; + case CE_PushButtonLabel: + if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { + QRect textRect = btn->rect; + + int tf = Qt::AlignVCenter|Qt::TextShowMnemonic; + if (!proxy()->styleHint(SH_UnderlineShortcut, btn, widget)) + tf |= Qt::TextHideMnemonic; + + if (btn->features & QStyleOptionButton::HasMenu) { + int indicatorSize = proxy()->pixelMetric(PM_MenuButtonIndicator, btn, widget); + if (btn->direction == Qt::LeftToRight) + textRect = textRect.adjusted(0, 0, -indicatorSize, 0); + else + textRect = textRect.adjusted(indicatorSize, 0, 0, 0); + } + if (!btn->icon.isNull()) { + //Center both icon and text + QIcon::Mode mode = btn->state & State_Enabled ? QIcon::Normal : QIcon::Disabled; + if (mode == QIcon::Normal && btn->state & State_HasFocus) + mode = QIcon::Active; + QIcon::State state = QIcon::Off; + if (btn->state & State_On) + state = QIcon::On; + + QPixmap pixmap = btn->icon.pixmap(btn->iconSize, painter->device()->devicePixelRatio(), mode, state); + int pixmapWidth = pixmap.width() / pixmap.devicePixelRatio(); + int pixmapHeight = pixmap.height() / pixmap.devicePixelRatio(); + int labelWidth = pixmapWidth; + int labelHeight = pixmapHeight; + int iconSpacing = 4;//### 4 is currently hardcoded in QPushButton::sizeHint() + if (!btn->text.isEmpty()) { + int textWidth = btn->fontMetrics.boundingRect(option->rect, tf, btn->text).width(); + labelWidth += (textWidth + iconSpacing); + } + + QRect iconRect = QRect(textRect.x() + (textRect.width() - labelWidth) / 2, + textRect.y() + (textRect.height() - labelHeight) / 2, + pixmapWidth, pixmapHeight); + + iconRect = visualRect(btn->direction, textRect, iconRect); + + if (btn->direction == Qt::RightToLeft) { + tf |= Qt::AlignRight; + textRect.setRight(iconRect.left() - iconSpacing / 2); + } else { + tf |= Qt::AlignLeft; //left align, we adjust the text-rect instead + textRect.setLeft(iconRect.left() + iconRect.width() + iconSpacing / 2); + } + + if (btn->state & (State_On | State_Sunken)) + iconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget), + proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget)); + painter->drawPixmap(iconRect, pixmap); + } else { + tf |= Qt::AlignHCenter; + } + + + if (btn->state & State_Sunken) + painter->setPen(flags & State_On ? QPen(WINUI3Colors[colorSchemeIndex][textOnAccentSecondary]) : QPen(WINUI3Colors[colorSchemeIndex][controlTextSecondary])); + else + painter->setPen(flags & State_On ? QPen(WINUI3Colors[colorSchemeIndex][textOnAccentPrimary]) : QPen(btn->palette.buttonText().color())); + proxy()->drawItemText(painter, textRect, tf, option->palette,btn->state & State_Enabled, btn->text); + } + break; + case CE_PushButtonBevel: + if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { + if (btn->features.testFlag(QStyleOptionButton::Flat)) { + painter->setPen(Qt::NoPen); + if (flags & (State_Sunken | State_On)) { + painter->setBrush(WINUI3Colors[colorSchemeIndex][subtlePressedColor]); + } + else if (flags & State_MouseOver) { + painter->setBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + } + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + } else { + QRectF rect = btn->rect.marginsRemoved(QMargins(2,2,2,2)); + painter->setPen(Qt::NoPen); + painter->setBrush(flags & State_On ? option->palette.accent() : option->palette.button()); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + if (flags.testFlags(State_Sunken | State_MouseOver)) { + if (flags & (State_Sunken)) + painter->setBrush(flags & State_On ? option->palette.accent().color().lighter(120) : WINUI3Colors[colorSchemeIndex][controlFillTertiary]); + else if (flags & State_MouseOver) + painter->setBrush(flags & State_On ? option->palette.accent().color().lighter(110) : WINUI3Colors[colorSchemeIndex][controlFillSecondary]); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + } + + rect.adjust(0.5,0.5,-0.5,-0.5); + painter->setBrush(Qt::NoBrush); + painter->setPen(btn->features.testFlag(QStyleOptionButton::DefaultButton) ? QPen(option->palette.accent().color()) : QPen(WINUI3Colors[colorSchemeIndex][controlStrokePrimary])); + painter->drawRoundedRect(rect, secondLevelRoundingRadius, secondLevelRoundingRadius); + + painter->setPen(btn->features.testFlag(QStyleOptionButton::DefaultButton) ? QPen(WINUI3Colors[colorSchemeIndex][controlStrokeOnAccentSecondary]) : QPen(WINUI3Colors[colorSchemeIndex][controlStrokeSecondary])); + if (flags & State_Raised) + painter->drawLine(rect.bottomLeft() + QPointF(4.0,0.0), rect.bottomRight() + QPointF(-4,0.0)); + } + } + break; + case CE_MenuBarItem: + if (const auto *mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { + constexpr int hPadding = 11; + constexpr int topPadding = 4; + constexpr int bottomPadding = 6; + bool active = mbi->state & State_Selected; + bool hasFocus = mbi->state & State_HasFocus; + bool down = mbi->state & State_Sunken; + bool enabled = mbi->state & State_Enabled; + QStyleOptionMenuItem newMbi = *mbi; + newMbi.font.setPointSize(10); + if (enabled && (active || hasFocus)) { + if (active && down) + painter->setBrushOrigin(painter->brushOrigin() + QPoint(1, 1)); + if (active && hasFocus) { + painter->setBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + painter->setPen(Qt::NoPen); + QRect rect = mbi->rect.marginsRemoved(QMargins(5,0,5,0)); + painter->drawRoundedRect(rect,secondLevelRoundingRadius,secondLevelRoundingRadius,Qt::AbsoluteSize); + } + } + newMbi.rect.adjust(hPadding,topPadding,-hPadding,-bottomPadding); + painter->setFont(newMbi.font); + QCommonStyle::drawControl(element, &newMbi, painter, widget); + } + break; + +#if QT_CONFIG(menu) + case CE_MenuEmptyArea: + break; + + case CE_MenuItem: + if (const auto *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { + int x, y, w, h; + menuitem->rect.getRect(&x, &y, &w, &h); + int tab = menuitem->reservedShortcutWidth; + bool dis = !(menuitem->state & State_Enabled); + bool checked = menuitem->checkType != QStyleOptionMenuItem::NotCheckable + ? menuitem->checked : false; + bool act = menuitem->state & State_Selected; + + // windows always has a check column, regardless whether we have an icon or not + int checkcol = qMax<int>(menuitem->maxIconWidth, 32); + + QBrush fill = (act == true && dis == false) ? QBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]) : menuitem->palette.brush(QPalette::Button); + painter->setBrush(fill); + painter->setPen(Qt::NoPen); + QRect rect = menuitem->rect; + rect = rect.marginsRemoved(QMargins(2,2,2,2)); + if (act && dis == false) + painter->drawRoundedRect(rect,secondLevelRoundingRadius,secondLevelRoundingRadius,Qt::AbsoluteSize); + + if (menuitem->menuItemType == QStyleOptionMenuItem::Separator){ + int yoff = 4; + painter->setPen(highContrastTheme == true ? menuitem->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->drawLine(x, y + yoff, x + w, y + yoff ); + break; + } + + QRect vCheckRect = visualRect(option->direction, menuitem->rect, QRect(menuitem->rect.x(), menuitem->rect.y(), checkcol, menuitem->rect.height())); + if (!menuitem->icon.isNull() && checked) { + if (act) { + qDrawShadePanel(painter, vCheckRect, + menuitem->palette, true, 1, + &menuitem->palette.brush(QPalette::Button)); + } else { + QBrush fill(menuitem->palette.light().color(), Qt::Dense4Pattern); + qDrawShadePanel(painter, vCheckRect, menuitem->palette, true, 1, &fill); + } + } + // On Windows Style, if we have a checkable item and an icon we + // draw the icon recessed to indicate an item is checked. If we + // have no icon, we draw a checkmark instead. + if (!menuitem->icon.isNull()) { + QIcon::Mode mode = dis ? QIcon::Disabled : QIcon::Normal; + if (act && !dis) + mode = QIcon::Active; + QPixmap pixmap; + if (checked) + pixmap = menuitem->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), mode, QIcon::On); + else + pixmap = menuitem->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), mode); + QRect pmr(QPoint(0, 0), pixmap.deviceIndependentSize().toSize()); + pmr.moveCenter(vCheckRect.center()); + painter->setPen(menuitem->palette.text().color()); + painter->drawPixmap(pmr.topLeft(), pixmap); + } else if (checked) { + QStyleOptionMenuItem newMi = *menuitem; + newMi.state = State_None; + if (!dis) + newMi.state |= State_Enabled; + if (act) + newMi.state |= State_On | State_Selected; + newMi.rect = visualRect(option->direction, menuitem->rect, QRect(menuitem->rect.x() + QWindowsStylePrivate::windowsItemFrame, + menuitem->rect.y() + QWindowsStylePrivate::windowsItemFrame, + checkcol - 2 * QWindowsStylePrivate::windowsItemFrame, + menuitem->rect.height() - 2 * QWindowsStylePrivate::windowsItemFrame)); + + QColor discol; + if (dis) { + discol = menuitem->palette.text().color(); + painter->setPen(discol); + } + int xm = int(QWindowsStylePrivate::windowsItemFrame) + checkcol / 4 + int(QWindowsStylePrivate::windowsItemHMargin); + int xpos = menuitem->rect.x() + xm; + QRect textRect(xpos, y + QWindowsStylePrivate::windowsItemVMargin, + w - xm - QWindowsStylePrivate::windowsRightBorder - tab + 1, h - 2 * QWindowsStylePrivate::windowsItemVMargin); + QRect vTextRect = visualRect(option->direction, menuitem->rect, textRect); + + painter->save(); + painter->setFont(assetFont); + int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, menuitem, widget)) + text_flags |= Qt::TextHideMnemonic; + text_flags |= Qt::AlignLeft; + + const QString textToDraw("\uE001"); + painter->setPen(option->palette.text().color()); + painter->drawText(vTextRect, text_flags, textToDraw); + painter->restore(); + } + painter->setPen(act ? menuitem->palette.highlightedText().color() : menuitem->palette.buttonText().color()); + + QColor discol = menuitem->palette.text().color(); + if (dis) { + discol = menuitem->palette.color(QPalette::Disabled, QPalette::WindowText); + painter->setPen(discol); + } + + int xm = int(QWindowsStylePrivate::windowsItemFrame) + checkcol + int(QWindowsStylePrivate::windowsItemHMargin); + int xpos = menuitem->rect.x() + xm; + QRect textRect(xpos, y + QWindowsStylePrivate::windowsItemVMargin, + w - xm - QWindowsStylePrivate::windowsRightBorder - tab + 1, h - 2 * QWindowsStylePrivate::windowsItemVMargin); + QRect vTextRect = visualRect(option->direction, menuitem->rect, textRect); + QStringView s(menuitem->text); + if (!s.isEmpty()) { // draw text + painter->save(); + qsizetype t = s.indexOf(u'\t'); + int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, menuitem, widget)) + text_flags |= Qt::TextHideMnemonic; + text_flags |= Qt::AlignLeft; + if (t >= 0) { + QRect vShortcutRect = visualRect(option->direction, menuitem->rect, + QRect(textRect.topRight(), QPoint(menuitem->rect.right(), textRect.bottom()))); + const QString textToDraw = s.mid(t + 1).toString(); + if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) { + painter->setPen(menuitem->palette.light().color()); + painter->drawText(vShortcutRect.adjusted(1, 1, 1, 1), text_flags, textToDraw); + painter->setPen(discol); + } + painter->setPen(menuitem->palette.color(QPalette::Disabled, QPalette::Text)); + painter->drawText(vShortcutRect, text_flags, textToDraw); + s = s.left(t); + } + QFont font = menuitem->font; + if (menuitem->menuItemType == QStyleOptionMenuItem::DefaultItem) + font.setBold(true); + painter->setFont(font); + const QString textToDraw = s.left(t).toString(); + painter->setPen(discol); + painter->drawText(vTextRect, text_flags, textToDraw); + painter->restore(); + } + if (menuitem->menuItemType == QStyleOptionMenuItem::SubMenu) {// draw sub menu arrow + int dim = (h - 2 * QWindowsStylePrivate::windowsItemFrame) / 2; + xpos = x + w - QWindowsStylePrivate::windowsArrowHMargin - QWindowsStylePrivate::windowsItemFrame - dim; + QRect vSubMenuRect = visualRect(option->direction, menuitem->rect, QRect(xpos, y + h / 2 - dim / 2, dim, dim)); + QStyleOptionMenuItem newMI = *menuitem; + newMI.rect = vSubMenuRect; + newMI.state = dis ? State_None : State_Enabled; + if (act) + newMI.palette.setColor(QPalette::ButtonText, + newMI.palette.highlightedText().color()); + painter->save(); + painter->setFont(assetFont); + int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, menuitem, widget)) + text_flags |= Qt::TextHideMnemonic; + text_flags |= Qt::AlignLeft; + const QString textToDraw("\uE013"); + painter->setPen(option->palette.text().color()); + painter->drawText(vSubMenuRect, text_flags, textToDraw); + painter->restore(); + } + } + break; +#endif // QT_CONFIG(menu) + case CE_MenuBarEmptyArea: { + break; + } + case CE_HeaderEmptyArea: + break; + case CE_HeaderSection: { + if (const QStyleOptionHeader *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { + painter->setPen(Qt::NoPen); + painter->setBrush(header->palette.button()); + painter->drawRect(header->rect); + + painter->setPen(highContrastTheme == true ? header->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + painter->setBrush(Qt::NoBrush); + + if (header->position == QStyleOptionHeader::OnlyOneSection) { + break; + } + else if (header->position == QStyleOptionHeader::Beginning) { + painter->drawLine(QPointF(option->rect.topRight()) + QPointF(0.5,0.0), + QPointF(option->rect.bottomRight()) + QPointF(0.5,0.0)); + } + else if (header->position == QStyleOptionHeader::End) { + painter->drawLine(QPointF(option->rect.topLeft()) - QPointF(0.5,0.0), + QPointF(option->rect.bottomLeft()) - QPointF(0.5,0.0)); + } else { + painter->drawLine(QPointF(option->rect.topRight()) + QPointF(0.5,0.0), + QPointF(option->rect.bottomRight()) + QPointF(0.5,0.0)); + painter->drawLine(QPointF(option->rect.topLeft()) - QPointF(0.5,0.0), + QPointF(option->rect.bottomLeft()) - QPointF(0.5,0.0)); + } + painter->drawLine(QPointF(option->rect.bottomLeft()) + QPointF(0.0,0.5), + QPointF(option->rect.bottomRight()) + QPointF(0.0,0.5)); + } + break; + } + case QStyle::CE_ItemViewItem: { + if (const QStyleOptionViewItem *vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { + if (const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(widget)) { + QRect checkRect = proxy()->subElementRect(SE_ItemViewItemCheckIndicator, vopt, widget); + QRect iconRect = proxy()->subElementRect(SE_ItemViewItemDecoration, vopt, widget); + QRect textRect = proxy()->subElementRect(SE_ItemViewItemText, vopt, widget); + + QRect rect = vopt->rect; + + painter->setPen(highContrastTheme == true ? vopt->palette.buttonText().color() : WINUI3Colors[colorSchemeIndex][frameColorLight]); + if (vopt->viewItemPosition == QStyleOptionViewItem::OnlyOne || vopt->viewItemPosition == QStyleOptionViewItem::Invalid) { + } else if (vopt->viewItemPosition == QStyleOptionViewItem::Beginning) { + painter->drawLine(QPointF(option->rect.topRight()) + QPointF(0.5,0.0), + QPointF(option->rect.bottomRight()) + QPointF(0.5,0.0)); + } else if (vopt->viewItemPosition == QStyleOptionViewItem::End) { + painter->drawLine(QPointF(option->rect.topLeft()) - QPointF(0.5,0.0), + QPointF(option->rect.bottomLeft()) - QPointF(0.5,0.0)); + } else { + painter->drawLine(QPointF(option->rect.topRight()) + QPointF(0.5,0.0), + QPointF(option->rect.bottomRight()) + QPointF(0.5,0.0)); + painter->drawLine(QPointF(option->rect.topLeft()) - QPointF(0.5,0.0), + QPointF(option->rect.bottomLeft()) - QPointF(0.5,0.0)); + } + painter->setPen(QPen(option->palette.buttonText().color())); + + const bool isTreeView = qobject_cast<const QTreeView *>(widget); + + if ((vopt->state & State_Selected || vopt->state & State_MouseOver) && !(isTreeView && vopt->state & State_MouseOver) && vopt->showDecorationSelected) { + painter->setBrush(WINUI3Colors[colorSchemeIndex][subtleHighlightColor]); + QWidget *editorWidget = view ? view->indexWidget(view->currentIndex()) : nullptr; + if (editorWidget) { + QPalette pal = editorWidget->palette(); + QColor editorBgColor = vopt->backgroundBrush == Qt::NoBrush ? vopt->palette.color(widget->backgroundRole()) : vopt->backgroundBrush.color(); + editorBgColor.setAlpha(255); + pal.setColor(editorWidget->backgroundRole(),editorBgColor); + editorWidget->setPalette(pal); + } + } else { + painter->setBrush(vopt->backgroundBrush); + } + painter->setPen(Qt::NoPen); + + if (vopt->viewItemPosition == QStyleOptionViewItem::OnlyOne || vopt->viewItemPosition == QStyleOptionViewItem::Invalid) { + painter->drawRoundedRect(vopt->rect.marginsRemoved(QMargins(2,2,2,2)),secondLevelRoundingRadius,secondLevelRoundingRadius); + } else if (vopt->viewItemPosition == QStyleOptionViewItem::Beginning) { + painter->drawRoundedRect(rect.marginsRemoved(QMargins(2,2,0,2)),secondLevelRoundingRadius,secondLevelRoundingRadius); + } else if (vopt->viewItemPosition == QStyleOptionViewItem::End) { + painter->drawRoundedRect(vopt->rect.marginsRemoved(QMargins(0,2,2,2)),secondLevelRoundingRadius,secondLevelRoundingRadius); + } else { + painter->drawRect(vopt->rect.marginsRemoved(QMargins(0,2,0,2))); + } + + // draw the check mark + if (vopt->features & QStyleOptionViewItem::HasCheckIndicator) { + QStyleOptionViewItem option(*vopt); + option.rect = checkRect; + option.state = option.state & ~QStyle::State_HasFocus; + + switch (vopt->checkState) { + case Qt::Unchecked: + option.state |= QStyle::State_Off; + break; + case Qt::PartiallyChecked: + option.state |= QStyle::State_NoChange; + break; + case Qt::Checked: + option.state |= QStyle::State_On; + break; + } + proxy()->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &option, painter, widget); + } + + // draw the icon + QIcon::Mode mode = QIcon::Normal; + if (!(vopt->state & QStyle::State_Enabled)) + mode = QIcon::Disabled; + else if (vopt->state & QStyle::State_Selected) + mode = QIcon::Selected; + QIcon::State state = vopt->state & QStyle::State_Open ? QIcon::On : QIcon::Off; + vopt->icon.paint(painter, iconRect, vopt->decorationAlignment, mode, state); + + painter->setPen(QPen(option->palette.buttonText().color())); + if (!view || !view->isPersistentEditorOpen(vopt->index)) + d->viewItemDrawText(painter, vopt, textRect); + if (vopt->state & State_Selected + && (vopt->viewItemPosition == QStyleOptionViewItem::Beginning + || vopt->viewItemPosition == QStyleOptionViewItem::OnlyOne + || vopt->viewItemPosition == QStyleOptionViewItem::Invalid)) { + if (const QListView *lv = qobject_cast<const QListView *>(widget); + lv && lv->viewMode() != QListView::IconMode) { + painter->setPen(QPen(vopt->palette.accent().color())); + painter->drawLine(option->rect.x(), option->rect.y() + 2, + option->rect.x(),option->rect.y() + option->rect.height() - 2); + painter->drawLine(option->rect.x() + 1, option->rect.y() + 2, + option->rect.x() + 1,option->rect.y() + option->rect.height() - 2); + } + } + } + } + break; + } + default: + QWindowsVistaStyle::drawControl(element, option, painter, widget); + } + painter->restore(); +} + +int QWindows11Style::styleHint(StyleHint hint, const QStyleOption *opt, + const QWidget *widget, QStyleHintReturn *returnData) const { + switch (hint) { + case SH_GroupBox_TextLabelColor: + if (opt!=nullptr && widget!=nullptr) + return opt->palette.text().color().rgba(); + return 0; + case QStyle::SH_ItemView_ShowDecorationSelected: + return 1; + case QStyle::SH_Slider_AbsoluteSetButtons: + return Qt::LeftButton; + case QStyle::SH_Slider_PageSetButtons: + return 0; + default: + return QWindowsVistaStyle::styleHint(hint, opt, widget, returnData); + } +} + +QRect QWindows11Style::subElementRect(QStyle::SubElement element, const QStyleOption *option, + const QWidget *widget) const +{ + QRect ret; + switch (element) { + case QStyle::SE_LineEditContents: + ret = option->rect.adjusted(8,0,-8,0); + break; + case QStyle::SE_ItemViewItemText: + if (const auto *item = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { + const int decorationOffset = item->features.testFlag(QStyleOptionViewItem::HasDecoration) ? item->decorationSize.width() : 0; + if (widget && widget->parentWidget() + && widget->parentWidget()->inherits("QComboBoxPrivateContainer")) { + ret = option->rect.adjusted(decorationOffset + 5, 0, -5, 0); + } else { + ret = QWindowsVistaStyle::subElementRect(element, option, widget); + } + } else { + ret = QWindowsVistaStyle::subElementRect(element, option, widget); + } + break; + default: + ret = QWindowsVistaStyle::subElementRect(element, option, widget); + } + return ret; +} + +/*! + \internal + */ +QRect QWindows11Style::subControlRect(ComplexControl control, const QStyleOptionComplex *option, + SubControl subControl, const QWidget *widget) const +{ + QRect ret; + + switch (control) { +#if QT_CONFIG(spinbox) + case CC_SpinBox: + if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox *>(option)) { + QSize bs; + int fw = spinbox->frame ? proxy()->pixelMetric(PM_SpinBoxFrameWidth, spinbox, widget) : 0; + bs.setHeight(qMax(8, spinbox->rect.height() - fw)); + bs.setWidth(qMin(24.0, spinbox->rect.width()*(1.0/4.0))); + int y = fw + spinbox->rect.y(); + int x, lx, rx; + x = spinbox->rect.x() + spinbox->rect.width() - fw - 2 * bs.width(); + lx = fw; + rx = x - fw; + switch (subControl) { + case SC_SpinBoxUp: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) + return QRect(); + ret = QRect(x, y, bs.width(), bs.height()); + break; + case SC_SpinBoxDown: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) + return QRect(); + ret = QRect(x + bs.width(), y, bs.width(), bs.height()); + break; + case SC_SpinBoxEditField: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) { + ret = QRect(lx, fw, spinbox->rect.width() - 2*fw, spinbox->rect.height() - 2*fw); + } else { + ret = QRect(lx, fw, rx, spinbox->rect.height() - 2*fw); + } + break; + case SC_SpinBoxFrame: + ret = spinbox->rect; + default: + break; + } + ret = visualRect(spinbox->direction, spinbox->rect, ret); + } + break; + case CC_TitleBar: + if (const QStyleOptionTitleBar *titlebar = qstyleoption_cast<const QStyleOptionTitleBar *>(option)) { + SubControl sc = subControl; + ret = QCommonStyle::subControlRect(control, option, subControl, widget); + static constexpr int indent = 3; + static constexpr int controlWidthMargin = 2; + const int controlHeight = titlebar->rect.height(); + const int controlWidth = 46; + const int iconSize = proxy()->pixelMetric(QStyle::PM_TitleBarButtonIconSize, option, widget); + int offset = -(controlWidthMargin + indent); + + bool isMinimized = titlebar->titleBarState & Qt::WindowMinimized; + bool isMaximized = titlebar->titleBarState & Qt::WindowMaximized; + + switch (sc) { + case SC_TitleBarLabel: + if (titlebar->titleBarFlags & (Qt::WindowTitleHint | Qt::WindowSystemMenuHint)) { + ret = titlebar->rect; + if (titlebar->titleBarFlags & Qt::WindowSystemMenuHint) + ret.adjust(iconSize + controlWidthMargin + indent, 0, -controlWidth, 0); + if (titlebar->titleBarFlags & Qt::WindowMinimizeButtonHint) + ret.adjust(0, 0, -controlWidth, 0); + if (titlebar->titleBarFlags & Qt::WindowMaximizeButtonHint) + ret.adjust(0, 0, -controlWidth, 0); + if (titlebar->titleBarFlags & Qt::WindowShadeButtonHint) + ret.adjust(0, 0, -controlWidth, 0); + if (titlebar->titleBarFlags & Qt::WindowContextHelpButtonHint) + ret.adjust(0, 0, -controlWidth, 0); + } + break; + case SC_TitleBarContextHelpButton: + if (titlebar->titleBarFlags & Qt::WindowContextHelpButtonHint) + offset += controlWidth; + Q_FALLTHROUGH(); + case SC_TitleBarMinButton: + if (!isMinimized && (titlebar->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += controlWidth; + else if (sc == SC_TitleBarMinButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarNormalButton: + if (isMinimized && (titlebar->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += controlWidth; + else if (isMaximized && (titlebar->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += controlWidth; + else if (sc == SC_TitleBarNormalButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarMaxButton: + if (!isMaximized && (titlebar->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += controlWidth; + else if (sc == SC_TitleBarMaxButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarShadeButton: + if (!isMinimized && (titlebar->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += controlWidth; + else if (sc == SC_TitleBarShadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarUnshadeButton: + if (isMinimized && (titlebar->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += controlWidth; + else if (sc == SC_TitleBarUnshadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarCloseButton: + if (titlebar->titleBarFlags & Qt::WindowSystemMenuHint) + offset += controlWidth; + else if (sc == SC_TitleBarCloseButton) + break; + ret.setRect(titlebar->rect.right() - offset, titlebar->rect.top(), + controlWidth, controlHeight); + break; + case SC_TitleBarSysMenu: + if (titlebar->titleBarFlags & Qt::WindowSystemMenuHint) { + ret.setRect(titlebar->rect.left() + controlWidthMargin + indent, titlebar->rect.top() + iconSize/2, + iconSize, iconSize); + } + break; + default: + break; + } + if (widget && isMinimized && titlebar->rect.width() < offset) + const_cast<QWidget*>(widget)->resize(controlWidthMargin + indent + offset + iconSize + controlWidthMargin, controlWidth); + ret = visualRect(titlebar->direction, titlebar->rect, ret); + } + break; +#endif // Qt_NO_SPINBOX + case CC_ScrollBar: + { + ret = QCommonStyle::subControlRect(control, option, subControl, widget); + + switch (subControl) { + case QStyle::SC_ScrollBarAddLine: + if (const QStyleOptionSlider *scrollbar = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + if (scrollbar->orientation == Qt::Vertical) { + ret = ret.adjusted(2,2,-2,-3); + } else { + ret = ret.adjusted(3,2,-2,-2); + } + } + break; + case QStyle::SC_ScrollBarSubLine: + if (const QStyleOptionSlider *scrollbar = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + if (scrollbar->orientation == Qt::Vertical) { + ret = ret.adjusted(2,2,-2,-3); + } else { + ret = ret.adjusted(3,2,-2,-2); + } + } + break; + default: + break; + } + break; + } + default: + ret = QWindowsVistaStyle::subControlRect(control, option, subControl, widget); + } + return ret; +} + +/*! + \internal + */ +QSize QWindows11Style::sizeFromContents(ContentsType type, const QStyleOption *option, + const QSize &size, const QWidget *widget) const +{ + QSize contentSize(size); + + switch (type) { + + case CT_Menu: + contentSize += QSize(10, 0); + break; + +#if QT_CONFIG(menubar) + case CT_MenuBarItem: + if (!contentSize.isEmpty()) { + constexpr int hMargin = 2 * 6; + constexpr int hPadding = 2 * 11; + constexpr int itemHeight = 32; + contentSize.setWidth(contentSize.width() + hMargin + hPadding); + contentSize.setHeight(itemHeight); + } + break; +#endif + + default: + contentSize = QWindowsVistaStyle::sizeFromContents(type, option, size, widget); + break; + } + + return contentSize; +} + + +/*! + \internal + */ +int QWindows11Style::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const +{ + int res = 0; + + switch (metric) { + case QStyle::PM_IndicatorWidth: + case QStyle::PM_IndicatorHeight: + case QStyle::PM_ExclusiveIndicatorWidth: + case QStyle::PM_ExclusiveIndicatorHeight: + res = 16; + break; + case QStyle::PM_SliderLength: + res = int(QStyleHelper::dpiScaled(16, option)); + break; + case QStyle::PM_TitleBarButtonIconSize: + res = 16; + break; + case QStyle::PM_TitleBarButtonSize: + res = 32; + break; + case QStyle::PM_ScrollBarExtent: + res = 12; + break; + default: + res = QWindowsVistaStyle::pixelMetric(metric, option, widget); + } + + return res; +} + +void QWindows11Style::polish(QWidget* widget) +{ + QWindowsVistaStyle::polish(widget); + const bool isScrollBar = qobject_cast<QScrollBar *>(widget); + if (isScrollBar || qobject_cast<QMenu *>(widget) || widget->inherits("QComboBoxPrivateContainer")) { + bool wasCreated = widget->testAttribute(Qt::WA_WState_Created); + bool layoutDirection = widget->testAttribute(Qt::WA_RightToLeft); + widget->setAttribute(Qt::WA_OpaquePaintEvent,false); + widget->setAttribute(Qt::WA_TranslucentBackground); + widget->setWindowFlag(Qt::FramelessWindowHint); + widget->setWindowFlag(Qt::NoDropShadowWindowHint); + widget->setAttribute(Qt::WA_RightToLeft, layoutDirection); + widget->setAttribute(Qt::WA_WState_Created, wasCreated); + auto pal = widget->palette(); + pal.setColor(widget->backgroundRole(), Qt::transparent); + widget->setPalette(pal); + if (!isScrollBar) { // for menus and combobox containers... + QGraphicsDropShadowEffect* dropshadow = new QGraphicsDropShadowEffect(widget); + dropshadow->setBlurRadius(3); + dropshadow->setXOffset(3); + dropshadow->setYOffset(3); + widget->setGraphicsEffect(dropshadow); + } + } else if (QComboBox* cb = qobject_cast<QComboBox*>(widget)) { + if (cb->isEditable()) { + QLineEdit *le = cb->lineEdit(); + le->setFrame(false); + } + } else if (widget->inherits("QAbstractButton") || widget->inherits("QToolButton")) { + widget->setAutoFillBackground(false); + } else if (qobject_cast<QGraphicsView *>(widget) && !qobject_cast<QTextEdit *>(widget)) { + QPalette pal = widget->palette(); + pal.setColor(QPalette::Base, pal.window().color()); + widget->setPalette(pal); + } else if (const auto *scrollarea = qobject_cast<QAbstractScrollArea *>(widget); + scrollarea +#if QT_CONFIG(mdiarea) + && !qobject_cast<QMdiArea *>(widget) +#endif + ) { + QPalette pal = scrollarea->viewport()->palette(); + const QPalette originalPalette = pal; + pal.setColor(scrollarea->viewport()->backgroundRole(), Qt::transparent); + scrollarea->viewport()->setPalette(pal); + scrollarea->viewport()->setProperty("_q_original_background_palette", originalPalette); + } else if (qobject_cast<QCommandLinkButton *>(widget)) { + widget->setProperty("_qt_usingVistaStyle",false); + QPalette pal = widget->palette(); + pal.setColor(QPalette::ButtonText, pal.text().color()); + pal.setColor(QPalette::BrightText, pal.text().color()); + widget->setPalette(pal); + } +} + +void QWindows11Style::unpolish(QWidget *widget) +{ + QWindowsVistaStyle::unpolish(widget); + if (const auto *scrollarea = qobject_cast<QAbstractScrollArea *>(widget); + scrollarea +#if QT_CONFIG(mdiarea) + && !qobject_cast<QMdiArea *>(widget) +#endif + ) { + const QPalette pal = scrollarea->viewport()->property("_q_original_background_palette").value<QPalette>(); + scrollarea->viewport()->setPalette(pal); + scrollarea->viewport()->setProperty("_q_original_background_palette", QVariant()); + } +} + +/* +The colors for Windows 11 are taken from the official WinUI3 Figma style at +https://www.figma.com/community/file/1159947337437047524 +*/ +#define SET_IF_UNRESOLVED(GROUP, ROLE, VALUE) \ + if (!result.isBrushSet(QPalette::Inactive, ROLE) || styleSheetChanged) \ + result.setColor(GROUP, ROLE, VALUE) + +static void populateLightSystemBasePalette(QPalette &result) +{ + static QString oldStyleSheet; + const bool styleSheetChanged = oldStyleSheet != qApp->styleSheet(); + + const QColor textColor = QColor(0x00,0x00,0x00,0xE4); + const QColor btnFace = QColor(0xFF,0xFF,0xFF,0xB3); + const QColor btnHighlight = result.accent().color(); + const QColor btnColor = result.button().color(); + + SET_IF_UNRESOLVED(QPalette::All, QPalette::Highlight, btnHighlight); + SET_IF_UNRESOLVED(QPalette::All, QPalette::WindowText, textColor); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Button, btnFace); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Light, btnColor.lighter(150)); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Dark, btnColor.darker(200)); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Mid, btnColor.darker(150)); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Text, textColor); + SET_IF_UNRESOLVED(QPalette::All, QPalette::BrightText, btnHighlight); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Base, btnFace); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Window, QColor(0xF3,0xF3,0xF3,0xFF)); + SET_IF_UNRESOLVED(QPalette::All, QPalette::ButtonText, textColor); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Midlight, btnColor.lighter(125)); + SET_IF_UNRESOLVED(QPalette::All, QPalette::Shadow, Qt::black); + SET_IF_UNRESOLVED(QPalette::All, QPalette::ToolTipBase, result.window().color()); + SET_IF_UNRESOLVED(QPalette::All, QPalette::ToolTipText, result.windowText().color()); + + if (result.midlight() == result.button()) + result.setColor(QPalette::Midlight, btnColor.lighter(110)); + oldStyleSheet = qApp->styleSheet(); +} + +/*! + \internal + */ +void QWindows11Style::polish(QPalette& result) +{ + highContrastTheme = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Unknown; + colorSchemeIndex = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Light ? 0 : 1; + + if (!highContrastTheme && colorSchemeIndex == 0) + populateLightSystemBasePalette(result); + + const bool styleSheetChanged = false; // so the macro works + + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Button, result.button().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Window, result.window().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Light, result.light().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Dark, result.dark().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Accent, result.accent().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Highlight, result.highlight().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::HighlightedText, result.highlightedText().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::Text, result.text().color()); + SET_IF_UNRESOLVED(QPalette::Inactive, QPalette::WindowText, result.windowText().color()); + + if (highContrastTheme) + result.setColor(QPalette::Active, QPalette::HighlightedText, result.windowText().color()); +} + +#undef SET_IF_UNRESOLVED + +QT_END_NAMESPACE diff --git a/src/plugins/styles/modernwindows/qwindows11style_p.h b/src/plugins/styles/modernwindows/qwindows11style_p.h new file mode 100644 index 0000000000..9c54afd967 --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindows11style_p.h @@ -0,0 +1,70 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWS11STYLE_P_H +#define QWINDOWS11STYLE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtWidgets/private/qtwidgetsglobal_p.h> +#include <qwindowsvistastyle_p_p.h> + +QT_BEGIN_NAMESPACE + +class QWindows11StylePrivate; +class QWindows11Style; + +class QWindows11Style : public QWindowsVistaStyle +{ + Q_OBJECT +public: + QWindows11Style(); + ~QWindows11Style() override; + void drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, + QPainter *painter, const QWidget *widget) const override; + void drawPrimitive(PrimitiveElement element, const QStyleOption *option, + QPainter *painter, const QWidget *widget) const override; + QRect subElementRect(QStyle::SubElement element, const QStyleOption *option, + const QWidget *widget = nullptr) const override; + QRect subControlRect(ComplexControl control, const QStyleOptionComplex *option, + SubControl subControl, const QWidget *widget) const override; + void drawControl(ControlElement element, const QStyleOption *option, + QPainter *painter, const QWidget *widget) const override; + int styleHint(StyleHint hint, const QStyleOption *opt = nullptr, + const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override; + void polish(QWidget* widget) override; + + QSize sizeFromContents(ContentsType type, const QStyleOption *option, + const QSize &size, const QWidget *widget) const override; + int pixelMetric(PixelMetric metric, const QStyleOption *option = nullptr, + const QWidget *widget = nullptr) const override; + void polish(QPalette &pal) override; + void unpolish(QWidget *widget) override; +protected: + QWindows11Style(QWindows11StylePrivate &dd); +private: + Q_DISABLE_COPY_MOVE(QWindows11Style) + Q_DECLARE_PRIVATE(QWindows11Style) + friend class QStyleFactory; + + bool highContrastTheme = false; + int colorSchemeIndex = 0; + const QFont assetFont = QFont("Segoe Fluent Icons"); //Font to load icons from +}; + +class QWindows11StylePrivate : public QWindowsVistaStylePrivate { + Q_DECLARE_PUBLIC(QWindows11Style) +}; + +QT_END_NAMESPACE + +#endif // QWINDOWS11STYLE_P_H diff --git a/src/plugins/styles/modernwindows/qwindowsthemedata.cpp b/src/plugins/styles/modernwindows/qwindowsthemedata.cpp new file mode 100644 index 0000000000..44569e054d --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindowsthemedata.cpp @@ -0,0 +1,59 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsthemedata_p.h" +#include "qwindowsvistastyle_p_p.h" + +/* \internal + Returns \c true if the QWindowsThemeData is valid for use. +*/ +bool QWindowsThemeData::isValid() +{ + return QWindowsVistaStylePrivate::useVista() && theme >= 0 && handle(); +} + +/* \internal + Returns the theme engine handle to the specific class. + If the handle hasn't been opened before, it opens the data, and + adds it to a static map, for caching. +*/ +HTHEME QWindowsThemeData::handle() +{ + if (!QWindowsVistaStylePrivate::useVista()) + return nullptr; + + if (!htheme) + htheme = QWindowsVistaStylePrivate::createTheme(theme, QWindowsVistaStylePrivate::winId(widget)); + return htheme; +} + +/* \internal + Converts a QRect to the native RECT structure. +*/ +RECT QWindowsThemeData::toRECT(const QRect &qr) +{ + RECT r; + r.left = qr.x(); + r.right = qr.x() + qr.width(); + r.top = qr.y(); + r.bottom = qr.y() + qr.height(); + return r; +} + +/* \internal + Returns the native region of a part, if the part is considered + transparent. The region is scaled to the parts size (rect). +*/ +HRGN QWindowsThemeData::mask(QWidget *widget) +{ + if (!IsThemeBackgroundPartiallyTransparent(handle(), partId, stateId)) + return nullptr; + + HRGN hrgn; + HDC dc = nullptr; + if (widget) + dc = QWindowsVistaStylePrivate::hdcForWidgetBackingStore(widget); + RECT nativeRect = toRECT(rect); + GetThemeBackgroundRegion(handle(), dc, partId, stateId, &nativeRect, &hrgn); + return hrgn; +} diff --git a/src/plugins/styles/modernwindows/qwindowsthemedata_p.h b/src/plugins/styles/modernwindows/qwindowsthemedata_p.h new file mode 100644 index 0000000000..5de2bcbdea --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindowsthemedata_p.h @@ -0,0 +1,183 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSTHEMEDATA_P_H +#define QWINDOWSTHEMEDATA_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qwidget.h> +#include <qt_windows.h> +#include <uxtheme.h> +#include <vssym32.h> +#include <limits.h> + + +// TMT_TEXTSHADOWCOLOR is wrongly defined in mingw +#if TMT_TEXTSHADOWCOLOR != 3818 +#undef TMT_TEXTSHADOWCOLOR +#define TMT_TEXTSHADOWCOLOR 3818 +#endif +#ifndef TST_NONE +# define TST_NONE 0 +#endif + +// These defines are missing from the tmschema, but still exist as +// states for their parts +#ifndef MINBS_INACTIVE +#define MINBS_INACTIVE 5 +#endif +#ifndef MAXBS_INACTIVE +#define MAXBS_INACTIVE 5 +#endif +#ifndef RBS_INACTIVE +#define RBS_INACTIVE 5 +#endif +#ifndef HBS_INACTIVE +#define HBS_INACTIVE 5 +#endif +#ifndef CBS_INACTIVE +#define CBS_INACTIVE 5 +#endif + +// Declarations ----------------------------------------------------------------------------------- +class QWindowsThemeData +{ +public: + explicit QWindowsThemeData(const QWidget *w = nullptr, QPainter *p = nullptr, int themeIn = -1, + int part = 0, int state = 0, const QRect &r = QRect()) + : widget(w), painter(p), theme(themeIn), partId(part), stateId(state), + mirrorHorizontally(false), mirrorVertically(false), noBorder(false), + noContent(false), invertPixels(false), rect(r) + {} + + HRGN mask(QWidget *widget); + HTHEME handle(); + bool isValid(); + QSizeF size(); + + static QSizeF themeSize(const QWidget *w = nullptr, QPainter *p = nullptr, + int themeIn = -1, int part = 0, int state = 0); + static RECT toRECT(const QRect &qr); + + QMarginsF margins(const QRect &rect, int propId = TMT_CONTENTMARGINS); + QMarginsF margins(int propId = TMT_CONTENTMARGINS); + + const QWidget *widget; + QPainter *painter; + + int theme; + HTHEME htheme = nullptr; + int partId; + int stateId; + + uint mirrorHorizontally : 1; + uint mirrorVertically : 1; + uint noBorder : 1; + uint noContent : 1; + uint invertPixels : 1; + uint rotate = 0; + QRect rect; +}; + +struct ThemeMapKey { + int theme = 0; + int partId = -1; + int stateId = -1; + bool noBorder = false; + bool noContent = false; + + ThemeMapKey() = default; + ThemeMapKey(const QWindowsThemeData &data) + : theme(data.theme), partId(data.partId), stateId(data.stateId), + noBorder(data.noBorder), noContent(data.noContent) {} + +}; + +inline size_t qHash(const ThemeMapKey &key) +{ return key.theme ^ key.partId ^ key.stateId; } + +inline bool operator==(const ThemeMapKey &k1, const ThemeMapKey &k2) +{ + return k1.theme == k2.theme + && k1.partId == k2.partId + && k1.stateId == k2.stateId; +} + +enum AlphaChannelType { + UnknownAlpha = -1, // Alpha of part & state not yet known + NoAlpha, // Totally opaque, no need to touch alpha (RGB) + MaskAlpha, // Alpha channel must be fixed (ARGB) + RealAlpha // Proper alpha values from Windows (ARGB_Premultiplied) +}; + +struct ThemeMapData { + AlphaChannelType alphaType = UnknownAlpha; // Which type of alpha on part & state + + bool dataValid : 1; // Only used to detect if hash value is ok + bool partIsTransparent : 1; + bool hasAlphaChannel : 1; // True = part & state has real Alpha + bool wasAlphaSwapped : 1; // True = alpha channel needs to be swapped + bool hadInvalidAlpha : 1; // True = alpha channel contained invalid alpha values + + ThemeMapData() : dataValid(false), partIsTransparent(false), + hasAlphaChannel(false), wasAlphaSwapped(false), hadInvalidAlpha(false) {} +}; + + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QWindowsThemeData &t); +QDebug operator<<(QDebug d, const ThemeMapKey &k); +QDebug operator<<(QDebug d, const ThemeMapData &td); +#endif + +inline QSizeF QWindowsThemeData::size() +{ + QSizeF result(0, 0); + if (isValid()) { + SIZE size; + if (SUCCEEDED(GetThemePartSize(handle(), nullptr, partId, stateId, nullptr, TS_TRUE, &size))) + result = QSize(size.cx, size.cy); + } + return result; +} + +inline QSizeF QWindowsThemeData::themeSize(const QWidget *w, QPainter *p, int themeIn, int part, int state) +{ + QWindowsThemeData theme(w, p, themeIn, part, state); + return theme.size(); +} + +inline QMarginsF QWindowsThemeData::margins(const QRect &qRect, int propId) +{ + QMarginsF result(0, 0, 0 ,0); + if (isValid()) { + MARGINS margins; + RECT rect = QWindowsThemeData::toRECT(qRect); + if (SUCCEEDED(GetThemeMargins(handle(), nullptr, partId, stateId, propId, &rect, &margins))) + result = QMargins(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight); + } + return result; +} + +inline QMarginsF QWindowsThemeData::margins(int propId) +{ + QMarginsF result(0, 0, 0 ,0); + if (isValid()) { + MARGINS margins; + if (SUCCEEDED(GetThemeMargins(handle(), nullptr, partId, stateId, propId, nullptr, &margins))) + result = QMargins(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight); + } + return result; +} + +#endif // QWINDOWSTHEMEDATA_P_H diff --git a/src/plugins/styles/modernwindows/qwindowsvistaanimation.cpp b/src/plugins/styles/modernwindows/qwindowsvistaanimation.cpp new file mode 100644 index 0000000000..ac9a5ad8c0 --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindowsvistaanimation.cpp @@ -0,0 +1,15 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsvistaanimation_p.h" +#include "qwindowsvistastyle_p_p.h" + +bool QWindowsVistaAnimation::isUpdateNeeded() const +{ + return QWindowsVistaStylePrivate::useVista(); +} + +void QWindowsVistaAnimation::paint(QPainter *painter, const QStyleOption *option) +{ + painter->drawImage(option->rect, currentImage()); +} diff --git a/src/plugins/styles/modernwindows/qwindowsvistaanimation_p.h b/src/plugins/styles/modernwindows/qwindowsvistaanimation_p.h new file mode 100644 index 0000000000..817fa5f4b5 --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindowsvistaanimation_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSVISTAANIMATION_P_H +#define QWINDOWSVISTAANIMATION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qstyleanimation_p.h> +#include <QtWidgets/private/qwindowsstyle_p.h> + +class QWindowsVistaAnimation : public QBlendStyleAnimation +{ + Q_OBJECT +public: + QWindowsVistaAnimation(Type type, QObject *target) : QBlendStyleAnimation(type, target) { } + + bool isUpdateNeeded() const override; + void paint(QPainter *painter, const QStyleOption *option); +}; + + +// Handles state transition animations +class QWindowsVistaTransition : public QWindowsVistaAnimation +{ + Q_OBJECT +public: + QWindowsVistaTransition(QObject *target) : QWindowsVistaAnimation(Transition, target) {} +}; + + +// Handles pulse animations (default buttons) +class QWindowsVistaPulse: public QWindowsVistaAnimation +{ + Q_OBJECT +public: + QWindowsVistaPulse(QObject *target) : QWindowsVistaAnimation(Pulse, target) {} +}; + +#endif // QWINDOWSVISTAANIMATION_P_H diff --git a/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp b/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp new file mode 100644 index 0000000000..beee1d6f31 --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp @@ -0,0 +1,5012 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsvistastyle_p.h" +#include "qwindowsvistastyle_p_p.h" +#include "qwindowsvistaanimation_p.h" +#include <qoperatingsystemversion.h> +#include <qscreen.h> +#include <qstylehints.h> +#include <qwindow.h> +#include <private/qstyleanimation_p.h> +#include <private/qstylehelper_p.h> +#include <qpa/qplatformnativeinterface.h> +#include <private/qapplication_p.h> +#include <private/qsystemlibrary_p.h> +#include <private/qwindowsthemecache_p.h> + +#include "qdrawutil.h" // for now +#include <qbackingstore.h> + + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static const int windowsItemFrame = 2; // menu item frame width +static const int windowsItemHMargin = 3; // menu item hor text margin +static const int windowsItemVMargin = 4; // menu item ver text margin +static const int windowsArrowHMargin = 6; // arrow horizontal margin +static const int windowsRightBorder = 15; // right border on windows + +#ifndef TMT_CONTENTMARGINS +# define TMT_CONTENTMARGINS 3602 +#endif +#ifndef TMT_SIZINGMARGINS +# define TMT_SIZINGMARGINS 3601 +#endif +#ifndef LISS_NORMAL +# define LISS_NORMAL 1 +# define LISS_HOT 2 +# define LISS_SELECTED 3 +# define LISS_DISABLED 4 +# define LISS_SELECTEDNOTFOCUS 5 +# define LISS_HOTSELECTED 6 +#endif +#ifndef BP_COMMANDLINK +# define BP_COMMANDLINK 6 +# define BP_COMMANDLINKGLYPH 7 +# define CMDLGS_NORMAL 1 +# define CMDLGS_HOT 2 +# define CMDLGS_PRESSED 3 +# define CMDLGS_DISABLED 4 +#endif + +// QWindowsVistaStylePrivate ------------------------------------------------------------------------- +// Static initializations +HWND QWindowsVistaStylePrivate::m_vistaTreeViewHelper = nullptr; +bool QWindowsVistaStylePrivate::useVistaTheme = false; +Q_CONSTINIT QBasicAtomicInt QWindowsVistaStylePrivate::ref = Q_BASIC_ATOMIC_INITIALIZER(-1); // -1 based refcounting + +static void qt_add_rect(HRGN &winRegion, QRect r) +{ + HRGN rgn = CreateRectRgn(r.left(), r.top(), r.x() + r.width(), r.y() + r.height()); + if (rgn) { + HRGN dest = CreateRectRgn(0,0,0,0); + int result = CombineRgn(dest, winRegion, rgn, RGN_OR); + if (result) { + DeleteObject(winRegion); + winRegion = dest; + } + DeleteObject(rgn); + } +} + +static HRGN qt_hrgn_from_qregion(const QRegion ®ion) +{ + HRGN hRegion = CreateRectRgn(0,0,0,0); + if (region.rectCount() == 1) { + qt_add_rect(hRegion, region.boundingRect()); + return hRegion; + } + for (const QRect &rect : region) + qt_add_rect(hRegion, rect); + return hRegion; +} + +static inline Qt::Orientation progressBarOrientation(const QStyleOption *option = nullptr) +{ + if (const auto *pb = qstyleoption_cast<const QStyleOptionProgressBar *>(option)) + return pb->state & QStyle::State_Horizontal ? Qt::Horizontal : Qt::Vertical; + return Qt::Horizontal; +} + +/* In order to obtain the correct VistaTreeViewTheme (arrows for PE_IndicatorBranch), + * we need to set the windows "explorer" theme explicitly on a native + * window and open the "TREEVIEW" theme handle passing its window handle + * in order to get Vista-style item view themes (particularly drawBackground() + * for selected items needs this). + * We invoke a service of the native Windows interface to create + * a non-visible window handle, open the theme on it and insert it into + * the cache so that it is found by QWindowsThemeData::handle() first. + */ +static inline HWND createTreeViewHelperWindow() +{ + using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; + + HWND result = nullptr; + if (auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration())) + result = nativeWindowsApp->createMessageWindow(QStringLiteral("QTreeViewThemeHelperWindowClass"), + QStringLiteral("QTreeViewThemeHelperWindow")); + return result; +} + +enum TransformType { SimpleTransform, HighDpiScalingTransform, ComplexTransform }; + +static inline TransformType transformType(const QTransform &transform, qreal devicePixelRatio) +{ + if (transform.type() <= QTransform::TxTranslate) + return SimpleTransform; + if (transform.type() > QTransform::TxScale) + return ComplexTransform; + return qFuzzyCompare(transform.m11(), devicePixelRatio) + && qFuzzyCompare(transform.m22(), devicePixelRatio) + ? HighDpiScalingTransform : ComplexTransform; +} + +// QTBUG-60571: Exclude known fully opaque theme parts which produce values +// invalid in ARGB32_Premultiplied (for example, 0x00ffffff). +static inline bool isFullyOpaque(const QWindowsThemeData &themeData) +{ + return themeData.theme == QWindowsVistaStylePrivate::TaskDialogTheme && themeData.partId == TDLG_PRIMARYPANEL; +} + +static inline QRectF scaleRect(const QRectF &r, qreal factor) +{ + return r.isValid() && factor > 1 + ? QRectF(r.topLeft() * factor, r.size() * factor) : r; +} + +static QRegion scaleRegion(const QRegion ®ion, qreal factor) +{ + if (region.isEmpty() || qFuzzyCompare(factor, qreal(1))) + return region; + QRegion result; + for (const QRect &rect : region) + result += QRectF(QPointF(rect.topLeft()) * factor, QSizeF(rect.size() * factor)).toRect(); + return result; +} + + +/* \internal + Checks if the theme engine can/should be used, or if we should fall back + to Windows style. For Windows 10, this will still return false for the + High Contrast themes. +*/ +bool QWindowsVistaStylePrivate::useVista(bool update) +{ + if (update) + useVistaTheme = IsThemeActive() && (IsAppThemed() || !QCoreApplication::instance()); + return useVistaTheme; +} + +/* \internal + Handles refcounting, and queries the theme engine for usage. +*/ +void QWindowsVistaStylePrivate::init(bool force) +{ + if (ref.ref() && !force) + return; + if (!force) // -1 based atomic refcounting + ref.ref(); + + useVista(true); +} + +/* \internal + Cleans up all static data. +*/ +void QWindowsVistaStylePrivate::cleanup(bool force) +{ + if (bufferBitmap) { + if (bufferDC && nullBitmap) + SelectObject(bufferDC, nullBitmap); + DeleteObject(bufferBitmap); + bufferBitmap = nullptr; + } + + if (bufferDC) + DeleteDC(bufferDC); + bufferDC = nullptr; + + if (ref.deref() && !force) + return; + if (!force) // -1 based atomic refcounting + ref.deref(); + + useVistaTheme = false; + cleanupHandleMap(); +} + +bool QWindowsVistaStylePrivate::transitionsEnabled() const +{ + BOOL animEnabled = false; + if (SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &animEnabled, 0)) + { + if (animEnabled) + return true; + } + return false; +} + +int QWindowsVistaStylePrivate::pixelMetricFromSystemDp(QStyle::PixelMetric pm, const QStyleOption *option, const QWidget *widget) +{ + switch (pm) { + case QStyle::PM_IndicatorWidth: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::ButtonTheme, BP_CHECKBOX, CBS_UNCHECKEDNORMAL).width(); + case QStyle::PM_IndicatorHeight: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::ButtonTheme, BP_CHECKBOX, CBS_UNCHECKEDNORMAL).height(); + case QStyle::PM_ExclusiveIndicatorWidth: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::ButtonTheme, BP_RADIOBUTTON, RBS_UNCHECKEDNORMAL).width(); + case QStyle::PM_ExclusiveIndicatorHeight: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::ButtonTheme, BP_RADIOBUTTON, RBS_UNCHECKEDNORMAL).height(); + case QStyle::PM_ProgressBarChunkWidth: + return progressBarOrientation(option) == Qt::Horizontal + ? QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::ProgressTheme, PP_CHUNK).width() + : QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::ProgressTheme, PP_CHUNKVERT).height(); + case QStyle::PM_SliderThickness: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::TrackBarTheme, TKP_THUMB).height(); + case QStyle::PM_TitleBarHeight: + return QWindowsStylePrivate::pixelMetricFromSystemDp(QStyle::PM_TitleBarHeight, option, widget); + case QStyle::PM_MdiSubWindowFrameWidth: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::WindowTheme, WP_FRAMELEFT, FS_ACTIVE).width(); + case QStyle::PM_DockWidgetFrameWidth: + return QWindowsThemeData::themeSize(widget, nullptr, QWindowsVistaStylePrivate::WindowTheme, WP_SMALLFRAMERIGHT, FS_ACTIVE).width(); + default: + break; + } + return QWindowsVistaStylePrivate::InvalidMetric; +} + +int QWindowsVistaStylePrivate::fixedPixelMetric(QStyle::PixelMetric pm) +{ + switch (pm) { + case QStyle::PM_DockWidgetTitleBarButtonMargin: + return 5; + case QStyle::PM_ScrollBarSliderMin: + return 18; + case QStyle::PM_MenuHMargin: + case QStyle::PM_MenuVMargin: + return 0; + case QStyle::PM_MenuPanelWidth: + return 3; + default: + break; + } + + return QWindowsVistaStylePrivate::InvalidMetric; +} + +bool QWindowsVistaStylePrivate::initVistaTreeViewTheming() +{ + if (m_vistaTreeViewHelper) + return true; + + m_vistaTreeViewHelper = createTreeViewHelperWindow(); + if (!m_vistaTreeViewHelper) { + qWarning("Unable to create the treeview helper window."); + return false; + } + if (FAILED(SetWindowTheme(m_vistaTreeViewHelper, L"explorer", nullptr))) { + qErrnoWarning("SetWindowTheme() failed."); + cleanupVistaTreeViewTheming(); + return false; + } + return true; +} + +void QWindowsVistaStylePrivate::cleanupVistaTreeViewTheming() +{ + if (m_vistaTreeViewHelper) { + DestroyWindow(m_vistaTreeViewHelper); + m_vistaTreeViewHelper = nullptr; + } +} + +/* \internal + Closes all open theme data handles to ensure that we don't leak + resources, and that we don't refer to old handles when for + example the user changes the theme style. +*/ +void QWindowsVistaStylePrivate::cleanupHandleMap() +{ + QWindowsThemeCache::clearAllThemeCaches(); + QWindowsVistaStylePrivate::cleanupVistaTreeViewTheming(); +} + +HTHEME QWindowsVistaStylePrivate::createTheme(int theme, HWND hwnd) +{ + if (theme == VistaTreeViewTheme && QWindowsVistaStylePrivate::initVistaTreeViewTheming()) + hwnd = QWindowsVistaStylePrivate::m_vistaTreeViewHelper; + return QWindowsThemeCache::createTheme(theme, hwnd); +} + +QBackingStore *QWindowsVistaStylePrivate::backingStoreForWidget(const QWidget *widget) +{ + if (QBackingStore *backingStore = widget->backingStore()) + return backingStore; + if (const QWidget *topLevel = widget->nativeParentWidget()) + if (QBackingStore *topLevelBackingStore = topLevel->backingStore()) + return topLevelBackingStore; + return nullptr; +} + +HDC QWindowsVistaStylePrivate::hdcForWidgetBackingStore(const QWidget *widget) +{ + if (QBackingStore *backingStore = backingStoreForWidget(widget)) { + QPlatformNativeInterface *nativeInterface = QGuiApplication::platformNativeInterface(); + if (nativeInterface) + return static_cast<HDC>(nativeInterface->nativeResourceForBackingStore(QByteArrayLiteral("getDC"), backingStore)); + } + return nullptr; +} + +QString QWindowsVistaStylePrivate::themeName(int theme) +{ + return QWindowsThemeCache::themeName(theme); +} + +bool QWindowsVistaStylePrivate::isItemViewDelegateLineEdit(const QWidget *widget) +{ + if (!widget) + return false; + const QWidget *parent1 = widget->parentWidget(); + // Exclude dialogs or other toplevels parented on item views. + if (!parent1 || parent1->isWindow()) + return false; + const QWidget *parent2 = parent1->parentWidget(); + return parent2 && widget->inherits("QLineEdit") + && parent2->inherits("QAbstractItemView"); +} + +// Returns whether base color is set for this widget +bool QWindowsVistaStylePrivate::isLineEditBaseColorSet(const QStyleOption *option, const QWidget *widget) +{ + uint resolveMask = option->palette.resolveMask(); + if (widget) { + // Since spin box includes a line edit we need to resolve the palette mask also from + // the parent, as while the color is always correct on the palette supplied by panel, + // the mask can still be empty. If either mask specifies custom base color, use that. +#if QT_CONFIG(spinbox) + if (const QAbstractSpinBox *spinbox = qobject_cast<QAbstractSpinBox*>(widget->parentWidget())) + resolveMask |= spinbox->palette().resolveMask(); +#endif // QT_CONFIG(spinbox) + } + return (resolveMask & (1 << QPalette::Base)) != 0; +} + +/*! \internal + This function will always return a valid window handle, and might + create a limbo widget to do so. + We often need a window handle to for example open theme data, so + this function ensures that we get one. +*/ +HWND QWindowsVistaStylePrivate::winId(const QWidget *widget) +{ + if (widget) { + if (const HWND hwnd = QApplicationPrivate::getHWNDForWidget(const_cast<QWidget *>(widget))) + return hwnd; + } + + // Find top level with native window (there might be dialogs that do not have one). + const auto allWindows = QGuiApplication::allWindows(); + for (const QWindow *window : allWindows) { + if (window->isTopLevel() && window->type() != Qt::Desktop && window->handle() != nullptr) + return reinterpret_cast<HWND>(window->winId()); + } + + return GetDesktopWindow(); +} + +/*! \internal + Returns a native buffer (DIB section) of at least the size of + ( \a x , \a y ). The buffer has a 32 bit depth, to not lose + the alpha values on proper alpha-pixmaps. +*/ +HBITMAP QWindowsVistaStylePrivate::buffer(int w, int h) +{ + // If we already have a HBITMAP which is of adequate size, just return that + if (bufferBitmap) { + if (bufferW >= w && bufferH >= h) + return bufferBitmap; + // Not big enough, discard the old one + if (bufferDC && nullBitmap) + SelectObject(bufferDC, nullBitmap); + DeleteObject(bufferBitmap); + bufferBitmap = nullptr; + } + + w = qMax(bufferW, w); + h = qMax(bufferH, h); + + if (!bufferDC) { + HDC displayDC = GetDC(nullptr); + bufferDC = CreateCompatibleDC(displayDC); + ReleaseDC(nullptr, displayDC); + } + + // Define the header + BITMAPINFO bmi; + memset(&bmi, 0, sizeof(bmi)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = w; + bmi.bmiHeader.biHeight = -h; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + // Create the pixmap + bufferPixels = nullptr; + bufferBitmap = CreateDIBSection(bufferDC, &bmi, DIB_RGB_COLORS, reinterpret_cast<void **>(&bufferPixels), nullptr, 0); + GdiFlush(); + nullBitmap = static_cast<HBITMAP>(SelectObject(bufferDC, bufferBitmap)); + + if (Q_UNLIKELY(!bufferBitmap)) { + qErrnoWarning("QWindowsVistaStylePrivate::buffer(%dx%d), CreateDIBSection() failed.", w, h); + bufferW = 0; + bufferH = 0; + return nullptr; + } + if (Q_UNLIKELY(!bufferPixels)) { + qErrnoWarning("QWindowsVistaStylePrivate::buffer(%dx%d), CreateDIBSection() did not allocate pixel data.", w, h); + bufferW = 0; + bufferH = 0; + return nullptr; + } + bufferW = w; + bufferH = h; +#ifdef DEBUG_XP_STYLE + qDebug("Creating new dib section (%d, %d)", w, h); +#endif + return bufferBitmap; +} + +/*! \internal + Returns \c true if the part contains any transparency at all. This does + not indicate what kind of transparency we're dealing with. It can be + - Alpha transparency + - Masked transparency +*/ +bool QWindowsVistaStylePrivate::isTransparent(QWindowsThemeData &themeData) +{ + return IsThemeBackgroundPartiallyTransparent(themeData.handle(), themeData.partId, + themeData.stateId); +} + + +/*! \internal + Returns a QRegion of the region of the part +*/ +QRegion QWindowsVistaStylePrivate::region(QWindowsThemeData &themeData) +{ + HRGN hRgn = nullptr; + const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(themeData.widget); + RECT rect = themeData.toRECT(QRect(themeData.rect.topLeft() / factor, themeData.rect.size() / factor)); + if (!SUCCEEDED(GetThemeBackgroundRegion(themeData.handle(), bufferHDC(), themeData.partId, + themeData.stateId, &rect, &hRgn))) { + return QRegion(); + } + + HRGN dest = CreateRectRgn(0, 0, 0, 0); + const bool success = CombineRgn(dest, hRgn, nullptr, RGN_COPY) != ERROR; + + QRegion region; + + if (success) { + QVarLengthArray<char> buf(256); + RGNDATA *rd = reinterpret_cast<RGNDATA *>(buf.data()); + if (GetRegionData(dest, buf.size(), rd) == 0) { + const auto numBytes = GetRegionData(dest, 0, nullptr); + if (numBytes > 0) { + buf.resize(numBytes); + rd = reinterpret_cast<RGNDATA *>(buf.data()); + if (GetRegionData(dest, numBytes, rd) == 0) + rd = nullptr; + } else { + rd = nullptr; + } + } + if (rd) { + RECT *r = reinterpret_cast<RECT *>(rd->Buffer); + for (uint i = 0; i < rd->rdh.nCount; ++i) { + QRect rect; + rect.setCoords(int(r->left * factor), int(r->top * factor), + int((r->right - 1) * factor), int((r->bottom - 1) * factor)); + ++r; + region |= rect; + } + } + } + + DeleteObject(hRgn); + DeleteObject(dest); + + return region; +} + +/*! \internal + Returns \c true if the native doublebuffer contains pixels with + varying alpha value. +*/ +bool QWindowsVistaStylePrivate::hasAlphaChannel(const QRect &rect) +{ + const int startX = rect.left(); + const int startY = rect.top(); + const int w = rect.width(); + const int h = rect.height(); + + int firstAlpha = -1; + for (int y = startY; y < h/2; ++y) { + auto buffer = reinterpret_cast<const DWORD *>(bufferPixels) + (y * bufferW); + for (int x = startX; x < w; ++x, ++buffer) { + int alpha = (*buffer) >> 24; + if (firstAlpha == -1) + firstAlpha = alpha; + else if (alpha != firstAlpha) + return true; + } + } + return false; +} + +/*! \internal + When the theme engine paints both a true alpha pixmap and a glyph + into our buffer, the glyph might not contain a proper alpha value. + The rule of thumb for premultiplied pixmaps is that the color + values of a pixel can never be higher than the alpha values, so + we use this to our advantage here, and fix all instances where + this occurs. +*/ +bool QWindowsVistaStylePrivate::fixAlphaChannel(const QRect &rect) +{ + const int startX = rect.left(); + const int startY = rect.top(); + const int w = rect.width(); + const int h = rect.height(); + bool hasFixedAlphaValue = false; + + for (int y = startY; y < h; ++y) { + auto buffer = reinterpret_cast<DWORD *>(bufferPixels) + (y * bufferW); + for (int x = startX; x < w; ++x, ++buffer) { + uint pixel = *buffer; + int alpha = qAlpha(pixel); + if (qRed(pixel) > alpha || qGreen(pixel) > alpha || qBlue(pixel) > alpha) { + *buffer |= 0xff000000; + hasFixedAlphaValue = true; + } + } + } + return hasFixedAlphaValue; +} + +/*! \internal + Swaps the alpha values on certain pixels: + 0xFF?????? -> 0x00?????? + 0x00?????? -> 0xFF?????? + Used to determine the mask of a non-alpha transparent pixmap in + the native doublebuffer, and swap the alphas so we may paint + the image as a Premultiplied QImage with drawImage(), and obtain + the mask transparency. +*/ +bool QWindowsVistaStylePrivate::swapAlphaChannel(const QRect &rect, bool allPixels) +{ + const int startX = rect.left(); + const int startY = rect.top(); + const int w = rect.width(); + const int h = rect.height(); + bool valueChange = false; + + // Flip the alphas, so that 255-alpha pixels are 0, and 0-alpha are 255. + for (int y = startY; y < h; ++y) { + auto buffer = reinterpret_cast<DWORD *>(bufferPixels) + (y * bufferW); + for (int x = startX; x < w; ++x, ++buffer) { + if (allPixels) { + *buffer |= 0xFF000000; + continue; + } + unsigned int alphaValue = (*buffer) & 0xFF000000; + if (alphaValue == 0xFF000000) { + *buffer = 0; + valueChange = true; + } else if (alphaValue == 0) { + *buffer |= 0xFF000000; + valueChange = true; + } + } + } + return valueChange; +} + +/*! \internal + Main theme drawing function. + Determines the correct lowlevel drawing method depending on several + factors. + Use drawBackgroundThruNativeBuffer() if: + - Painter does not have an HDC + - Theme part is flipped (mirrored horizontally) + else use drawBackgroundDirectly(). + \note drawBackgroundThruNativeBuffer() can return false for large + sizes due to buffer()/CreateDIBSection() failing. +*/ +bool QWindowsVistaStylePrivate::drawBackground(QWindowsThemeData &themeData, qreal correctionFactor) +{ + if (themeData.rect.isEmpty()) + return true; + + QPainter *painter = themeData.painter; + Q_ASSERT_X(painter != nullptr, "QWindowsVistaStylePrivate::drawBackground()", "Trying to draw a theme part without a painter"); + if (!painter || !painter->isActive()) + return false; + + painter->save(); + + // Access paintDevice via engine since the painter may + // return the clip device which can still be a widget device in case of grabWidget(). + + bool translucentToplevel = false; + const QPaintDevice *paintDevice = painter->device(); + const qreal aditionalDevicePixelRatio = themeData.widget ? themeData.widget->devicePixelRatio() : qreal(1); + if (paintDevice->devType() == QInternal::Widget) { + const QWidget *window = static_cast<const QWidget *>(paintDevice)->window(); + translucentToplevel = window->testAttribute(Qt::WA_TranslucentBackground); + } + + const TransformType tt = transformType(painter->deviceTransform(), aditionalDevicePixelRatio); + + bool canDrawDirectly = false; + if (themeData.widget && painter->opacity() == 1.0 && !themeData.rotate + && !isFullyOpaque(themeData) + && tt != ComplexTransform && !themeData.mirrorVertically && !themeData.invertPixels + && !translucentToplevel) { + // Draw on backing store DC only for real widgets or backing store images. + const QPaintDevice *enginePaintDevice = painter->paintEngine()->paintDevice(); + switch (enginePaintDevice->devType()) { + case QInternal::Widget: + canDrawDirectly = true; + break; + case QInternal::Image: + // Ensure the backing store has received as resize and is initialized. + if (QBackingStore *bs = backingStoreForWidget(themeData.widget)) { + if (bs->size().isValid() && bs->paintDevice() == enginePaintDevice) + canDrawDirectly = true; + } + break; + } + } + + const HDC dc = canDrawDirectly ? hdcForWidgetBackingStore(themeData.widget) : nullptr; + const bool result = dc && qFuzzyCompare(correctionFactor, qreal(1)) + ? drawBackgroundDirectly(dc, themeData, aditionalDevicePixelRatio) + : drawBackgroundThruNativeBuffer(themeData, aditionalDevicePixelRatio, correctionFactor); + painter->restore(); + return result; +} + +/*! \internal + This function draws the theme parts directly to the paintengines HDC. + Do not use this if you need to perform other transformations on the + resulting data. +*/ +bool QWindowsVistaStylePrivate::drawBackgroundDirectly(HDC dc, QWindowsThemeData &themeData, qreal additionalDevicePixelRatio) +{ + QPainter *painter = themeData.painter; + + const auto &deviceTransform = painter->deviceTransform(); + const QPointF redirectionDelta(deviceTransform.dx(), deviceTransform.dy()); + const QRect area = scaleRect(QRectF(themeData.rect), additionalDevicePixelRatio).translated(redirectionDelta).toRect(); + + QRegion sysRgn = painter->paintEngine()->systemClip(); + if (sysRgn.isEmpty()) + sysRgn = area; + else + sysRgn &= area; + if (painter->hasClipping()) + sysRgn &= scaleRegion(painter->clipRegion(), additionalDevicePixelRatio).translated(redirectionDelta.toPoint()); + HRGN hrgn = qt_hrgn_from_qregion(sysRgn); + SelectClipRgn(dc, hrgn); + +#ifdef DEBUG_XP_STYLE + printf("---[ DIRECT PAINTING ]------------------> Name(%-10s) Part(%d) State(%d)\n", + qPrintable(themeData.name), themeData.partId, themeData.stateId); + showProperties(themeData); +#endif + + RECT drawRECT = themeData.toRECT(area); + DTBGOPTS drawOptions; + memset(&drawOptions, 0, sizeof(drawOptions)); + drawOptions.dwSize = sizeof(drawOptions); + drawOptions.rcClip = themeData.toRECT(sysRgn.boundingRect()); + drawOptions.dwFlags = DTBG_CLIPRECT + | (themeData.noBorder ? DTBG_OMITBORDER : 0) + | (themeData.noContent ? DTBG_OMITCONTENT : 0) + | (themeData.mirrorHorizontally ? DTBG_MIRRORDC : 0); + + const HRESULT result = DrawThemeBackgroundEx(themeData.handle(), dc, themeData.partId, themeData.stateId, &(drawRECT), &drawOptions); + SelectClipRgn(dc, nullptr); + DeleteObject(hrgn); + return SUCCEEDED(result); +} + +/*! \internal + This function uses a secondary Native doublebuffer for painting parts. + It should only be used when the painteengine doesn't provide a proper + HDC for direct painting (e.g. when doing a grabWidget(), painting to + other pixmaps etc), or when special transformations are needed (e.g. + flips (horizontal mirroring only, vertical are handled by the theme + engine). + + \a correctionFactor is an additional factor used to scale up controls + that are too small on High DPI screens, as has been observed for + WP_MDICLOSEBUTTON, WP_MDIRESTOREBUTTON, WP_MDIMINBUTTON (QTBUG-75927). +*/ +bool QWindowsVistaStylePrivate::drawBackgroundThruNativeBuffer(QWindowsThemeData &themeData, + qreal additionalDevicePixelRatio, + qreal correctionFactor) +{ + QPainter *painter = themeData.painter; + QRectF rectF = scaleRect(QRectF(themeData.rect), additionalDevicePixelRatio); + + if ((themeData.rotate + 90) % 180 == 0) { // Catch 90,270,etc.. degree flips. + rectF = QRectF(0, 0, rectF.height(), rectF.width()); + } + rectF.moveTo(0, 0); + + const bool hasCorrectionFactor = !qFuzzyCompare(correctionFactor, qreal(1)); + QRect rect = rectF.toRect(); + const QRect drawRect = hasCorrectionFactor + ? QRectF(rectF.topLeft() / correctionFactor, rectF.size() / correctionFactor).toRect() + : rect; + int partId = themeData.partId; + int stateId = themeData.stateId; + int w = rect.width(); + int h = rect.height(); + + // Values initialized later, either from cached values, or from function calls + AlphaChannelType alphaType = UnknownAlpha; + bool stateHasData = true; // We assume so; + bool hasAlpha = false; + bool partIsTransparent; + bool potentialInvalidAlpha; + + QString pixmapCacheKey = QStringLiteral("$qt_xp_"); + pixmapCacheKey.append(themeName(themeData.theme)); + pixmapCacheKey.append(QLatin1Char('p')); + pixmapCacheKey.append(QString::number(partId)); + pixmapCacheKey.append(QLatin1Char('s')); + pixmapCacheKey.append(QString::number(stateId)); + pixmapCacheKey.append(QLatin1Char('s')); + pixmapCacheKey.append(themeData.noBorder ? QLatin1Char('0') : QLatin1Char('1')); + pixmapCacheKey.append(QLatin1Char('b')); + pixmapCacheKey.append(themeData.noContent ? QLatin1Char('0') : QLatin1Char('1')); + pixmapCacheKey.append(QString::number(w)); + pixmapCacheKey.append(QLatin1Char('w')); + pixmapCacheKey.append(QString::number(h)); + pixmapCacheKey.append(QLatin1Char('h')); + pixmapCacheKey.append(QString::number(additionalDevicePixelRatio)); + pixmapCacheKey.append(QLatin1Char('d')); + if (hasCorrectionFactor) { + pixmapCacheKey.append(QLatin1Char('c')); + pixmapCacheKey.append(QString::number(correctionFactor)); + } + + QPixmap cachedPixmap; + ThemeMapKey key(themeData); + ThemeMapData data = alphaCache.value(key); + + bool haveCachedPixmap = false; + bool isCached = data.dataValid; + if (isCached) { + partIsTransparent = data.partIsTransparent; + hasAlpha = data.hasAlphaChannel; + alphaType = data.alphaType; + potentialInvalidAlpha = data.hadInvalidAlpha; + + haveCachedPixmap = QPixmapCache::find(pixmapCacheKey, &cachedPixmap); + +#ifdef DEBUG_XP_STYLE + char buf[25]; + ::sprintf(buf, "+ Pixmap(%3d, %3d) ]", w, h); + printf("---[ CACHED %s--------> Name(%-10s) Part(%d) State(%d)\n", + haveCachedPixmap ? buf : "]-------------------", + qPrintable(themeData.name), themeData.partId, themeData.stateId); +#endif + } else { + // Not cached, so get values from Theme Engine + BOOL tmt_borderonly = false; + COLORREF tmt_transparentcolor = 0x0; + PROPERTYORIGIN proporigin = PO_NOTFOUND; + GetThemeBool(themeData.handle(), themeData.partId, themeData.stateId, TMT_BORDERONLY, &tmt_borderonly); + GetThemeColor(themeData.handle(), themeData.partId, themeData.stateId, TMT_TRANSPARENTCOLOR, &tmt_transparentcolor); + GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, TMT_CAPTIONMARGINS, &proporigin); + + partIsTransparent = isTransparent(themeData); + + potentialInvalidAlpha = false; + GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, TMT_GLYPHTYPE, &proporigin); + if (proporigin == PO_PART || proporigin == PO_STATE) { + int tmt_glyphtype = GT_NONE; + GetThemeEnumValue(themeData.handle(), themeData.partId, themeData.stateId, TMT_GLYPHTYPE, &tmt_glyphtype); + potentialInvalidAlpha = partIsTransparent && tmt_glyphtype == GT_IMAGEGLYPH; + } + +#ifdef DEBUG_XP_STYLE + printf("---[ NOT CACHED ]-----------------------> Name(%-10s) Part(%d) State(%d)\n", + qPrintable(themeData.name), themeData.partId, themeData.stateId); + printf("-->partIsTransparen = %d\n", partIsTransparent); + printf("-->potentialInvalidAlpha = %d\n", potentialInvalidAlpha); + showProperties(themeData); +#endif + } + bool wasAlphaSwapped = false; + bool wasAlphaFixed = false; + + // OLD PSDK Workaround ------------------------------------------------------------------------ + // See if we need extra clipping for the older PSDK, which does + // not have a DrawThemeBackgroundEx function for DTGB_OMITBORDER + // and DTGB_OMITCONTENT + bool addBorderContentClipping = false; + QRegion extraClip; + QRect area = drawRect; + if (themeData.noBorder || themeData.noContent) { + extraClip = area; + // We are running on a system where the uxtheme.dll does not have + // the DrawThemeBackgroundEx function, so we need to clip away + // borders or contents manually. + + int borderSize = 0; + PROPERTYORIGIN origin = PO_NOTFOUND; + GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, TMT_BORDERSIZE, &origin); + GetThemeInt(themeData.handle(), themeData.partId, themeData.stateId, TMT_BORDERSIZE, &borderSize); + borderSize *= additionalDevicePixelRatio; + + // Clip away border region + if ((origin == PO_CLASS || origin == PO_PART || origin == PO_STATE) && borderSize > 0) { + if (themeData.noBorder) { + extraClip &= area; + area = area.adjusted(-borderSize, -borderSize, borderSize, borderSize); + } + + // Clip away content region + if (themeData.noContent) { + QRegion content = area.adjusted(borderSize, borderSize, -borderSize, -borderSize); + extraClip ^= content; + } + } + addBorderContentClipping = (themeData.noBorder | themeData.noContent); + } + + QImage img; + if (!haveCachedPixmap) { // If the pixmap is not cached, generate it! ------------------------- + if (!buffer(drawRect.width(), drawRect.height())) // Ensure a buffer of at least (w, h) in size + return false; + HDC dc = bufferHDC(); + + // Clear the buffer + if (alphaType != NoAlpha) { + // Consider have separate "memset" function for small chunks for more speedup + memset(bufferPixels, 0x00, bufferW * drawRect.height() * 4); + } + + // Difference between area and rect + int dx = area.x() - drawRect.x(); + int dy = area.y() - drawRect.y(); + + // Adjust so painting rect starts from Origo + rect.moveTo(0,0); + area.moveTo(dx,dy); + DTBGOPTS drawOptions; + drawOptions.dwSize = sizeof(drawOptions); + drawOptions.rcClip = themeData.toRECT(rect); + drawOptions.dwFlags = DTBG_CLIPRECT + | (themeData.noBorder ? DTBG_OMITBORDER : 0) + | (themeData.noContent ? DTBG_OMITCONTENT : 0); + + // Drawing the part into the backing store + RECT wRect(themeData.toRECT(area)); + DrawThemeBackgroundEx(themeData.handle(), dc, themeData.partId, themeData.stateId, &wRect, &drawOptions); + + // If not cached, analyze the buffer data to figure + // out alpha type, and if it contains data + if (!isCached) { + // SHORTCUT: If the part's state has no data, cache it for NOOP later + if (!stateHasData) { + memset(static_cast<void *>(&data), 0, sizeof(data)); + data.dataValid = true; + alphaCache.insert(key, data); + return true; + } + hasAlpha = hasAlphaChannel(rect); + if (!hasAlpha && partIsTransparent) + potentialInvalidAlpha = true; +#if defined(DEBUG_XP_STYLE) && 1 + dumpNativeDIB(drawRect.width(), drawRect.height()); +#endif + } + + // Fix alpha values, if needed + if (potentialInvalidAlpha) + wasAlphaFixed = fixAlphaChannel(drawRect); + + QImage::Format format; + if ((partIsTransparent && !wasAlphaSwapped) || (!partIsTransparent && hasAlpha)) { + format = QImage::Format_ARGB32_Premultiplied; + alphaType = RealAlpha; + } else if (wasAlphaSwapped) { + format = QImage::Format_ARGB32_Premultiplied; + alphaType = MaskAlpha; + } else { + format = QImage::Format_RGB32; + // The image data we got from the theme engine does not have any transparency, + // thus the alpha channel is set to 0. + // However, Format_RGB32 requires the alpha part to be set to 0xff, thus + // we must flip it from 0x00 to 0xff + swapAlphaChannel(rect, true); + alphaType = NoAlpha; + } +#if defined(DEBUG_XP_STYLE) && 1 + printf("Image format is: %s\n", alphaType == RealAlpha ? "Real Alpha" : alphaType == MaskAlpha ? "Masked Alpha" : "No Alpha"); +#endif + img = QImage(bufferPixels, bufferW, bufferH, format); + if (themeData.invertPixels) + img.invertPixels(); + + if (hasCorrectionFactor) + img = img.scaled(img.size() * correctionFactor, Qt::KeepAspectRatio, Qt::SmoothTransformation); + img.setDevicePixelRatio(additionalDevicePixelRatio); + } + + // Blitting backing store + bool useRegion = partIsTransparent && !hasAlpha && !wasAlphaSwapped; + + QRegion newRegion; + QRegion oldRegion; + if (useRegion) { + newRegion = region(themeData); + oldRegion = painter->clipRegion(); + painter->setClipRegion(newRegion); +#if defined(DEBUG_XP_STYLE) && 0 + printf("Using region:\n"); + for (const QRect &r : newRegion) + printf(" (%d, %d, %d, %d)\n", r.x(), r.y(), r.right(), r.bottom()); +#endif + } + + if (addBorderContentClipping) + painter->setClipRegion(scaleRegion(extraClip, 1.0 / additionalDevicePixelRatio), Qt::IntersectClip); + + if (!themeData.mirrorHorizontally && !themeData.mirrorVertically && !themeData.rotate) { + if (!haveCachedPixmap) + painter->drawImage(themeData.rect, img, rect); + else + painter->drawPixmap(themeData.rect, cachedPixmap); + } else { + // This is _slow_! + // Make a copy containing only the necessary data, and mirror + // on all wanted axes. Then draw the copy. + // If cached, the normal pixmap is cached, instead of caching + // all possible orientations for each part and state. + QImage imgCopy; + if (!haveCachedPixmap) + imgCopy = img.copy(rect); + else + imgCopy = cachedPixmap.toImage(); + + if (themeData.rotate) { + QTransform rotMatrix; + rotMatrix.rotate(themeData.rotate); + imgCopy = imgCopy.transformed(rotMatrix); + } + if (themeData.mirrorHorizontally || themeData.mirrorVertically) + imgCopy = imgCopy.mirrored(themeData.mirrorHorizontally, themeData.mirrorVertically); + painter->drawImage(themeData.rect, imgCopy); + } + + if (useRegion || addBorderContentClipping) { + if (oldRegion.isEmpty()) + painter->setClipping(false); + else + painter->setClipRegion(oldRegion); + } + + // Cache the pixmap to avoid expensive swapAlphaChannel() calls + if (!haveCachedPixmap && w && h) { + QPixmap pix = QPixmap::fromImage(img).copy(rect); + QPixmapCache::insert(pixmapCacheKey, pix); +#ifdef DEBUG_XP_STYLE + printf("+++Adding pixmap to cache, size(%d, %d), wasAlphaSwapped(%d), wasAlphaFixed(%d), name(%s)\n", + w, h, wasAlphaSwapped, wasAlphaFixed, qPrintable(pixmapCacheKey)); +#endif + } + + // Add to theme part cache + if (!isCached) { + memset(static_cast<void *>(&data), 0, sizeof(data)); + data.dataValid = true; + data.partIsTransparent = partIsTransparent; + data.alphaType = alphaType; + data.hasAlphaChannel = hasAlpha; + data.wasAlphaSwapped = wasAlphaSwapped; + data.hadInvalidAlpha = wasAlphaFixed; + alphaCache.insert(key, data); + } + return true; +} + +/*! + \internal + + Animations are started at a frame that is based on the current time, + which makes it impossible to run baseline tests with this style. Allow + overriding through a dynamic property. +*/ +QTime QWindowsVistaStylePrivate::animationTime() const +{ + Q_Q(const QWindowsVistaStyle); + static bool animationTimeOverride = q->dynamicPropertyNames().contains("_qt_animation_time"); + if (animationTimeOverride) + return q->property("_qt_animation_time").toTime(); + return QTime::currentTime(); +} + +/* \internal + Checks and returns the style object +*/ +inline QObject *styleObject(const QStyleOption *option) { + return option ? option->styleObject : nullptr; +} + +/* \internal + Checks if we can animate on a style option +*/ +bool canAnimate(const QStyleOption *option) { + return option + && option->styleObject + && !option->styleObject->property("_q_no_animation").toBool(); +} + +static inline QImage createAnimationBuffer(const QStyleOption *option, const QWidget *widget) +{ + const qreal devicePixelRatio = widget + ? widget->devicePixelRatioF() : qApp->devicePixelRatio(); + QImage result(option->rect.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(devicePixelRatio); + result.fill(0); + return result; +} + +/* \internal + Used by animations to clone a styleoption and shift its offset +*/ +QStyleOption *clonedAnimationStyleOption(const QStyleOption*option) { + QStyleOption *styleOption = nullptr; + if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider*>(option)) + styleOption = new QStyleOptionSlider(*slider); + else if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox*>(option)) + styleOption = new QStyleOptionSpinBox(*spinbox); + else if (const QStyleOptionGroupBox *groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option)) + styleOption = new QStyleOptionGroupBox(*groupBox); + else if (const QStyleOptionComboBox *combo = qstyleoption_cast<const QStyleOptionComboBox*>(option)) + styleOption = new QStyleOptionComboBox(*combo); + else if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton*>(option)) + styleOption = new QStyleOptionButton(*button); + else + styleOption = new QStyleOption(*option); + styleOption->rect = QRect(QPoint(0,0), option->rect.size()); + return styleOption; +} + +/* \internal + Used by animations to delete cloned styleoption +*/ +void deleteClonedAnimationStyleOption(const QStyleOption *option) +{ + if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider*>(option)) + delete slider; + else if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox*>(option)) + delete spinbox; + else if (const QStyleOptionGroupBox *groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option)) + delete groupBox; + else if (const QStyleOptionComboBox *combo = qstyleoption_cast<const QStyleOptionComboBox*>(option)) + delete combo; + else if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton*>(option)) + delete button; + else + delete option; +} + +static void populateTitleBarButtonTheme(const QStyle *proxy, const QWidget *widget, + const QStyleOptionComplex *option, + QStyle::SubControl subControl, + bool isTitleBarActive, int part, + QWindowsThemeData *theme) +{ + theme->rect = proxy->subControlRect(QStyle::CC_TitleBar, option, subControl, widget); + theme->partId = part; + if (widget && !widget->isEnabled()) + theme->stateId = RBS_DISABLED; + else if (option->activeSubControls == subControl && option->state.testFlag(QStyle::State_Sunken)) + theme->stateId = RBS_PUSHED; + else if (option->activeSubControls == subControl && option->state.testFlag(QStyle::State_MouseOver)) + theme->stateId = RBS_HOT; + else if (!isTitleBarActive) + theme->stateId = RBS_INACTIVE; + else + theme->stateId = RBS_NORMAL; +} + +#if QT_CONFIG(mdiarea) +// Helper for drawing MDI buttons into the corner widget of QMenuBar in case a +// QMdiSubWindow is maximized. +static void populateMdiButtonTheme(const QStyle *proxy, const QWidget *widget, + const QStyleOptionComplex *option, + QStyle::SubControl subControl, int part, + QWindowsThemeData *theme) +{ + theme->partId = part; + theme->rect = proxy->subControlRect(QStyle::CC_MdiControls, option, subControl, widget); + if (!option->state.testFlag(QStyle::State_Enabled)) + theme->stateId = CBS_INACTIVE; + else if (option->state.testFlag(QStyle::State_Sunken) && option->activeSubControls.testFlag(subControl)) + theme->stateId = CBS_PUSHED; + else if (option->state.testFlag(QStyle::State_MouseOver) && option->activeSubControls.testFlag(subControl)) + theme->stateId = CBS_HOT; + else + theme->stateId = CBS_NORMAL; +} + +// Calculate an small (max 2), empirical correction factor for scaling up +// WP_MDICLOSEBUTTON, WP_MDIRESTOREBUTTON, WP_MDIMINBUTTON, which are too +// small on High DPI screens (QTBUG-75927). +static qreal mdiButtonCorrectionFactor(QWindowsThemeData &theme, const QPaintDevice *pd = nullptr) +{ + const auto dpr = pd ? pd->devicePixelRatio() : qApp->devicePixelRatio(); + const QSizeF nativeSize = QSizeF(theme.size()) / dpr; + const QSizeF requestedSize(theme.rect.size()); + const auto rawFactor = qMin(requestedSize.width() / nativeSize.width(), + requestedSize.height() / nativeSize.height()); + const auto factor = rawFactor >= qreal(2) ? qreal(2) : qreal(1); + return factor; +} +#endif // QT_CONFIG(mdiarea) + +/* + This function is used by subControlRect to check if a button + should be drawn for the given subControl given a set of window flags. +*/ +static bool buttonVisible(const QStyle::SubControl sc, const QStyleOptionTitleBar *tb){ + + bool isMinimized = tb->titleBarState & Qt::WindowMinimized; + bool isMaximized = tb->titleBarState & Qt::WindowMaximized; + const auto flags = tb->titleBarFlags; + bool retVal = false; + switch (sc) { + case QStyle::SC_TitleBarContextHelpButton: + if (flags & Qt::WindowContextHelpButtonHint) + retVal = true; + break; + case QStyle::SC_TitleBarMinButton: + if (!isMinimized && (flags & Qt::WindowMinimizeButtonHint)) + retVal = true; + break; + case QStyle::SC_TitleBarNormalButton: + if (isMinimized && (flags & Qt::WindowMinimizeButtonHint)) + retVal = true; + else if (isMaximized && (flags & Qt::WindowMaximizeButtonHint)) + retVal = true; + break; + case QStyle::SC_TitleBarMaxButton: + if (!isMaximized && (flags & Qt::WindowMaximizeButtonHint)) + retVal = true; + break; + case QStyle::SC_TitleBarShadeButton: + if (!isMinimized && flags & Qt::WindowShadeButtonHint) + retVal = true; + break; + case QStyle::SC_TitleBarUnshadeButton: + if (isMinimized && flags & Qt::WindowShadeButtonHint) + retVal = true; + break; + case QStyle::SC_TitleBarCloseButton: + if (flags & Qt::WindowSystemMenuHint) + retVal = true; + break; + case QStyle::SC_TitleBarSysMenu: + if (flags & Qt::WindowSystemMenuHint) + retVal = true; + break; + default : + retVal = true; + } + return retVal; +} + +//convert Qt state flags to uxtheme button states +static int buttonStateId(int flags, int partId) +{ + int stateId = 0; + if (partId == BP_RADIOBUTTON || partId == BP_CHECKBOX) { + if (!(flags & QStyle::State_Enabled)) + stateId = RBS_UNCHECKEDDISABLED; + else if (flags & QStyle::State_Sunken) + stateId = RBS_UNCHECKEDPRESSED; + else if (flags & QStyle::State_MouseOver) + stateId = RBS_UNCHECKEDHOT; + else + stateId = RBS_UNCHECKEDNORMAL; + + if (flags & QStyle::State_On) + stateId += RBS_CHECKEDNORMAL-1; + + } else if (partId == BP_PUSHBUTTON) { + if (!(flags & QStyle::State_Enabled)) + stateId = PBS_DISABLED; + else if (flags & (QStyle::State_Sunken | QStyle::State_On)) + stateId = PBS_PRESSED; + else if (flags & QStyle::State_MouseOver) + stateId = PBS_HOT; + else + stateId = PBS_NORMAL; + } else { + Q_ASSERT(1); + } + return stateId; +} + +static inline bool supportsStateTransition(QStyle::PrimitiveElement element, + const QStyleOption *option, + const QWidget *widget) +{ + bool result = false; + switch (element) { + case QStyle::PE_IndicatorRadioButton: + case QStyle::PE_IndicatorCheckBox: + result = true; + break; + // QTBUG-40634, do not animate when color is set in palette for PE_PanelLineEdit. + case QStyle::PE_FrameLineEdit: + result = !QWindowsVistaStylePrivate::isLineEditBaseColorSet(option, widget); + break; + default: + break; + } + return result; +} + +/*! + \class QWindowsVistaStyle + \brief The QWindowsVistaStyle class provides a look and feel suitable for applications on Microsoft Windows Vista. + \since 4.3 + \ingroup appearance + \inmodule QtWidgets + \internal + + \warning This style is only available on the Windows Vista platform + because it makes use of Windows Vista's style engine. + + \sa QMacStyle, QFusionStyle +*/ + +/*! + Constructs a QWindowsVistaStyle object. +*/ +QWindowsVistaStyle::QWindowsVistaStyle() : QWindowsStyle(*new QWindowsVistaStylePrivate) +{ +} + +/*! + \internal + Constructs a QWindowsStyle object. +*/ +QWindowsVistaStyle::QWindowsVistaStyle(QWindowsVistaStylePrivate &dd) : QWindowsStyle(dd) +{ +} + +/*! + Destructor. +*/ +QWindowsVistaStyle::~QWindowsVistaStyle() = default; + + +/*! + \internal + + Animations are used for some state transitions on specific widgets. + + Only one running animation can exist for a widget at any specific + time. Animations can be added through + QWindowsVistaStylePrivate::startAnimation(Animation *) and any + existing animation on a widget can be retrieved with + QWindowsVistaStylePrivate::widgetAnimation(Widget *). + + Once an animation has been started, + QWindowsVistaStylePrivate::timerEvent(QTimerEvent *) will + continuously call update() on the widget until it is stopped, + meaning that drawPrimitive will be called many times until the + transition has completed. During this time, the result will be + retrieved by the Animation::paint(...) function and not by the style + itself. + + To determine if a transition should occur, the style needs to know + the previous state of the widget as well as the current one. This is + solved by updating dynamic properties on the widget every time the + function is called. + + Transitions interrupting existing transitions should always be + smooth, so whenever a hover-transition is started on a pulsating + button, it uses the current frame of the pulse-animation as the + starting image for the hover transition. + + */ +void QWindowsVistaStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option, + QPainter *painter, const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) { + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + + QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); + + int state = option->state; + QRect rect = option->rect; + + if ((state & State_Enabled) && d->transitionsEnabled() && canAnimate(option)) { + if (supportsStateTransition(element, option, widget)) { + // Retrieve and update the dynamic properties tracking + // the previous state of the widget: + QObject *styleObject = option->styleObject; + styleObject->setProperty("_q_no_animation", true); + int oldState = styleObject->property("_q_stylestate").toInt(); + QRect oldRect = styleObject->property("_q_stylerect").toRect(); + QRect newRect = rect; + styleObject->setProperty("_q_stylestate", int(option->state)); + styleObject->setProperty("_q_stylerect", option->rect); + + bool doTransition = oldState && + ((state & State_Sunken) != (oldState & State_Sunken) || + (state & State_On) != (oldState & State_On) || + (state & State_MouseOver) != (oldState & State_MouseOver)); + + if (oldRect != newRect || + (state & State_Enabled) != (oldState & State_Enabled) || + (state & State_Active) != (oldState & State_Active)) + d->stopAnimation(styleObject); + + if (state & State_ReadOnly && element == PE_FrameLineEdit) // Do not animate read only line edits + doTransition = false; + + if (doTransition) { + QStyleOption *styleOption = clonedAnimationStyleOption(option); + styleOption->state = QStyle::State(oldState); + + QWindowsVistaAnimation *animate = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); + QWindowsVistaTransition *transition = new QWindowsVistaTransition(styleObject); + + // We create separate images for the initial and final transition states and store them in the + // Transition object. + QImage startImage = createAnimationBuffer(option, widget); + QPainter startPainter(&startImage); + + QImage endImage = createAnimationBuffer(option, widget); + QPainter endPainter(&endImage); + + // If we have a running animation on the widget already, we will use that to paint the initial + // state of the new transition, this ensures a smooth transition from a current animation such as a + // pulsating default button into the intended target state. + if (!animate) + proxy()->drawPrimitive(element, styleOption, &startPainter, widget); + else + animate->paint(&startPainter, styleOption); + + transition->setStartImage(startImage); + + // The end state of the transition is simply the result we would have painted + // if the style was not animated. + styleOption->styleObject = nullptr; + styleOption->state = option->state; + proxy()->drawPrimitive(element, styleOption, &endPainter, widget); + + transition->setEndImage(endImage); + + HTHEME theme; + int partId; + DWORD duration; + int fromState = 0; + int toState = 0; + + //translate state flags to UXTHEME states : + if (element == PE_FrameLineEdit) { + theme = OpenThemeData(nullptr, L"Edit"); + partId = EP_EDITBORDER_NOSCROLL; + + if (oldState & State_HasFocus) + fromState = ETS_SELECTED; + else if (oldState & State_MouseOver) + fromState = ETS_HOT; + else + fromState = ETS_NORMAL; + + if (state & State_HasFocus) + toState = ETS_SELECTED; + else if (state & State_MouseOver) + toState = ETS_HOT; + else + toState = ETS_NORMAL; + + } else { + theme = OpenThemeData(nullptr, L"Button"); + if (element == PE_IndicatorRadioButton) + partId = BP_RADIOBUTTON; + else if (element == PE_IndicatorCheckBox) + partId = BP_CHECKBOX; + else + partId = BP_PUSHBUTTON; + + fromState = buttonStateId(oldState, partId); + toState = buttonStateId(option->state, partId); + } + + // Retrieve the transition time between the states from the system. + if (theme + && SUCCEEDED(GetThemeTransitionDuration(theme, partId, fromState, toState, + TMT_TRANSITIONDURATIONS, &duration))) { + transition->setDuration(int(duration)); + } + transition->setStartTime(d->animationTime()); + + deleteClonedAnimationStyleOption(styleOption); + d->startAnimation(transition); + } + styleObject->setProperty("_q_no_animation", false); + } + } + + int themeNumber = -1; + int partId = 0; + int stateId = 0; + bool hMirrored = false; + bool vMirrored = false; + bool noBorder = false; + bool noContent = false; + int rotate = 0; + + switch (element) { + case PE_PanelButtonCommand: + if (const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { + QBrush fill; + if (!(state & State_Sunken) && (state & State_On)) + fill = QBrush(option->palette.light().color(), Qt::Dense4Pattern); + else + fill = option->palette.brush(QPalette::Button); + if (btn->features & QStyleOptionButton::DefaultButton && state & State_Sunken) { + painter->setPen(option->palette.dark().color()); + painter->setBrush(fill); + painter->drawRect(rect.adjusted(0, 0, -1, -1)); + } else if (state & (State_Raised | State_On | State_Sunken)) { + qDrawWinButton(painter, rect, option->palette, state & (State_Sunken | State_On), + &fill); + } else { + painter->fillRect(rect, fill); + } + } + break; + + case PE_PanelButtonTool: +#if QT_CONFIG(dockwidget) + if (widget && widget->inherits("QDockWidgetTitleButton")) { + if (const QWidget *dw = widget->parentWidget()) + if (dw->isWindow()) { + return; + } + } +#endif // QT_CONFIG(dockwidget) + themeNumber = QWindowsVistaStylePrivate::ToolBarTheme; + partId = TP_BUTTON; + if (!(option->state & State_Enabled)) + stateId = TS_DISABLED; + else if (option->state & State_Sunken) + stateId = TS_PRESSED; + else if (option->state & State_MouseOver) + stateId = option->state & State_On ? TS_HOTCHECKED : TS_HOT; + else if (option->state & State_On) + stateId = TS_CHECKED; + else if (!(option->state & State_AutoRaise)) + stateId = TS_HOT; + else + stateId = TS_NORMAL; + + break; + + case PE_IndicatorHeaderArrow: + if (const auto *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { + int stateId = HSAS_SORTEDDOWN; + if (header->sortIndicator & QStyleOptionHeader::SortDown) + stateId = HSAS_SORTEDUP; //note that the uxtheme sort down indicator is the inverse of ours + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::HeaderTheme, + HP_HEADERSORTARROW, stateId, option->rect); + d->drawBackground(theme); + return; + } + break; + + case PE_IndicatorCheckBox: + if (auto *animate = + qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option)))) { + animate->paint(painter, option); + return; + } else { + themeNumber = QWindowsVistaStylePrivate::ButtonTheme; + partId = BP_CHECKBOX; + + if (!(option->state & State_Enabled)) + stateId = CBS_UNCHECKEDDISABLED; + else if (option->state & State_Sunken) + stateId = CBS_UNCHECKEDPRESSED; + else if (option->state & State_MouseOver) + stateId = CBS_UNCHECKEDHOT; + else + stateId = CBS_UNCHECKEDNORMAL; + + if (option->state & State_On) + stateId += CBS_CHECKEDNORMAL-1; + else if (option->state & State_NoChange) + stateId += CBS_MIXEDNORMAL-1; + } + break; + + case PE_IndicatorItemViewItemCheck: { + QStyleOptionButton button; + button.QStyleOption::operator=(*option); + button.state &= ~State_MouseOver; + proxy()->drawPrimitive(PE_IndicatorCheckBox, &button, painter, widget); + return; + } + + case PE_IndicatorBranch: { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::VistaTreeViewTheme); + static int decoration_size = 0; + if (!decoration_size && theme.isValid()) { + QWindowsThemeData themeSize = theme; + themeSize.partId = TVP_HOTGLYPH; + themeSize.stateId = GLPS_OPENED; + const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + decoration_size = qRound(qMax(size.width(), size.height())); + } + int mid_h = option->rect.x() + option->rect.width() / 2; + int mid_v = option->rect.y() + option->rect.height() / 2; + if (option->state & State_Children) { + int delta = decoration_size / 2; + theme.rect = QRect(mid_h - delta, mid_v - delta, decoration_size, decoration_size); + theme.partId = option->state & State_MouseOver ? TVP_HOTGLYPH : TVP_GLYPH; + theme.stateId = option->state & QStyle::State_Open ? GLPS_OPENED : GLPS_CLOSED; + if (option->direction == Qt::RightToLeft) + theme.mirrorHorizontally = true; + d->drawBackground(theme); + } + return; + } + + case PE_PanelButtonBevel: + if (QWindowsVistaAnimation *animate = + qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option)))) { + animate->paint(painter, option); + return; + } + + themeNumber = QWindowsVistaStylePrivate::ButtonTheme; + partId = BP_PUSHBUTTON; + if (!(option->state & State_Enabled)) + stateId = PBS_DISABLED; + else if ((option->state & State_Sunken) || (option->state & State_On)) + stateId = PBS_PRESSED; + else if (option->state & State_MouseOver) + stateId = PBS_HOT; + else + stateId = PBS_NORMAL; + break; + + case PE_IndicatorRadioButton: + if (QWindowsVistaAnimation *animate = + qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option)))) { + animate->paint(painter, option); + return; + } else { + themeNumber = QWindowsVistaStylePrivate::ButtonTheme; + partId = BP_RADIOBUTTON; + + if (!(option->state & State_Enabled)) + stateId = RBS_UNCHECKEDDISABLED; + else if (option->state & State_Sunken) + stateId = RBS_UNCHECKEDPRESSED; + else if (option->state & State_MouseOver) + stateId = RBS_UNCHECKEDHOT; + else + stateId = RBS_UNCHECKEDNORMAL; + + if (option->state & State_On) + stateId += RBS_CHECKEDNORMAL-1; + } + break; + + case PE_Frame: +#if QT_CONFIG(accessibility) + if (QStyleHelper::isInstanceOf(option->styleObject, QAccessible::EditableText) + || QStyleHelper::isInstanceOf(option->styleObject, QAccessible::StaticText) || +#else + if ( +#endif + (widget && widget->inherits("QTextEdit"))) { + painter->save(); + int stateId = ETS_NORMAL; + if (!(state & State_Enabled)) + stateId = ETS_DISABLED; + else if (state & State_ReadOnly) + stateId = ETS_READONLY; + else if (state & State_HasFocus) + stateId = ETS_SELECTED; + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::EditTheme, + EP_EDITBORDER_HVSCROLL, stateId, option->rect); + // Since EP_EDITBORDER_HVSCROLL does not us borderfill, theme.noContent cannot be used for clipping + int borderSize = 1; + GetThemeInt(theme.handle(), theme.partId, theme.stateId, TMT_BORDERSIZE, &borderSize); + QRegion clipRegion = option->rect; + QRegion content = option->rect.adjusted(borderSize, borderSize, -borderSize, -borderSize); + clipRegion ^= content; + painter->setClipRegion(clipRegion); + d->drawBackground(theme); + painter->restore(); + return; + } else { + if (option->state & State_Raised) + return; + + themeNumber = QWindowsVistaStylePrivate::ListViewTheme; + partId = LVP_LISTGROUP; + QWindowsThemeData theme(widget, nullptr, themeNumber, partId); + + if (!(option->state & State_Enabled)) + stateId = ETS_DISABLED; + else + stateId = ETS_NORMAL; + + int fillType; + + if (GetThemeEnumValue(theme.handle(), partId, stateId, TMT_BGTYPE, &fillType) == S_OK) { + if (fillType == BT_BORDERFILL) { + COLORREF bcRef; + GetThemeColor(theme.handle(), partId, stateId, TMT_BORDERCOLOR, &bcRef); + QColor bordercolor(qRgb(GetRValue(bcRef), GetGValue(bcRef), GetBValue(bcRef))); + QPen oldPen = painter->pen(); + + // Inner white border + painter->setPen(QPen(option->palette.base().color(), 0)); + const qreal dpi = QStyleHelper::dpi(option); + const auto topLevelAdjustment = QStyleHelper::dpiScaled(0.5, dpi); + const auto bottomRightAdjustment = QStyleHelper::dpiScaled(-1, dpi); + painter->drawRect(QRectF(option->rect).adjusted(topLevelAdjustment, topLevelAdjustment, + bottomRightAdjustment, bottomRightAdjustment)); + // Outer dark border + painter->setPen(QPen(bordercolor, 0)); + painter->drawRect(QRectF(option->rect).adjusted(0, 0, -topLevelAdjustment, -topLevelAdjustment)); + painter->setPen(oldPen); + } + + if (fillType == BT_BORDERFILL || fillType == BT_NONE) + return; + } + } + break; + + case PE_FrameMenu: { + int stateId = option->state & State_Active ? MB_ACTIVE : MB_INACTIVE; + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPBORDERS, stateId, option->rect); + d->drawBackground(theme); + return; + } + + case PE_PanelMenuBar: + break; + +#if QT_CONFIG(dockwidget) + case PE_IndicatorDockWidgetResizeHandle: + return; + + case PE_FrameDockWidget: + if (const auto *frm = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + themeNumber = QWindowsVistaStylePrivate::WindowTheme; + if (option->state & State_Active) + stateId = FS_ACTIVE; + else + stateId = FS_INACTIVE; + + int fwidth = proxy()->pixelMetric(PM_DockWidgetFrameWidth, frm, widget); + + QWindowsThemeData theme(widget, painter, themeNumber, 0, stateId); + + if (!theme.isValid()) + break; + + theme.rect = QRect(frm->rect.x(), frm->rect.y(), frm->rect.x()+fwidth, frm->rect.height()-fwidth); + theme.partId = WP_SMALLFRAMELEFT; + d->drawBackground(theme); + theme.rect = QRect(frm->rect.width()-fwidth, frm->rect.y(), fwidth, frm->rect.height()-fwidth); + theme.partId = WP_SMALLFRAMERIGHT; + d->drawBackground(theme); + theme.rect = QRect(frm->rect.x(), frm->rect.bottom()-fwidth+1, frm->rect.width(), fwidth); + theme.partId = WP_SMALLFRAMEBOTTOM; + d->drawBackground(theme); + return; + } + break; +#endif // QT_CONFIG(dockwidget) + + case PE_FrameTabWidget: + if (const auto *tab = qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option)) { + themeNumber = QWindowsVistaStylePrivate::TabTheme; + partId = TABP_PANE; + + if (widget) { + bool useGradient = true; + const int maxlength = 256; + wchar_t themeFileName[maxlength]; + wchar_t themeColor[maxlength]; + // Due to a a scaling issue with the XP Silver theme, tab gradients are not used with it + if (GetCurrentThemeName(themeFileName, maxlength, themeColor, maxlength, nullptr, 0) == S_OK) { + wchar_t *offset = nullptr; + if ((offset = wcsrchr(themeFileName, QChar(QLatin1Char('\\')).unicode())) != nullptr) { + offset++; + if (!lstrcmp(offset, L"Luna.msstyles") && !lstrcmp(offset, L"Metallic")) + useGradient = false; + } + } + // This should work, but currently there's an error in the ::drawBackgroundDirectly() + // code, when using the HDC directly.. + if (useGradient) { + QStyleOptionTabWidgetFrame frameOpt = *tab; + frameOpt.rect = widget->rect(); + QRect contentsRect = subElementRect(SE_TabWidgetTabContents, &frameOpt, widget); + QRegion reg = option->rect; + reg -= contentsRect; + painter->setClipRegion(reg); + QWindowsThemeData theme(widget, painter, themeNumber, partId, stateId, rect); + theme.mirrorHorizontally = hMirrored; + theme.mirrorVertically = vMirrored; + d->drawBackground(theme); + painter->setClipRect(contentsRect); + partId = TABP_BODY; + } + } + switch (tab->shape) { + case QTabBar::RoundedNorth: + case QTabBar::TriangularNorth: + break; + case QTabBar::RoundedSouth: + case QTabBar::TriangularSouth: + vMirrored = true; + break; + case QTabBar::RoundedEast: + case QTabBar::TriangularEast: + rotate = 90; + break; + case QTabBar::RoundedWest: + case QTabBar::TriangularWest: + rotate = 90; + hMirrored = true; + break; + default: + break; + } + } + break; + + case PE_FrameStatusBarItem: + themeNumber = QWindowsVistaStylePrivate::StatusTheme; + partId = SP_PANE; + break; + + case PE_FrameWindow: + if (const auto *frm = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + themeNumber = QWindowsVistaStylePrivate::WindowTheme; + if (option->state & State_Active) + stateId = FS_ACTIVE; + else + stateId = FS_INACTIVE; + + int fwidth = int((frm->lineWidth + frm->midLineWidth) / QWindowsStylePrivate::nativeMetricScaleFactor(widget)); + + QWindowsThemeData theme(widget, painter, themeNumber, 0, stateId); + if (!theme.isValid()) + break; + + // May fail due to too-large buffers for large widgets, fall back to Windows style. + theme.rect = QRect(option->rect.x(), option->rect.y()+fwidth, option->rect.x()+fwidth, option->rect.height()-fwidth); + theme.partId = WP_FRAMELEFT; + if (!d->drawBackground(theme)) { + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + theme.rect = QRect(option->rect.width()-fwidth, option->rect.y()+fwidth, fwidth, option->rect.height()-fwidth); + theme.partId = WP_FRAMERIGHT; + if (!d->drawBackground(theme)) { + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + theme.rect = QRect(option->rect.x(), option->rect.height()-fwidth, option->rect.width(), fwidth); + theme.partId = WP_FRAMEBOTTOM; + if (!d->drawBackground(theme)) { + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + theme.rect = QRect(option->rect.x(), option->rect.y(), option->rect.width(), option->rect.y()+fwidth); + theme.partId = WP_CAPTION; + if (!d->drawBackground(theme)) + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + break; + + case PE_PanelLineEdit: + if (const auto *panel = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + bool isEnabled = state & State_Enabled; + if (QWindowsVistaStylePrivate::isLineEditBaseColorSet(option, widget)) { + painter->fillRect(panel->rect, panel->palette.brush(QPalette::Base)); + } else { + int partId = EP_BACKGROUND; + int stateId = EBS_NORMAL; + if (!isEnabled) + stateId = EBS_DISABLED; + else if (option->state & State_ReadOnly) + stateId = EBS_READONLY; + else if (option->state & State_MouseOver) + stateId = EBS_HOT; + + QWindowsThemeData theme(nullptr, painter, QWindowsVistaStylePrivate::EditTheme, + partId, stateId, rect); + if (!theme.isValid()) { + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + int bgType; + GetThemeEnumValue(theme.handle(), partId, stateId, TMT_BGTYPE, &bgType); + if (bgType == BT_IMAGEFILE) { + d->drawBackground(theme); + } else { + QBrush fillColor = option->palette.brush(QPalette::Base); + if (!isEnabled) { + PROPERTYORIGIN origin = PO_NOTFOUND; + GetThemePropertyOrigin(theme.handle(), theme.partId, theme.stateId, TMT_FILLCOLOR, &origin); + // Use only if the fill property comes from our part + if ((origin == PO_PART || origin == PO_STATE)) { + COLORREF bgRef; + GetThemeColor(theme.handle(), partId, stateId, TMT_FILLCOLOR, &bgRef); + fillColor = QBrush(qRgb(GetRValue(bgRef), GetGValue(bgRef), GetBValue(bgRef))); + } + } + painter->fillRect(option->rect, fillColor); + } + } + if (panel->lineWidth > 0) + proxy()->drawPrimitive(PE_FrameLineEdit, panel, painter, widget); + } + return; + + case PE_IndicatorButtonDropDown: + themeNumber = QWindowsVistaStylePrivate::ToolBarTheme; + partId = TP_SPLITBUTTONDROPDOWN; + if (!(option->state & State_Enabled)) + stateId = TS_DISABLED; + else if (option->state & State_Sunken) + stateId = TS_PRESSED; + else if (option->state & State_MouseOver) + stateId = option->state & State_On ? TS_HOTCHECKED : TS_HOT; + else if (option->state & State_On) + stateId = TS_CHECKED; + else if (!(option->state & State_AutoRaise)) + stateId = TS_HOT; + else + stateId = TS_NORMAL; + if (option->direction == Qt::RightToLeft) + hMirrored = true; + break; + + case PE_FrameLineEdit: + if (QWindowsVistaAnimation *animate = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option)))) { + animate->paint(painter, option); + } else { + if (QWindowsVistaStylePrivate::isItemViewDelegateLineEdit(widget)) { + // we try to check if this lineedit is a delegate on a QAbstractItemView-derived class. + QPen oldPen = painter->pen(); + // Inner white border + painter->setPen(QPen(option->palette.base().color(), 1)); + painter->drawRect(option->rect.adjusted(1, 1, -2, -2)); + // Outer dark border + painter->setPen(QPen(option->palette.shadow().color(), 1)); + painter->drawRect(option->rect.adjusted(0, 0, -1, -1)); + painter->setPen(oldPen); + return; + } + int stateId = ETS_NORMAL; + if (!(state & State_Enabled)) + stateId = ETS_DISABLED; + else if (state & State_ReadOnly) + stateId = ETS_READONLY; + else if (state & State_HasFocus) + stateId = ETS_SELECTED; + else if (state & State_MouseOver) + stateId = ETS_HOT; + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::EditTheme, + EP_EDITBORDER_NOSCROLL, stateId, option->rect); + theme.noContent = true; + painter->save(); + QRegion clipRegion = option->rect; + clipRegion -= option->rect.adjusted(2, 2, -2, -2); + painter->setClipRegion(clipRegion); + d->drawBackground(theme); + painter->restore(); + } + return; + + case PE_FrameGroupBox: + themeNumber = QWindowsVistaStylePrivate::ButtonTheme; + partId = BP_GROUPBOX; + if (!(option->state & State_Enabled)) + stateId = GBS_DISABLED; + else + stateId = GBS_NORMAL; + if (const auto *frame = qstyleoption_cast<const QStyleOptionFrame *>(option)) { + if (frame->features & QStyleOptionFrame::Flat) { + // Windows XP does not have a theme part for a flat GroupBox, paint it with the windows style + QRect fr = frame->rect; + QPoint p1(fr.x(), fr.y() + 1); + QPoint p2(fr.x() + fr.width(), p1.y() + 1); + rect = QRect(p1, p2); + themeNumber = -1; + } + } + break; + + case PE_IndicatorToolBarHandle: { + QWindowsThemeData theme; + QRect rect; + if (option->state & State_Horizontal) { + theme = QWindowsThemeData(widget, painter, + QWindowsVistaStylePrivate::RebarTheme, + RP_GRIPPER, ETS_NORMAL, option->rect.adjusted(0, 1, -2, -2)); + rect = option->rect.adjusted(0, 1, 0, -2); + rect.setWidth(4); + } else { + theme = QWindowsThemeData(widget, painter, QWindowsVistaStylePrivate::RebarTheme, + RP_GRIPPERVERT, ETS_NORMAL, option->rect.adjusted(0, 1, -2, -2)); + rect = option->rect.adjusted(1, 0, -1, 0); + rect.setHeight(4); + } + theme.rect = rect; + d->drawBackground(theme); + return; + } + + case PE_IndicatorToolBarSeparator: { + QPen pen = painter->pen(); + int margin = 3; + painter->setPen(option->palette.window().color().darker(114)); + if (option->state & State_Horizontal) { + int x1 = option->rect.center().x(); + painter->drawLine(QPoint(x1, option->rect.top() + margin), QPoint(x1, option->rect.bottom() - margin)); + } else { + int y1 = option->rect.center().y(); + painter->drawLine(QPoint(option->rect.left() + margin, y1), QPoint(option->rect.right() - margin, y1)); + } + painter->setPen(pen); + return; + } + + case PE_PanelTipLabel: { + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::ToolTipTheme, + TTP_STANDARD, TTSS_NORMAL, option->rect); + d->drawBackground(theme); + return; + } + + case PE_FrameTabBarBase: + if (const auto *tbb = qstyleoption_cast<const QStyleOptionTabBarBase *>(option)) { + painter->save(); + switch (tbb->shape) { + case QTabBar::RoundedNorth: + painter->setPen(QPen(tbb->palette.dark(), 0)); + painter->drawLine(tbb->rect.topLeft(), tbb->rect.topRight()); + break; + case QTabBar::RoundedWest: + painter->setPen(QPen(tbb->palette.dark(), 0)); + painter->drawLine(tbb->rect.left(), tbb->rect.top(), tbb->rect.left(), tbb->rect.bottom()); + break; + case QTabBar::RoundedSouth: + painter->setPen(QPen(tbb->palette.dark(), 0)); + painter->drawLine(tbb->rect.left(), tbb->rect.top(), + tbb->rect.right(), tbb->rect.top()); + break; + case QTabBar::RoundedEast: + painter->setPen(QPen(tbb->palette.dark(), 0)); + painter->drawLine(tbb->rect.topLeft(), tbb->rect.bottomLeft()); + break; + case QTabBar::TriangularNorth: + case QTabBar::TriangularEast: + case QTabBar::TriangularWest: + case QTabBar::TriangularSouth: + painter->restore(); + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + painter->restore(); + } + return; + + case PE_Widget: { +#if QT_CONFIG(dialogbuttonbox) + const QDialogButtonBox *buttonBox = nullptr; + if (qobject_cast<const QMessageBox *> (widget)) + buttonBox = widget->findChild<const QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); +#if QT_CONFIG(inputdialog) + else if (qobject_cast<const QInputDialog *> (widget)) + buttonBox = widget->findChild<const QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); +#endif // QT_CONFIG(inputdialog) + if (buttonBox) { + //draw white panel part + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::TaskDialogTheme, + TDLG_PRIMARYPANEL, 0, option->rect); + QRect toprect = option->rect; + toprect.setBottom(buttonBox->geometry().top()); + theme.rect = toprect; + d->drawBackground(theme); + + //draw bottom panel part + QRect buttonRect = option->rect; + buttonRect.setTop(buttonBox->geometry().top()); + theme.rect = buttonRect; + theme.partId = TDLG_SECONDARYPANEL; + d->drawBackground(theme); + } +#endif + return; + } + + case PE_PanelItemViewItem: { + const QStyleOptionViewItem *vopt; + bool newStyle = true; + QAbstractItemView::SelectionBehavior selectionBehavior = QAbstractItemView::SelectRows; + QAbstractItemView::SelectionMode selectionMode = QAbstractItemView::NoSelection; + if (const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(widget)) { + newStyle = !qobject_cast<const QTableView*>(view); + selectionBehavior = view->selectionBehavior(); + selectionMode = view->selectionMode(); +#if QT_CONFIG(accessibility) + } else if (!widget) { + newStyle = !QStyleHelper::hasAncestor(option->styleObject, QAccessible::MenuItem) ; +#endif + } + + if (newStyle && (vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option))) { + bool selected = vopt->state & QStyle::State_Selected; + const bool hover = selectionMode != QAbstractItemView::NoSelection && (vopt->state & QStyle::State_MouseOver); + bool active = vopt->state & QStyle::State_Active; + + if (vopt->features & QStyleOptionViewItem::Alternate) + painter->fillRect(vopt->rect, vopt->palette.alternateBase()); + + QPalette::ColorGroup cg = vopt->state & QStyle::State_Enabled + ? QPalette::Normal : QPalette::Disabled; + if (cg == QPalette::Normal && !(vopt->state & QStyle::State_Active)) + cg = QPalette::Inactive; + + QRect itemRect = subElementRect(QStyle::SE_ItemViewItemFocusRect, option, widget).adjusted(-1, 0, 1, 0); + itemRect.setTop(vopt->rect.top()); + itemRect.setBottom(vopt->rect.bottom()); + + QSize sectionSize = itemRect.size(); + if (vopt->showDecorationSelected) + sectionSize = vopt->rect.size(); + + if (selectionBehavior == QAbstractItemView::SelectRows) + sectionSize.setWidth(vopt->rect.width()); + QPixmap pixmap; + + if (vopt->backgroundBrush.style() != Qt::NoBrush) { + const QPointF oldBrushOrigin = painter->brushOrigin(); + painter->setBrushOrigin(vopt->rect.topLeft()); + painter->fillRect(vopt->rect, vopt->backgroundBrush); + painter->setBrushOrigin(oldBrushOrigin); + } + + if (hover || selected) { + if (sectionSize.width() > 0 && sectionSize.height() > 0) { + QString key = QString::fromLatin1("qvdelegate-%1-%2-%3-%4-%5").arg(sectionSize.width()) + .arg(sectionSize.height()).arg(selected).arg(active).arg(hover); + if (!QPixmapCache::find(key, &pixmap)) { + pixmap = QPixmap(sectionSize); + pixmap.fill(Qt::transparent); + + int state; + if (selected && hover) + state = LISS_HOTSELECTED; + else if (selected && !active) + state = LISS_SELECTEDNOTFOCUS; + else if (selected) + state = LISS_SELECTED; + else + state = LISS_HOT; + + QPainter pixmapPainter(&pixmap); + + QWindowsThemeData theme(widget, &pixmapPainter, + QWindowsVistaStylePrivate::VistaTreeViewTheme, + LVP_LISTITEM, state, QRect(0, 0, sectionSize.width(), sectionSize.height())); + + if (!theme.isValid()) + break; + + d->drawBackground(theme); + QPixmapCache::insert(key, pixmap); + } + } + + if (vopt->showDecorationSelected) { + const int frame = 2; //Assumes a 2 pixel pixmap border + QRect srcRect = QRect(0, 0, sectionSize.width(), sectionSize.height()); + QRect pixmapRect = vopt->rect; + bool reverse = vopt->direction == Qt::RightToLeft; + bool leftSection = vopt->viewItemPosition == QStyleOptionViewItem::Beginning; + bool rightSection = vopt->viewItemPosition == QStyleOptionViewItem::End; + if (vopt->viewItemPosition == QStyleOptionViewItem::OnlyOne + || vopt->viewItemPosition == QStyleOptionViewItem::Invalid) + painter->drawPixmap(pixmapRect.topLeft(), pixmap); + else if (reverse ? rightSection : leftSection){ + painter->drawPixmap(QRect(pixmapRect.topLeft(), + QSize(frame, pixmapRect.height())), pixmap, + QRect(QPoint(0, 0), QSize(frame, pixmapRect.height()))); + painter->drawPixmap(pixmapRect.adjusted(frame, 0, 0, 0), + pixmap, srcRect.adjusted(frame, 0, -frame, 0)); + } else if (reverse ? leftSection : rightSection) { + painter->drawPixmap(QRect(pixmapRect.topRight() - QPoint(frame - 1, 0), + QSize(frame, pixmapRect.height())), pixmap, + QRect(QPoint(pixmapRect.width() - frame, 0), + QSize(frame, pixmapRect.height()))); + painter->drawPixmap(pixmapRect.adjusted(0, 0, -frame, 0), + pixmap, srcRect.adjusted(frame, 0, -frame, 0)); + } else if (vopt->viewItemPosition == QStyleOptionViewItem::Middle) + painter->drawPixmap(pixmapRect, pixmap, + srcRect.adjusted(frame, 0, -frame, 0)); + } else { + if (vopt->text.isEmpty() && vopt->icon.isNull()) + break; + painter->drawPixmap(itemRect.topLeft(), pixmap); + } + } + return; + } + break; + } + + default: + break; + } + + QWindowsThemeData theme(widget, painter, themeNumber, partId, stateId, rect); + + if (!theme.isValid()) { + QWindowsStyle::drawPrimitive(element, option, painter, widget); + return; + } + + theme.mirrorHorizontally = hMirrored; + theme.mirrorVertically = vMirrored; + theme.noBorder = noBorder; + theme.noContent = noContent; + theme.rotate = rotate; + + d->drawBackground(theme); +} + +/*! \internal */ +int QWindowsVistaStyle::styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, + QStyleHintReturn *returnData) const +{ + QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); + + int ret = 0; + switch (hint) { + case SH_EtchDisabledText: + ret = (qobject_cast<const QLabel*>(widget) != 0); + break; + + case SH_SpinControls_DisableOnBounds: + ret = 0; + break; + + case SH_TitleBar_AutoRaise: + case SH_TitleBar_NoBorder: + ret = 1; + break; + + case SH_GroupBox_TextLabelColor: + if (!widget || widget->isEnabled()) + ret = d->groupBoxTextColor; + else + ret = d->groupBoxTextColorDisabled; + break; + + case SH_WindowFrame_Mask: { + ret = 1; + auto *mask = qstyleoption_cast<QStyleHintReturnMask *>(returnData); + const auto *titlebar = qstyleoption_cast<const QStyleOptionTitleBar *>(option); + if (mask && titlebar) { + // Note certain themes will not return the whole window frame but only the titlebar part when + // queried This function needs to return the entire window mask, hence we will only fetch the mask for the + // titlebar itself and add the remaining part of the window rect at the bottom. + int tbHeight = proxy()->pixelMetric(PM_TitleBarHeight, option, widget); + QRect titleBarRect = option->rect; + titleBarRect.setHeight(tbHeight); + QWindowsThemeData themeData; + if (titlebar->titleBarState & Qt::WindowMinimized) { + themeData = QWindowsThemeData(widget, nullptr, + QWindowsVistaStylePrivate::WindowTheme, + WP_MINCAPTION, CS_ACTIVE, titleBarRect); + } else + themeData = QWindowsThemeData(widget, nullptr, + QWindowsVistaStylePrivate::WindowTheme, + WP_CAPTION, CS_ACTIVE, titleBarRect); + mask->region = d->region(themeData) + + QRect(0, tbHeight, option->rect.width(), option->rect.height() - tbHeight); + } + break; + } + +#if QT_CONFIG(rubberband) + case SH_RubberBand_Mask: + if (qstyleoption_cast<const QStyleOptionRubberBand *>(option)) + ret = 0; + break; +#endif // QT_CONFIG(rubberband) + + case SH_MessageBox_CenterButtons: + ret = false; + break; + + case SH_ToolTip_Mask: + if (option) { + if (QStyleHintReturnMask *mask = qstyleoption_cast<QStyleHintReturnMask*>(returnData)) { + ret = true; + QWindowsThemeData themeData(widget, nullptr, + QWindowsVistaStylePrivate::ToolTipTheme, + TTP_STANDARD, TTSS_NORMAL, option->rect); + mask->region = d->region(themeData); + } + } + break; + + case SH_Table_GridLineColor: + if (option) + ret = int(option->palette.color(QPalette::Base).darker(118).rgba()); + else + ret = -1; + break; + + case SH_Header_ArrowAlignment: + ret = Qt::AlignTop | Qt::AlignHCenter; + break; + + case SH_ItemView_DrawDelegateFrame: + ret = 1; + break; + + default: + ret = QWindowsStyle::styleHint(hint, option, widget, returnData); + break; + } + + return ret; +} + + +/*! + \internal + + see drawPrimitive for comments on the animation support + */ +void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption *option, + QPainter *painter, const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) { + QWindowsStyle::drawControl(element, option, painter, widget); + return; + } + + QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); + + bool selected = option->state & State_Selected; + bool pressed = option->state & State_Sunken; + bool disabled = !(option->state & State_Enabled); + + int state = option->state; + int themeNumber = -1; + + QRect rect(option->rect); + State flags = option->state; + int partId = 0; + int stateId = 0; + + if (d->transitionsEnabled() && canAnimate(option)) { + if (element == CE_PushButtonBevel) { + QRect oldRect; + QRect newRect; + + QObject *styleObject = option->styleObject; + + int oldState = styleObject->property("_q_stylestate").toInt(); + oldRect = styleObject->property("_q_stylerect").toRect(); + newRect = option->rect; + styleObject->setProperty("_q_stylestate", int(option->state)); + styleObject->setProperty("_q_stylerect", option->rect); + + bool wasDefault = false; + bool isDefault = false; + if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton *>(option)) { + wasDefault = styleObject->property("_q_isdefault").toBool(); + isDefault = button->features & QStyleOptionButton::DefaultButton; + styleObject->setProperty("_q_isdefault", isDefault); + } + + bool doTransition = ((state & State_Sunken) != (oldState & State_Sunken) || + (state & State_On) != (oldState & State_On) || + (state & State_MouseOver) != (oldState & State_MouseOver)); + + if (oldRect != newRect || (wasDefault && !isDefault)) { + doTransition = false; + d->stopAnimation(styleObject); + } + + if (doTransition) { + styleObject->setProperty("_q_no_animation", true); + + QWindowsVistaTransition *t = new QWindowsVistaTransition(styleObject); + QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); + QStyleOption *styleOption = clonedAnimationStyleOption(option); + styleOption->state = QStyle::State(oldState); + + QImage startImage = createAnimationBuffer(option, widget); + QPainter startPainter(&startImage); + + // Use current state of existing animation if already one is running + if (!anim) { + proxy()->drawControl(element, styleOption, &startPainter, widget); + } else { + anim->paint(&startPainter, styleOption); + d->stopAnimation(styleObject); + } + + t->setStartImage(startImage); + QImage endImage = createAnimationBuffer(option, widget); + QPainter endPainter(&endImage); + styleOption->state = option->state; + proxy()->drawControl(element, styleOption, &endPainter, widget); + t->setEndImage(endImage); + + + DWORD duration = 0; + const HTHEME theme = OpenThemeData(nullptr, L"Button"); + + int fromState = buttonStateId(oldState, BP_PUSHBUTTON); + int toState = buttonStateId(option->state, BP_PUSHBUTTON); + if (GetThemeTransitionDuration(theme, BP_PUSHBUTTON, fromState, toState, TMT_TRANSITIONDURATIONS, &duration) == S_OK) + t->setDuration(int(duration)); + else + t->setDuration(0); + t->setStartTime(d->animationTime()); + styleObject->setProperty("_q_no_animation", false); + + deleteClonedAnimationStyleOption(styleOption); + d->startAnimation(t); + } + + QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); + if (anim) { + anim->paint(painter, option); + return; + } + + } + } + + bool hMirrored = false; + bool vMirrored = false; + int rotate = 0; + + switch (element) { + case CE_PushButtonBevel: + if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { + themeNumber = QWindowsVistaStylePrivate::ButtonTheme; + partId = BP_PUSHBUTTON; + if (btn->features & QStyleOptionButton::CommandLinkButton) + partId = BP_COMMANDLINK; + bool justFlat = (btn->features & QStyleOptionButton::Flat) && !(flags & (State_On|State_Sunken)); + if (!(flags & State_Enabled) && !(btn->features & QStyleOptionButton::Flat)) + stateId = PBS_DISABLED; + else if (justFlat) + ; + else if (flags & (State_Sunken | State_On)) + stateId = PBS_PRESSED; + else if (flags & State_MouseOver) + stateId = PBS_HOT; + else if (btn->features & QStyleOptionButton::DefaultButton && (state & State_Active)) + stateId = PBS_DEFAULTED; + else + stateId = PBS_NORMAL; + + if (!justFlat) { + + if (d->transitionsEnabled() && (btn->features & QStyleOptionButton::DefaultButton) && + !(state & (State_Sunken | State_On)) && !(state & State_MouseOver) && + (state & State_Enabled) && (state & State_Active)) + { + QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option))); + + if (!anim) { + QImage startImage = createAnimationBuffer(option, widget); + QImage alternateImage = createAnimationBuffer(option, widget); + + QWindowsVistaPulse *pulse = new QWindowsVistaPulse(styleObject(option)); + + QPainter startPainter(&startImage); + stateId = PBS_DEFAULTED; + QWindowsThemeData theme(widget, &startPainter, themeNumber, partId, stateId, rect); + d->drawBackground(theme); + + QPainter alternatePainter(&alternateImage); + theme.stateId = PBS_DEFAULTED_ANIMATING; + theme.painter = &alternatePainter; + d->drawBackground(theme); + + pulse->setStartImage(startImage); + pulse->setEndImage(alternateImage); + pulse->setStartTime(d->animationTime()); + pulse->setDuration(2000); + d->startAnimation(pulse); + anim = pulse; + } + + if (anim) + anim->paint(painter, option); + else { + QWindowsThemeData theme(widget, painter, themeNumber, partId, stateId, rect); + d->drawBackground(theme); + } + } + else { + QWindowsThemeData theme(widget, painter, themeNumber, partId, stateId, rect); + d->drawBackground(theme); + } + } + + if (btn->features & QStyleOptionButton::HasMenu) { + int mbiw = 0, mbih = 0; + QWindowsThemeData theme(widget, nullptr, QWindowsVistaStylePrivate::ToolBarTheme, + TP_DROPDOWNBUTTON); + if (theme.isValid()) { + const QSizeF size = theme.size() * QStyleHelper::dpiScaled(1, option); + if (!size.isEmpty()) { + mbiw = qRound(size.width()); + mbih = qRound(size.height()); + } + } + QRect ir = subElementRect(SE_PushButtonContents, option, nullptr); + QStyleOptionButton newBtn = *btn; + newBtn.rect = QStyle::visualRect(option->direction, option->rect, + QRect(ir.right() - mbiw - 2, + option->rect.top() + (option->rect.height()/2) - (mbih/2), + mbiw + 1, mbih + 1)); + proxy()->drawPrimitive(PE_IndicatorArrowDown, &newBtn, painter, widget); + } + } + return; + + case CE_SizeGrip: { + themeNumber = QWindowsVistaStylePrivate::StatusTheme; + partId = SP_GRIPPER; + QWindowsThemeData theme(nullptr, painter, themeNumber, partId); + QSize size = (theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); + size.rheight()--; + if (const auto *sg = qstyleoption_cast<const QStyleOptionSizeGrip *>(option)) { + switch (sg->corner) { + case Qt::BottomRightCorner: + rect = QRect(QPoint(rect.right() - size.width(), rect.bottom() - size.height()), size); + break; + case Qt::BottomLeftCorner: + rect = QRect(QPoint(rect.left() + 1, rect.bottom() - size.height()), size); + hMirrored = true; + break; + case Qt::TopRightCorner: + rect = QRect(QPoint(rect.right() - size.width(), rect.top() + 1), size); + vMirrored = true; + break; + case Qt::TopLeftCorner: + rect = QRect(rect.topLeft() + QPoint(1, 1), size); + hMirrored = vMirrored = true; + } + } + break; + } + + case CE_Splitter: + painter->eraseRect(option->rect); + return; + + case CE_TabBarTab: + if (const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) + stateId = tab->state & State_Enabled ? TIS_NORMAL : TIS_DISABLED; + break; + + case CE_TabBarTabShape: + if (const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) { + themeNumber = QWindowsVistaStylePrivate::TabTheme; + const bool isDisabled = !(tab->state & State_Enabled); + const bool hasFocus = tab->state & State_HasFocus; + const bool isHot = tab->state & State_MouseOver; + const bool selected = tab->state & State_Selected; + bool lastTab = tab->position == QStyleOptionTab::End; + bool firstTab = tab->position == QStyleOptionTab::Beginning; + const bool onlyOne = tab->position == QStyleOptionTab::OnlyOneTab; + const bool leftAligned = proxy()->styleHint(SH_TabBar_Alignment, tab, widget) == Qt::AlignLeft; + const bool centerAligned = proxy()->styleHint(SH_TabBar_Alignment, tab, widget) == Qt::AlignCenter; + const int borderThickness = proxy()->pixelMetric(PM_DefaultFrameWidth, option, widget); + const int tabOverlap = proxy()->pixelMetric(PM_TabBarTabOverlap, option, widget); + + if (isDisabled) + stateId = TIS_DISABLED; + else if (selected) + stateId = TIS_SELECTED; + else if (hasFocus) + stateId = TIS_FOCUSED; + else if (isHot) + stateId = TIS_HOT; + else + stateId = TIS_NORMAL; + + // Selecting proper part depending on position + if (firstTab || onlyOne) { + if (leftAligned) + partId = TABP_TABITEMLEFTEDGE; + else if (centerAligned) + partId = TABP_TABITEM; + else // rightAligned + partId = TABP_TABITEMRIGHTEDGE; + } else { + partId = TABP_TABITEM; + } + + if (tab->direction == Qt::RightToLeft + && (tab->shape == QTabBar::RoundedNorth || tab->shape == QTabBar::RoundedSouth)) { + bool temp = firstTab; + firstTab = lastTab; + lastTab = temp; + } + + const bool begin = firstTab || onlyOne; + const bool end = lastTab || onlyOne; + + switch (tab->shape) { + case QTabBar::RoundedNorth: + if (selected) + rect.adjust(begin ? 0 : -tabOverlap, 0, end ? 0 : tabOverlap, borderThickness); + else + rect.adjust(begin? tabOverlap : 0, tabOverlap, end ? -tabOverlap : 0, 0); + break; + case QTabBar::RoundedSouth: + //vMirrored = true; + rotate = 180; // Not 100% correct, but works + if (selected) + rect.adjust(begin ? 0 : -tabOverlap , -borderThickness, end ? 0 : tabOverlap, 0); + else + rect.adjust(begin ? tabOverlap : 0, 0, end ? -tabOverlap : 0 , -tabOverlap); + break; + case QTabBar::RoundedEast: + rotate = 90; + if (selected) + rect.adjust(-borderThickness, begin ? 0 : -tabOverlap, 0, end ? 0 : tabOverlap); + else + rect.adjust(0, begin ? tabOverlap : 0, -tabOverlap, end ? -tabOverlap : 0); + break; + case QTabBar::RoundedWest: + hMirrored = true; + rotate = 90; + if (selected) + rect.adjust(0, begin ? 0 : -tabOverlap, borderThickness, end ? 0 : tabOverlap); + else + rect.adjust(tabOverlap, begin ? tabOverlap : 0, 0, end ? -tabOverlap : 0); + break; + default: + themeNumber = -1; // Do our own painting for triangular + break; + } + + if (!selected) { + switch (tab->shape) { + case QTabBar::RoundedNorth: + rect.adjust(0,0, 0,-1); + break; + case QTabBar::RoundedSouth: + rect.adjust(0,1, 0,0); + break; + case QTabBar::RoundedEast: + rect.adjust( 1,0, 0,0); + break; + case QTabBar::RoundedWest: + rect.adjust(0,0, -1,0); + break; + default: + break; + } + } + } + break; + + case CE_ProgressBarGroove: { + Qt::Orientation orient = Qt::Horizontal; + if (const auto *pb = qstyleoption_cast<const QStyleOptionProgressBar *>(option)) + if (!(pb->state & QStyle::State_Horizontal)) + orient = Qt::Vertical; + + partId = (orient == Qt::Horizontal) ? PP_BAR : PP_BARVERT; + themeNumber = QWindowsVistaStylePrivate::ProgressTheme; + stateId = 1; + break; + } + + case CE_ProgressBarContents: + if (const auto *bar = qstyleoption_cast<const QStyleOptionProgressBar *>(option)) { + bool isIndeterminate = (bar->minimum == 0 && bar->maximum == 0); + const bool vertical = !(bar->state & QStyle::State_Horizontal); + const bool inverted = bar->invertedAppearance; + + if (isIndeterminate || (bar->progress > 0 && (bar->progress < bar->maximum) && d->transitionsEnabled())) { + if (!d->animation(styleObject(option))) + d->startAnimation(new QProgressStyleAnimation(d->animationFps, styleObject(option))); + } else { + d->stopAnimation(styleObject(option)); + } + + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::ProgressTheme, + vertical ? PP_FILLVERT : PP_FILL); + theme.rect = option->rect; + bool reverse = (bar->direction == Qt::LeftToRight && inverted) || (bar->direction == Qt::RightToLeft && !inverted); + QTime current = d->animationTime(); + + if (isIndeterminate) { + if (auto *progressAnimation = qobject_cast<QProgressStyleAnimation *>(d->animation(styleObject(option)))) { + int glowSize = 120; + int animationWidth = glowSize * 2 + (vertical ? theme.rect.height() : theme.rect.width()); + int animOffset = progressAnimation->startTime().msecsTo(current) / 4; + if (animOffset > animationWidth) + progressAnimation->setStartTime(d->animationTime()); + painter->save(); + painter->setClipRect(theme.rect); + QRect animRect; + QSize pixmapSize(14, 14); + if (vertical) { + animRect = QRect(theme.rect.left(), + inverted ? rect.top() - glowSize + animOffset : + rect.bottom() + glowSize - animOffset, + rect.width(), glowSize); + pixmapSize.setHeight(animRect.height()); + } else { + animRect = QRect(rect.left() - glowSize + animOffset, + rect.top(), glowSize, rect.height()); + animRect = QStyle::visualRect(reverse ? Qt::RightToLeft : Qt::LeftToRight, + option->rect, animRect); + pixmapSize.setWidth(animRect.width()); + } + QString name = QString::fromLatin1("qiprogress-%1-%2").arg(pixmapSize.width()).arg(pixmapSize.height()); + QPixmap pixmap; + if (!QPixmapCache::find(name, &pixmap)) { + QImage image(pixmapSize, QImage::Format_ARGB32); + image.fill(Qt::transparent); + QPainter imagePainter(&image); + theme.painter = &imagePainter; + theme.partId = vertical ? PP_FILLVERT : PP_FILL; + theme.rect = QRect(QPoint(0,0), animRect.size()); + QLinearGradient alphaGradient(0, 0, vertical ? 0 : image.width(), + vertical ? image.height() : 0); + alphaGradient.setColorAt(0, QColor(0, 0, 0, 0)); + alphaGradient.setColorAt(0.5, QColor(0, 0, 0, 220)); + alphaGradient.setColorAt(1, QColor(0, 0, 0, 0)); + imagePainter.fillRect(image.rect(), alphaGradient); + imagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + d->drawBackground(theme); + imagePainter.end(); + pixmap = QPixmap::fromImage(image); + QPixmapCache::insert(name, pixmap); + } + painter->drawPixmap(animRect, pixmap); + painter->restore(); + } + } else { + qint64 progress = qMax<qint64>(bar->progress, bar->minimum); // workaround for bug in QProgressBar + + if (vertical) { + int maxHeight = option->rect.height(); + int minHeight = 0; + double vc6_workaround = ((progress - qint64(bar->minimum)) / qMax(double(1.0), double(qint64(bar->maximum) - qint64(bar->minimum))) * maxHeight); + int height = isIndeterminate ? maxHeight: qMax(int(vc6_workaround), minHeight); + theme.rect.setHeight(height); + if (!inverted) + theme.rect.moveTop(rect.height() - theme.rect.height()); + } else { + int maxWidth = option->rect.width(); + int minWidth = 0; + double vc6_workaround = ((progress - qint64(bar->minimum)) / qMax(double(1.0), double(qint64(bar->maximum) - qint64(bar->minimum))) * maxWidth); + int width = isIndeterminate ? maxWidth : qMax(int(vc6_workaround), minWidth); + theme.rect.setWidth(width); + theme.rect = QStyle::visualRect(reverse ? Qt::RightToLeft : Qt::LeftToRight, + option->rect, theme.rect); + } + d->drawBackground(theme); + + if (QProgressStyleAnimation *a = qobject_cast<QProgressStyleAnimation *>(d->animation(styleObject(option)))) { + int glowSize = 140; + int animationWidth = glowSize * 2 + (vertical ? theme.rect.height() : theme.rect.width()); + int animOffset = a->startTime().msecsTo(current) / 4; + theme.partId = vertical ? PP_MOVEOVERLAYVERT : PP_MOVEOVERLAY; + if (animOffset > animationWidth) { + if (bar->progress < bar->maximum) + a->setStartTime(d->animationTime()); + else + d->stopAnimation(styleObject(option)); //we stop the glow motion only after it has + //moved out of view + } + painter->save(); + painter->setClipRect(theme.rect); + if (vertical) { + theme.rect = QRect(theme.rect.left(), + inverted ? rect.top() - glowSize + animOffset : + rect.bottom() + glowSize - animOffset, + rect.width(), glowSize); + } else { + theme.rect = QRect(rect.left() - glowSize + animOffset,rect.top(), glowSize, rect.height()); + theme.rect = QStyle::visualRect(reverse ? Qt::RightToLeft : Qt::LeftToRight, option->rect, theme.rect); + } + d->drawBackground(theme); + painter->restore(); + } + } + } + return; + + case CE_MenuBarItem: + if (const auto *mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { + if (mbi->menuItemType == QStyleOptionMenuItem::DefaultItem) + break; + + QPalette::ColorRole textRole = disabled ? QPalette::Text : QPalette::ButtonText; + QPixmap pix = mbi->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), QIcon::Normal); + + int alignment = Qt::AlignCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, mbi, widget)) + alignment |= Qt::TextHideMnemonic; + + if (widget && mbi->palette.color(QPalette::Window) != Qt::transparent) { // Not needed for QtQuick Controls + //The rect adjustment is a workaround for the menu not really filling its background. + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::MenuTheme, + MENU_BARBACKGROUND, 0, option->rect.adjusted(-1, 0, 2, 1)); + d->drawBackground(theme); + + int stateId = MBI_NORMAL; + if (disabled) + stateId = MBI_DISABLED; + else if (pressed) + stateId = MBI_PUSHED; + else if (selected) + stateId = MBI_HOT; + + QWindowsThemeData theme2(widget, painter, + QWindowsVistaStylePrivate::MenuTheme, + MENU_BARITEM, stateId, option->rect); + d->drawBackground(theme2); + } + + if (!pix.isNull()) + drawItemPixmap(painter, mbi->rect, alignment, pix); + else + drawItemText(painter, mbi->rect, alignment, mbi->palette, mbi->state & State_Enabled, mbi->text, textRole); + } + return; + +#if QT_CONFIG(menu) + case CE_MenuEmptyArea: + if (const auto *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { + QBrush fill = menuitem->palette.brush((menuitem->state & State_Selected) ? + QPalette::Highlight : QPalette::Button); + painter->fillRect(rect, fill); + break; + } + return; + + case CE_MenuItem: + if (const auto *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { + // windows always has a check column, regardless whether we have an icon or not + const qreal factor = QWindowsVistaStylePrivate::nativeMetricScaleFactor(widget); + int checkcol = qRound(qreal(25) * factor); + const int gutterWidth = qRound(qreal(3) * factor); + { + QWindowsThemeData theme(widget, nullptr, QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPCHECKBACKGROUND, MBI_HOT); + QWindowsThemeData themeSize = theme; + themeSize.partId = MENU_POPUPCHECK; + themeSize.stateId = 0; + const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + const QMarginsF margins = themeSize.margins() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + checkcol = qMax(menuitem->maxIconWidth, qRound(gutterWidth + size.width() + margins.left() + margins.right())); + } + QRect rect = option->rect; + + //draw vertical menu line + if (option->direction == Qt::LeftToRight) + checkcol += rect.x(); + QPoint p1 = QStyle::visualPos(option->direction, menuitem->rect, QPoint(checkcol, rect.top())); + QPoint p2 = QStyle::visualPos(option->direction, menuitem->rect, QPoint(checkcol, rect.bottom())); + QRect gutterRect(p1.x(), p1.y(), gutterWidth, p2.y() - p1.y() + 1); + QWindowsThemeData theme2(widget, painter, QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPGUTTER, stateId, gutterRect); + d->drawBackground(theme2); + + int x, y, w, h; + menuitem->rect.getRect(&x, &y, &w, &h); + int tab = menuitem->reservedShortcutWidth; + bool dis = !(menuitem->state & State_Enabled); + bool checked = menuitem->checkType != QStyleOptionMenuItem::NotCheckable + ? menuitem->checked : false; + bool act = menuitem->state & State_Selected; + + if (menuitem->menuItemType == QStyleOptionMenuItem::Separator) { + int yoff = y-2 + h / 2; + const int separatorSize = qRound(qreal(6) * QWindowsStylePrivate::nativeMetricScaleFactor(widget)); + QPoint p1 = QPoint(x + checkcol, yoff); + QPoint p2 = QPoint(x + w + separatorSize, yoff); + stateId = MBI_HOT; + QRect subRect(p1.x() + (gutterWidth - menuitem->rect.x()), p1.y(), + p2.x() - p1.x(), separatorSize); + subRect = QStyle::visualRect(option->direction, option->rect, subRect ); + QWindowsThemeData theme2(widget, painter, + QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPSEPARATOR, stateId, subRect); + d->drawBackground(theme2); + return; + } + + QRect vCheckRect = visualRect(option->direction, menuitem->rect, QRect(menuitem->rect.x(), + menuitem->rect.y(), checkcol - (gutterWidth + menuitem->rect.x()), menuitem->rect.height())); + + if (act) { + stateId = dis ? MBI_DISABLED : MBI_HOT; + QWindowsThemeData theme2(widget, painter, + QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPITEM, stateId, option->rect); + d->drawBackground(theme2); + } + + if (checked) { + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPCHECKBACKGROUND, + menuitem->icon.isNull() ? MBI_HOT : MBI_PUSHED, vCheckRect); + QWindowsThemeData themeSize = theme; + themeSize.partId = MENU_POPUPCHECK; + themeSize.stateId = 0; + const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + const QMarginsF margins = themeSize.margins() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + QRect checkRect(0, 0, qRound(size.width() + margins.left() + margins.right()), + qRound(size.height() + margins.bottom() + margins.top())); + checkRect.moveCenter(vCheckRect.center()); + theme.rect = checkRect; + + d->drawBackground(theme); + + if (menuitem->icon.isNull()) { + checkRect = QRect(QPoint(0, 0), size.toSize()); + checkRect.moveCenter(theme.rect.center()); + theme.rect = checkRect; + + theme.partId = MENU_POPUPCHECK; + bool bullet = menuitem->checkType & QStyleOptionMenuItem::Exclusive; + if (dis) + theme.stateId = bullet ? MC_BULLETDISABLED: MC_CHECKMARKDISABLED; + else + theme.stateId = bullet ? MC_BULLETNORMAL: MC_CHECKMARKNORMAL; + d->drawBackground(theme); + } + } + + if (!menuitem->icon.isNull()) { + QIcon::Mode mode = dis ? QIcon::Disabled : QIcon::Normal; + if (act && !dis) + mode = QIcon::Active; + const auto size = proxy()->pixelMetric(PM_SmallIconSize, option, widget); + const auto dpr = painter->device()->devicePixelRatio(); + const auto pixmap = menuitem->icon.pixmap({size, size}, dpr, mode, + checked ? QIcon::On : QIcon::Off); + QRect pmr(QPoint(0, 0), pixmap.deviceIndependentSize().toSize()); + pmr.moveCenter(vCheckRect.center()); + painter->setPen(menuitem->palette.text().color()); + painter->drawPixmap(pmr.topLeft(), pixmap); + } + + painter->setPen(menuitem->palette.buttonText().color()); + + const QColor textColor = menuitem->palette.text().color(); + if (dis) + painter->setPen(textColor); + + int xm = windowsItemFrame + checkcol + windowsItemHMargin + (gutterWidth - menuitem->rect.x()) - 1; + int xpos = menuitem->rect.x() + xm; + QRect textRect(xpos, y + windowsItemVMargin, w - xm - windowsRightBorder - tab + 1, h - 2 * windowsItemVMargin); + QRect vTextRect = visualRect(option->direction, menuitem->rect, textRect); + QString s = menuitem->text; + if (!s.isEmpty()) { // draw text + painter->save(); + int t = s.indexOf(QLatin1Char('\t')); + int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, menuitem, widget)) + text_flags |= Qt::TextHideMnemonic; + text_flags |= Qt::AlignLeft; + if (t >= 0) { + QRect vShortcutRect = visualRect(option->direction, menuitem->rect, + QRect(textRect.topRight(), QPoint(menuitem->rect.right(), textRect.bottom()))); + painter->drawText(vShortcutRect, text_flags, s.mid(t + 1)); + s = s.left(t); + } + QFont font = menuitem->font; + if (menuitem->menuItemType == QStyleOptionMenuItem::DefaultItem) + font.setBold(true); + painter->setFont(font); + painter->setPen(textColor); + painter->drawText(vTextRect, text_flags, s.left(t)); + painter->restore(); + } + if (menuitem->menuItemType == QStyleOptionMenuItem::SubMenu) {// draw sub menu arrow + int dim = (h - 2 * windowsItemFrame) / 2; + PrimitiveElement arrow; + arrow = (option->direction == Qt::RightToLeft) ? PE_IndicatorArrowLeft : PE_IndicatorArrowRight; + xpos = x + w - windowsArrowHMargin - windowsItemFrame - dim; + QRect vSubMenuRect = visualRect(option->direction, menuitem->rect, QRect(xpos, y + h / 2 - dim / 2, dim, dim)); + QStyleOptionMenuItem newMI = *menuitem; + newMI.rect = vSubMenuRect; + newMI.state = dis ? State_None : State_Enabled; + proxy()->drawPrimitive(arrow, &newMI, painter, widget); + } + } + return; +#endif // QT_CONFIG(menu) + + case CE_HeaderSection: + if (const QStyleOptionHeader *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { + partId = HP_HEADERITEM; + if (flags & State_Sunken) + stateId = HIS_PRESSED; + else if (flags & State_MouseOver) + stateId = HIS_HOT; + else + stateId = HIS_NORMAL; + + if (header->sortIndicator != QStyleOptionHeader::None) + stateId += 3; + + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::HeaderTheme, + partId, stateId, option->rect); + d->drawBackground(theme); + } + return; + + case CE_MenuBarEmptyArea: { + stateId = MBI_NORMAL; + if (!(state & State_Enabled)) + stateId = MBI_DISABLED; + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::MenuTheme, + MENU_BARBACKGROUND, stateId, option->rect); + d->drawBackground(theme); + return; + } + + case CE_ToolBar: + if (const auto *toolbar = qstyleoption_cast<const QStyleOptionToolBar *>(option)) { + QPalette pal = option->palette; + pal.setColor(QPalette::Dark, option->palette.window().color().darker(130)); + QStyleOptionToolBar copyOpt = *toolbar; + copyOpt.palette = pal; + QWindowsStyle::drawControl(element, ©Opt, painter, widget); + } + return; + +#if QT_CONFIG(dockwidget) + case CE_DockWidgetTitle: + if (const auto *dwOpt = qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { + QRect rect = option->rect; + const QDockWidget *dw = qobject_cast<const QDockWidget *>(widget); + bool isFloating = dw && dw->isFloating(); + int buttonMargin = 4; + int mw = proxy()->pixelMetric(QStyle::PM_DockWidgetTitleMargin, dwOpt, widget); + int fw = proxy()->pixelMetric(PM_DockWidgetFrameWidth, dwOpt, widget); + + const bool verticalTitleBar = dwOpt->verticalTitleBar; + + if (verticalTitleBar) { + rect = rect.transposed(); + + painter->translate(rect.left() - 1, rect.top() + rect.width()); + painter->rotate(-90); + painter->translate(-rect.left() + 1, -rect.top()); + } + + QRect r = option->rect.adjusted(0, 2, -1, -3); + QRect titleRect = r; + + if (dwOpt->closable) { + QSize sz = proxy()->standardIcon(QStyle::SP_TitleBarCloseButton, dwOpt, widget).actualSize(QSize(10, 10)); + titleRect.adjust(0, 0, -sz.width() - mw - buttonMargin, 0); + } + + if (dwOpt->floatable) { + QSize sz = proxy()->standardIcon(QStyle::SP_TitleBarMaxButton, dwOpt, widget).actualSize(QSize(10, 10)); + titleRect.adjust(0, 0, -sz.width() - mw - buttonMargin, 0); + } + + if (isFloating) { + titleRect.adjust(0, -fw, 0, 0); + if (widget && widget->windowIcon().cacheKey() != QApplication::windowIcon().cacheKey()) + titleRect.adjust(titleRect.height() + mw, 0, 0, 0); + } else { + titleRect.adjust(mw, 0, 0, 0); + if (!dwOpt->floatable && !dwOpt->closable) + titleRect.adjust(0, 0, -mw, 0); + } + + if (!verticalTitleBar) + titleRect = visualRect(dwOpt->direction, r, titleRect); + + if (isFloating) { + const bool isActive = dwOpt->state & State_Active; + themeNumber = QWindowsVistaStylePrivate::WindowTheme; + if (isActive) + stateId = CS_ACTIVE; + else + stateId = CS_INACTIVE; + + int titleHeight = rect.height() - 2; + rect = rect.adjusted(-fw, -fw, fw, 0); + + QWindowsThemeData theme(widget, painter, themeNumber, 0, stateId); + if (!theme.isValid()) + break; + + // Draw small type title bar + theme.rect = rect; + theme.partId = WP_SMALLCAPTION; + d->drawBackground(theme); + + // Figure out maximal button space on title bar + + QIcon ico = widget->windowIcon(); + bool hasIcon = (ico.cacheKey() != QApplication::windowIcon().cacheKey()); + if (hasIcon) { + QPixmap pxIco = ico.pixmap(titleHeight); + if (!verticalTitleBar && dwOpt->direction == Qt::RightToLeft) + painter->drawPixmap(rect.width() - titleHeight - pxIco.width(), rect.bottom() - titleHeight - 2, pxIco); + else + painter->drawPixmap(fw, rect.bottom() - titleHeight - 2, pxIco); + } + if (!dwOpt->title.isEmpty()) { + QPen oldPen = painter->pen(); + QFont oldFont = painter->font(); + QFont titleFont = oldFont; + titleFont.setBold(true); + painter->setFont(titleFont); + QString titleText + = painter->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width()); + + int result = TST_NONE; + GetThemeEnumValue(theme.handle(), WP_SMALLCAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWTYPE, &result); + if (result != TST_NONE) { + COLORREF textShadowRef; + GetThemeColor(theme.handle(), WP_SMALLCAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWCOLOR, &textShadowRef); + QColor textShadow = qRgb(GetRValue(textShadowRef), GetGValue(textShadowRef), GetBValue(textShadowRef)); + painter->setPen(textShadow); + drawItemText(painter, titleRect.adjusted(1, 1, 1, 1), + Qt::AlignLeft | Qt::AlignBottom | Qt::TextHideMnemonic, dwOpt->palette, + dwOpt->state & State_Enabled, titleText); + } + + COLORREF captionText = GetSysColor(isActive ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT); + QColor textColor = qRgb(GetRValue(captionText), GetGValue(captionText), GetBValue(captionText)); + painter->setPen(textColor); + drawItemText(painter, titleRect, + Qt::AlignLeft | Qt::AlignBottom | Qt::TextHideMnemonic, dwOpt->palette, + dwOpt->state & State_Enabled, titleText); + painter->setFont(oldFont); + painter->setPen(oldPen); + } + } else { + painter->setBrush(option->palette.window().color().darker(110)); + painter->setPen(option->palette.window().color().darker(130)); + painter->drawRect(rect.adjusted(0, 1, -1, -3)); + + if (!dwOpt->title.isEmpty()) { + QString titleText = painter->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, + verticalTitleBar ? titleRect.height() : titleRect.width()); + const int indent = 4; + drawItemText(painter, rect.adjusted(indent + 1, 1, -indent - 1, -1), + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextHideMnemonic, + dwOpt->palette, + dwOpt->state & State_Enabled, titleText, + QPalette::WindowText); + } + } + } + return; +#endif // QT_CONFIG(dockwidget) + +#if QT_CONFIG(rubberband) + case CE_RubberBand: + if (qstyleoption_cast<const QStyleOptionRubberBand *>(option)) { + QColor highlight = option->palette.color(QPalette::Active, QPalette::Highlight); + painter->save(); + painter->setPen(highlight.darker(120)); + QColor dimHighlight(qMin(highlight.red()/2 + 110, 255), + qMin(highlight.green()/2 + 110, 255), + qMin(highlight.blue()/2 + 110, 255), + (widget && widget->isWindow())? 255 : 127); + painter->setBrush(dimHighlight); + painter->drawRect(option->rect.adjusted(0, 0, -1, -1)); + painter->restore(); + return; + } + break; +#endif // QT_CONFIG(rubberband) + + case CE_HeaderEmptyArea: + if (option->state & State_Horizontal) { + themeNumber = QWindowsVistaStylePrivate::HeaderTheme; + stateId = HIS_NORMAL; + } else { + QWindowsStyle::drawControl(CE_HeaderEmptyArea, option, painter, widget); + return; + } + break; + +#if QT_CONFIG(itemviews) + case CE_ItemViewItem: { + const QStyleOptionViewItem *vopt; + const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(widget); + bool newStyle = true; + + if (qobject_cast<const QTableView*>(widget)) + newStyle = false; + + QWindowsThemeData theme(widget, painter, themeNumber, partId, stateId, rect); + + if (newStyle && view && (vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option))) { + /* + // We cannot currently get the correct selection color for "explorer style" views + COLORREF cref = 0; + QWindowsThemeData theme(d->treeViewHelper(), 0, QLatin1String("LISTVIEW"), 0, 0); + unsigned int res = GetThemeColor(theme.handle(), LVP_LISTITEM, LISS_SELECTED, TMT_TEXTCOLOR, &cref); + QColor textColor(GetRValue(cref), GetGValue(cref), GetBValue(cref)); + */ + QPalette palette = vopt->palette; + palette.setColor(QPalette::All, QPalette::HighlightedText, palette.color(QPalette::Active, QPalette::Text)); + // Note that setting a saturated color here results in ugly XOR colors in the focus rect + palette.setColor(QPalette::All, QPalette::Highlight, palette.base().color().darker(108)); + QStyleOptionViewItem adjustedOption = *vopt; + adjustedOption.palette = palette; + // We hide the focusrect in singleselection as it is not required + if ((view->selectionMode() == QAbstractItemView::SingleSelection) + && !(vopt->state & State_KeyboardFocusChange)) + adjustedOption.state &= ~State_HasFocus; + if (!theme.isValid()) { + QWindowsStyle::drawControl(element, &adjustedOption, painter, widget); + return; + } + } else { + if (!theme.isValid()) { + QWindowsStyle::drawControl(element, option, painter, widget); + return; + } + } + + theme.rotate = rotate; + theme.mirrorHorizontally = hMirrored; + theme.mirrorVertically = vMirrored; + d->drawBackground(theme); + return; + } +#endif // QT_CONFIG(itemviews) + +#if QT_CONFIG(combobox) + case CE_ComboBoxLabel: + QCommonStyle::drawControl(element, option, painter, widget); + return; +#endif // QT_CONFIG(combobox) + + default: + break; + } + + QWindowsThemeData theme(widget, painter, themeNumber, partId, stateId, rect); + + if (!theme.isValid()) { + QWindowsStyle::drawControl(element, option, painter, widget); + return; + } + + theme.rotate = rotate; + theme.mirrorHorizontally = hMirrored; + theme.mirrorVertically = vMirrored; + + d->drawBackground(theme); +} + +/*! + \internal + see drawPrimitive for comments on the animation support + + */ +void QWindowsVistaStyle::drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, + QPainter *painter, const QWidget *widget) const +{ + QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); + + if (!QWindowsVistaStylePrivate::useVista()) { + QWindowsStyle::drawComplexControl(control, option, painter, widget); + return; + } + + State state = option->state; + SubControls sub = option->subControls; + QRect r = option->rect; + + int partId = 0; + int stateId = 0; + + State flags = option->state; + if (widget && widget->testAttribute(Qt::WA_UnderMouse) && widget->isActiveWindow()) + flags |= State_MouseOver; + + if (d->transitionsEnabled() && canAnimate(option)) + { + if (control == CC_ScrollBar || control == CC_SpinBox || control == CC_ComboBox) { + QObject *styleObject = option->styleObject; // Can be widget or qquickitem + + int oldState = styleObject->property("_q_stylestate").toInt(); + int oldActiveControls = styleObject->property("_q_stylecontrols").toInt(); + + QRect oldRect = styleObject->property("_q_stylerect").toRect(); + styleObject->setProperty("_q_stylestate", int(option->state)); + styleObject->setProperty("_q_stylecontrols", int(option->activeSubControls)); + styleObject->setProperty("_q_stylerect", option->rect); + + bool doTransition = ((state & State_Sunken) != (oldState & State_Sunken) + || (state & State_On) != (oldState & State_On) + || (state & State_MouseOver) != (oldState & State_MouseOver) + || oldActiveControls != int(option->activeSubControls)); + + if (qstyleoption_cast<const QStyleOptionSlider *>(option)) { + QRect oldSliderPos = styleObject->property("_q_stylesliderpos").toRect(); + QRect currentPos = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); + styleObject->setProperty("_q_stylesliderpos", currentPos); + if (oldSliderPos != currentPos) { + doTransition = false; + d->stopAnimation(styleObject); + } + } else if (control == CC_SpinBox) { + //spinboxes have a transition when focus changes + if (!doTransition) + doTransition = (state & State_HasFocus) != (oldState & State_HasFocus); + } + + if (oldRect != option->rect) { + doTransition = false; + d->stopAnimation(styleObject); + } + + if (doTransition) { + QImage startImage = createAnimationBuffer(option, widget); + QPainter startPainter(&startImage); + + QImage endImage = createAnimationBuffer(option, widget); + QPainter endPainter(&endImage); + + QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); + QWindowsVistaTransition *t = new QWindowsVistaTransition(styleObject); + + // Draw the image that ends the animation by using the current styleoption + QStyleOptionComplex *styleOption = qstyleoption_cast<QStyleOptionComplex*>(clonedAnimationStyleOption(option)); + + styleObject->setProperty("_q_no_animation", true); + + // Draw transition source + if (!anim) { + styleOption->state = QStyle::State(oldState); + styleOption->activeSubControls = QStyle::SubControl(oldActiveControls); + proxy()->drawComplexControl(control, styleOption, &startPainter, widget); + } else { + anim->paint(&startPainter, option); + } + t->setStartImage(startImage); + + // Draw transition target + styleOption->state = option->state; + styleOption->activeSubControls = option->activeSubControls; + proxy()->drawComplexControl(control, styleOption, &endPainter, widget); + + styleObject->setProperty("_q_no_animation", false); + + t->setEndImage(endImage); + t->setStartTime(d->animationTime()); + + if (option->state & State_MouseOver || option->state & State_Sunken) + t->setDuration(150); + else + t->setDuration(500); + + deleteClonedAnimationStyleOption(styleOption); + d->startAnimation(t); + } + if (QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject))) { + anim->paint(painter, option); + return; + } + } + } + + switch (control) { + +#if QT_CONFIG(slider) + case CC_Slider: + if (const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::TrackBarTheme); + QRect slrect = slider->rect; + QRegion tickreg = slrect; + if (sub & SC_SliderGroove) { + theme.rect = proxy()->subControlRect(CC_Slider, option, SC_SliderGroove, widget); + if (slider->orientation == Qt::Horizontal) { + partId = TKP_TRACK; + stateId = TRS_NORMAL; + theme.rect = QRect(slrect.left(), theme.rect.center().y() - 2, slrect.width(), 4); + } else { + partId = TKP_TRACKVERT; + stateId = TRVS_NORMAL; + theme.rect = QRect(theme.rect.center().x() - 2, slrect.top(), 4, slrect.height()); + } + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + tickreg -= theme.rect; + } + if (sub & SC_SliderTickmarks) { + int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget); + int ticks = slider->tickPosition; + int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget); + int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + int interval = slider->tickInterval; + if (interval <= 0) { + interval = slider->singleStep; + if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, + available) + - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + 0, available) < 3) + interval = slider->pageStep; + } + if (!interval) + interval = 1; + int fudge = len / 2; + int pos; + int bothOffset = (ticks & QSlider::TicksAbove && ticks & QSlider::TicksBelow) ? 1 : 0; + painter->setPen(d->sliderTickColor); + QVarLengthArray<QLine, 32> lines; + int v = slider->minimum; + while (v <= slider->maximum + 1) { + if (v == slider->maximum + 1 && interval == 1) + break; + const int v_ = qMin(v, slider->maximum); + int tickLength = (v_ == slider->minimum || v_ >= slider->maximum) ? 4 : 3; + pos = QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, + v_, available) + fudge; + if (slider->orientation == Qt::Horizontal) { + if (ticks & QSlider::TicksAbove) { + lines.append(QLine(pos, tickOffset - 1 - bothOffset, + pos, tickOffset - 1 - bothOffset - tickLength)); + } + + if (ticks & QSlider::TicksBelow) { + lines.append(QLine(pos, tickOffset + thickness + bothOffset, + pos, tickOffset + thickness + bothOffset + tickLength)); + } + } else { + if (ticks & QSlider::TicksAbove) { + lines.append(QLine(tickOffset - 1 - bothOffset, pos, + tickOffset - 1 - bothOffset - tickLength, pos)); + } + + if (ticks & QSlider::TicksBelow) { + lines.append(QLine(tickOffset + thickness + bothOffset, pos, + tickOffset + thickness + bothOffset + tickLength, pos)); + } + } + // in the case where maximum is max int + int nextInterval = v + interval; + if (nextInterval < v) + break; + v = nextInterval; + } + if (!lines.isEmpty()) { + painter->save(); + painter->translate(slrect.topLeft()); + painter->drawLines(lines.constData(), lines.size()); + painter->restore(); + } + } + if (sub & SC_SliderHandle) { + theme.rect = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); + if (slider->orientation == Qt::Horizontal) { + if (slider->tickPosition == QSlider::TicksAbove) + partId = TKP_THUMBTOP; + else if (slider->tickPosition == QSlider::TicksBelow) + partId = TKP_THUMBBOTTOM; + else + partId = TKP_THUMB; + + if (!(slider->state & State_Enabled)) + stateId = TUS_DISABLED; + else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_Sunken)) + stateId = TUS_PRESSED; + else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_MouseOver)) + stateId = TUS_HOT; + else if (flags & State_HasFocus) + stateId = TUS_FOCUSED; + else + stateId = TUS_NORMAL; + } else { + if (slider->tickPosition == QSlider::TicksLeft) + partId = TKP_THUMBLEFT; + else if (slider->tickPosition == QSlider::TicksRight) + partId = TKP_THUMBRIGHT; + else + partId = TKP_THUMBVERT; + + if (!(slider->state & State_Enabled)) + stateId = TUVS_DISABLED; + else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_Sunken)) + stateId = TUVS_PRESSED; + else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_MouseOver)) + stateId = TUVS_HOT; + else if (flags & State_HasFocus) + stateId = TUVS_FOCUSED; + else + stateId = TUVS_NORMAL; + } + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + if (slider->state & State_HasFocus) { + QStyleOptionFocusRect fropt; + fropt.QStyleOption::operator=(*slider); + fropt.rect = subElementRect(SE_SliderFocusRect, slider, widget); + proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); + } + } + break; +#endif + +#if QT_CONFIG(toolbutton) + case CC_ToolButton: + if (const auto *toolbutton = qstyleoption_cast<const QStyleOptionToolButton *>(option)) { + QRect button, menuarea; + button = proxy()->subControlRect(control, toolbutton, SC_ToolButton, widget); + menuarea = proxy()->subControlRect(control, toolbutton, SC_ToolButtonMenu, widget); + + State bflags = toolbutton->state & ~State_Sunken; + State mflags = bflags; + bool autoRaise = flags & State_AutoRaise; + if (autoRaise) { + if (!(bflags & State_MouseOver) || !(bflags & State_Enabled)) + bflags &= ~State_Raised; + } + + if (toolbutton->state & State_Sunken) { + if (toolbutton->activeSubControls & SC_ToolButton) { + bflags |= State_Sunken; + mflags |= State_MouseOver | State_Sunken; + } else if (toolbutton->activeSubControls & SC_ToolButtonMenu) { + mflags |= State_Sunken; + bflags |= State_MouseOver; + } + } + + QStyleOption tool = *toolbutton; + if (toolbutton->subControls & SC_ToolButton) { + if (flags & (State_Sunken | State_On | State_Raised) || !autoRaise) { + if (toolbutton->features & QStyleOptionToolButton::MenuButtonPopup && autoRaise) { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::ToolBarTheme); + theme.partId = TP_SPLITBUTTON; + theme.rect = button; + if (!(bflags & State_Enabled)) + stateId = TS_DISABLED; + else if (bflags & State_Sunken) + stateId = TS_PRESSED; + else if (bflags & State_MouseOver || !(flags & State_AutoRaise)) + stateId = flags & State_On ? TS_HOTCHECKED : TS_HOT; + else if (bflags & State_On) + stateId = TS_CHECKED; + else + stateId = TS_NORMAL; + if (option->direction == Qt::RightToLeft) + theme.mirrorHorizontally = true; + theme.stateId = stateId; + d->drawBackground(theme); + } else { + tool.rect = option->rect; + tool.state = bflags; + if (autoRaise) // for tool bars + proxy()->drawPrimitive(PE_PanelButtonTool, &tool, painter, widget); + else + proxy()->drawPrimitive(PE_PanelButtonBevel, &tool, painter, widget); + } + } + } + + if (toolbutton->state & State_HasFocus) { + QStyleOptionFocusRect fr; + fr.QStyleOption::operator=(*toolbutton); + fr.rect.adjust(3, 3, -3, -3); + if (toolbutton->features & QStyleOptionToolButton::MenuButtonPopup) + fr.rect.adjust(0, 0, -proxy()->pixelMetric(QStyle::PM_MenuButtonIndicator, + toolbutton, widget), 0); + proxy()->drawPrimitive(PE_FrameFocusRect, &fr, painter, widget); + } + QStyleOptionToolButton label = *toolbutton; + label.state = bflags; + int fw = 2; + if (!autoRaise) + label.state &= ~State_Sunken; + label.rect = button.adjusted(fw, fw, -fw, -fw); + proxy()->drawControl(CE_ToolButtonLabel, &label, painter, widget); + + if (toolbutton->subControls & SC_ToolButtonMenu) { + tool.rect = menuarea; + tool.state = mflags; + if (autoRaise) { + proxy()->drawPrimitive(PE_IndicatorButtonDropDown, &tool, painter, widget); + } else { + tool.state = mflags; + menuarea.adjust(-2, 0, 0, 0); + // Draw menu button + if ((bflags & State_Sunken) != (mflags & State_Sunken)){ + painter->save(); + painter->setClipRect(menuarea); + tool.rect = option->rect; + proxy()->drawPrimitive(PE_PanelButtonBevel, &tool, painter, nullptr); + painter->restore(); + } + // Draw arrow + painter->save(); + painter->setPen(option->palette.dark().color()); + painter->drawLine(menuarea.left(), menuarea.top() + 3, + menuarea.left(), menuarea.bottom() - 3); + painter->setPen(option->palette.light().color()); + painter->drawLine(menuarea.left() - 1, menuarea.top() + 3, + menuarea.left() - 1, menuarea.bottom() - 3); + + tool.rect = menuarea.adjusted(2, 3, -2, -1); + proxy()->drawPrimitive(PE_IndicatorArrowDown, &tool, painter, widget); + painter->restore(); + } + } else if (toolbutton->features & QStyleOptionToolButton::HasMenu) { + int mbi = proxy()->pixelMetric(PM_MenuButtonIndicator, toolbutton, widget); + QRect ir = toolbutton->rect; + QStyleOptionToolButton newBtn = *toolbutton; + newBtn.rect = QRect(ir.right() + 4 - mbi, ir.height() - mbi + 4, mbi - 5, mbi - 5); + proxy()->drawPrimitive(PE_IndicatorArrowDown, &newBtn, painter, widget); + } + } + break; +#endif // QT_CONFIG(toolbutton) + + case CC_TitleBar: + if (const auto *tb = qstyleoption_cast<const QStyleOptionTitleBar *>(option)) { + const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); + bool isActive = tb->titleBarState & QStyle::State_Active; + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::WindowTheme); + if (sub & SC_TitleBarLabel) { + partId = (tb->titleBarState & Qt::WindowMinimized) ? WP_MINCAPTION : WP_CAPTION; + theme.rect = option->rect; + if (widget && !widget->isEnabled()) + stateId = CS_DISABLED; + else if (isActive) + stateId = CS_ACTIVE; + else + stateId = CS_INACTIVE; + + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + + QRect ir = proxy()->subControlRect(CC_TitleBar, tb, SC_TitleBarLabel, widget); + + int result = TST_NONE; + GetThemeEnumValue(theme.handle(), WP_CAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWTYPE, &result); + if (result != TST_NONE) { + COLORREF textShadowRef; + GetThemeColor(theme.handle(), WP_CAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWCOLOR, &textShadowRef); + QColor textShadow = qRgb(GetRValue(textShadowRef), GetGValue(textShadowRef), GetBValue(textShadowRef)); + painter->setPen(textShadow); + painter->drawText(int(ir.x() + 3 * factor), int(ir.y() + 2 * factor), + int(ir.width() - 1 * factor), ir.height(), + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, tb->text); + } + COLORREF captionText = GetSysColor(isActive ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT); + QColor textColor = qRgb(GetRValue(captionText), GetGValue(captionText), GetBValue(captionText)); + painter->setPen(textColor); + painter->drawText(int(ir.x() + 2 * factor), int(ir.y() + 1 * factor), + int(ir.width() - 2 * factor), ir.height(), + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, tb->text); + } + if (sub & SC_TitleBarSysMenu && tb->titleBarFlags & Qt::WindowSystemMenuHint) { + theme.rect = proxy()->subControlRect(CC_TitleBar, option, SC_TitleBarSysMenu, widget); + partId = WP_SYSBUTTON; + if ((widget && !widget->isEnabled()) || !isActive) + stateId = SBS_DISABLED; + else if (option->activeSubControls == SC_TitleBarSysMenu && (option->state & State_Sunken)) + stateId = SBS_PUSHED; + else if (option->activeSubControls == SC_TitleBarSysMenu && (option->state & State_MouseOver)) + stateId = SBS_HOT; + else + stateId = SBS_NORMAL; + if (!tb->icon.isNull()) { + tb->icon.paint(painter, theme.rect); + } else { + theme.partId = partId; + theme.stateId = stateId; + if (theme.size().isEmpty()) { + int iconSize = proxy()->pixelMetric(PM_SmallIconSize, tb, widget); + QPixmap pm = proxy()->standardIcon(SP_TitleBarMenuButton, tb, widget).pixmap(iconSize, iconSize); + painter->save(); + drawItemPixmap(painter, theme.rect, Qt::AlignCenter, pm); + painter->restore(); + } else { + d->drawBackground(theme); + } + } + } + + if (sub & SC_TitleBarMinButton && tb->titleBarFlags & Qt::WindowMinimizeButtonHint + && !(tb->titleBarState & Qt::WindowMinimized)) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarMinButton, isActive, WP_MINBUTTON, &theme); + d->drawBackground(theme); + } + if (sub & SC_TitleBarMaxButton && tb->titleBarFlags & Qt::WindowMaximizeButtonHint + && !(tb->titleBarState & Qt::WindowMaximized)) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarMaxButton, isActive, WP_MAXBUTTON, &theme); + d->drawBackground(theme); + } + if (sub & SC_TitleBarContextHelpButton + && tb->titleBarFlags & Qt::WindowContextHelpButtonHint) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarContextHelpButton, isActive, WP_HELPBUTTON, &theme); + d->drawBackground(theme); + } + bool drawNormalButton = (sub & SC_TitleBarNormalButton) + && (((tb->titleBarFlags & Qt::WindowMinimizeButtonHint) + && (tb->titleBarState & Qt::WindowMinimized)) + || ((tb->titleBarFlags & Qt::WindowMaximizeButtonHint) + && (tb->titleBarState & Qt::WindowMaximized))); + if (drawNormalButton) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarNormalButton, isActive, WP_RESTOREBUTTON, &theme); + d->drawBackground(theme); + } + if (sub & SC_TitleBarShadeButton && tb->titleBarFlags & Qt::WindowShadeButtonHint + && !(tb->titleBarState & Qt::WindowMinimized)) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarShadeButton, isActive, WP_MINBUTTON, &theme); + d->drawBackground(theme); + } + if (sub & SC_TitleBarUnshadeButton && tb->titleBarFlags & Qt::WindowShadeButtonHint + && tb->titleBarState & Qt::WindowMinimized) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarUnshadeButton, isActive, WP_RESTOREBUTTON, &theme); + d->drawBackground(theme); + } + if (sub & SC_TitleBarCloseButton && tb->titleBarFlags & Qt::WindowSystemMenuHint) { + populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarCloseButton, isActive, WP_CLOSEBUTTON, &theme); + d->drawBackground(theme); + } + } + break; + +#if QT_CONFIG(mdiarea) + case CC_MdiControls: { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::WindowTheme, WP_MDICLOSEBUTTON, CBS_NORMAL); + if (Q_UNLIKELY(!theme.isValid())) + return; + + if (option->subControls.testFlag(SC_MdiCloseButton)) { + populateMdiButtonTheme(proxy(), widget, option, SC_MdiCloseButton, WP_MDICLOSEBUTTON, &theme); + d->drawBackground(theme, mdiButtonCorrectionFactor(theme, widget)); + } + if (option->subControls.testFlag(SC_MdiNormalButton)) { + populateMdiButtonTheme(proxy(), widget, option, SC_MdiNormalButton, WP_MDIRESTOREBUTTON, &theme); + d->drawBackground(theme, mdiButtonCorrectionFactor(theme, widget)); + } + if (option->subControls.testFlag(QStyle::SC_MdiMinButton)) { + populateMdiButtonTheme(proxy(), widget, option, SC_MdiMinButton, WP_MDIMINBUTTON, &theme); + d->drawBackground(theme, mdiButtonCorrectionFactor(theme, widget)); + } + break; + } +#endif // QT_CONFIG(mdiarea) + +#if QT_CONFIG(dial) + case CC_Dial: + if (const auto *dial = qstyleoption_cast<const QStyleOptionSlider *>(option)) + QStyleHelper::drawDial(dial, painter); + break; +#endif // QT_CONFIG(dial) + + case CC_ComboBox: + if (const QStyleOptionComboBox *cmb = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { + if (cmb->editable) { + if (sub & SC_ComboBoxEditField) { + partId = EP_EDITBORDER_NOSCROLL; + if (!(flags & State_Enabled)) + stateId = ETS_DISABLED; + else if (flags & State_MouseOver) + stateId = ETS_HOT; + else if (flags & State_HasFocus) + stateId = ETS_FOCUSED; + else + stateId = ETS_NORMAL; + + QWindowsThemeData theme(widget, painter, + QWindowsVistaStylePrivate::EditTheme, + partId, stateId, r); + + d->drawBackground(theme); + } + if (sub & SC_ComboBoxArrow) { + QRect subRect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxArrow, widget); + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::ComboboxTheme); + theme.rect = subRect; + partId = option->direction == Qt::RightToLeft ? CP_DROPDOWNBUTTONLEFT : CP_DROPDOWNBUTTONRIGHT; + + if (!(cmb->state & State_Enabled)) + stateId = CBXS_DISABLED; + else if (cmb->state & State_Sunken || cmb->state & State_On) + stateId = CBXS_PRESSED; + else if (cmb->state & State_MouseOver && option->activeSubControls & SC_ComboBoxArrow) + stateId = CBXS_HOT; + else + stateId = CBXS_NORMAL; + + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + + } else { + if (sub & SC_ComboBoxFrame) { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::ComboboxTheme); + theme.rect = option->rect; + theme.partId = CP_READONLY; + if (!(cmb->state & State_Enabled)) + theme.stateId = CBXS_DISABLED; + else if (cmb->state & State_Sunken || cmb->state & State_On) + theme.stateId = CBXS_PRESSED; + else if (cmb->state & State_MouseOver) + theme.stateId = CBXS_HOT; + else + theme.stateId = CBXS_NORMAL; + d->drawBackground(theme); + } + if (sub & SC_ComboBoxArrow) { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::ComboboxTheme); + theme.rect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxArrow, widget); + theme.partId = option->direction == Qt::RightToLeft ? CP_DROPDOWNBUTTONLEFT : CP_DROPDOWNBUTTONRIGHT; + if (!(cmb->state & State_Enabled)) + theme.stateId = CBXS_DISABLED; + else + theme.stateId = CBXS_NORMAL; + d->drawBackground(theme); + } + if ((sub & SC_ComboBoxEditField) && (flags & State_HasFocus)) { + QStyleOptionFocusRect fropt; + fropt.QStyleOption::operator=(*cmb); + fropt.rect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxEditField, widget); + proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); + } + } + } + break; + + case CC_ScrollBar: + if (const QStyleOptionSlider *scrollbar = qstyleoption_cast<const QStyleOptionSlider *>(option)) { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::ScrollBarTheme); + bool maxedOut = (scrollbar->maximum == scrollbar->minimum); + if (maxedOut) + flags &= ~State_Enabled; + + bool isHorz = flags & State_Horizontal; + bool isRTL = option->direction == Qt::RightToLeft; + if (sub & SC_ScrollBarAddLine) { + theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddLine, widget); + partId = SBP_ARROWBTN; + if (!(flags & State_Enabled)) + stateId = (isHorz ? (isRTL ? ABS_LEFTDISABLED : ABS_RIGHTDISABLED) : ABS_DOWNDISABLED); + else if (scrollbar->activeSubControls & SC_ScrollBarAddLine && (scrollbar->state & State_Sunken)) + stateId = (isHorz ? (isRTL ? ABS_LEFTPRESSED : ABS_RIGHTPRESSED) : ABS_DOWNPRESSED); + else if (scrollbar->activeSubControls & SC_ScrollBarAddLine && (scrollbar->state & State_MouseOver)) + stateId = (isHorz ? (isRTL ? ABS_LEFTHOT : ABS_RIGHTHOT) : ABS_DOWNHOT); + else if (scrollbar->state & State_MouseOver) + stateId = (isHorz ? (isRTL ? ABS_LEFTHOVER : ABS_RIGHTHOVER) : ABS_DOWNHOVER); + else + stateId = (isHorz ? (isRTL ? ABS_LEFTNORMAL : ABS_RIGHTNORMAL) : ABS_DOWNNORMAL); + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + if (sub & SC_ScrollBarSubLine) { + theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubLine, widget); + partId = SBP_ARROWBTN; + if (!(flags & State_Enabled)) + stateId = (isHorz ? (isRTL ? ABS_RIGHTDISABLED : ABS_LEFTDISABLED) : ABS_UPDISABLED); + else if (scrollbar->activeSubControls & SC_ScrollBarSubLine && (scrollbar->state & State_Sunken)) + stateId = (isHorz ? (isRTL ? ABS_RIGHTPRESSED : ABS_LEFTPRESSED) : ABS_UPPRESSED); + else if (scrollbar->activeSubControls & SC_ScrollBarSubLine && (scrollbar->state & State_MouseOver)) + stateId = (isHorz ? (isRTL ? ABS_RIGHTHOT : ABS_LEFTHOT) : ABS_UPHOT); + else if (scrollbar->state & State_MouseOver) + stateId = (isHorz ? (isRTL ? ABS_RIGHTHOVER : ABS_LEFTHOVER) : ABS_UPHOVER); + else + stateId = (isHorz ? (isRTL ? ABS_RIGHTNORMAL : ABS_LEFTNORMAL) : ABS_UPNORMAL); + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + if (maxedOut) { + theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); + theme.rect = theme.rect.united(proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubPage, widget)); + theme.rect = theme.rect.united(proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddPage, widget)); + partId = flags & State_Horizontal ? SBP_LOWERTRACKHORZ : SBP_LOWERTRACKVERT; + stateId = SCRBS_DISABLED; + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } else { + if (sub & SC_ScrollBarSubPage) { + theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubPage, widget); + partId = flags & State_Horizontal ? SBP_UPPERTRACKHORZ : SBP_UPPERTRACKVERT; + if (!(flags & State_Enabled)) + stateId = SCRBS_DISABLED; + else if (scrollbar->activeSubControls & SC_ScrollBarSubPage && (scrollbar->state & State_Sunken)) + stateId = SCRBS_PRESSED; + else if (scrollbar->activeSubControls & SC_ScrollBarSubPage && (scrollbar->state & State_MouseOver)) + stateId = SCRBS_HOT; + else + stateId = SCRBS_NORMAL; + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + if (sub & SC_ScrollBarAddPage) { + theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddPage, widget); + partId = flags & State_Horizontal ? SBP_LOWERTRACKHORZ : SBP_LOWERTRACKVERT; + if (!(flags & State_Enabled)) + stateId = SCRBS_DISABLED; + else if (scrollbar->activeSubControls & SC_ScrollBarAddPage && (scrollbar->state & State_Sunken)) + stateId = SCRBS_PRESSED; + else if (scrollbar->activeSubControls & SC_ScrollBarAddPage && (scrollbar->state & State_MouseOver)) + stateId = SCRBS_HOT; + else + stateId = SCRBS_NORMAL; + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + if (sub & SC_ScrollBarSlider) { + theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); + if (!(flags & State_Enabled)) + stateId = SCRBS_DISABLED; + else if (scrollbar->activeSubControls & SC_ScrollBarSlider && (scrollbar->state & State_Sunken)) + stateId = SCRBS_PRESSED; + else if (scrollbar->activeSubControls & SC_ScrollBarSlider && (scrollbar->state & State_MouseOver)) + stateId = SCRBS_HOT; + else if (option->state & State_MouseOver) + stateId = SCRBS_HOVER; + else + stateId = SCRBS_NORMAL; + + // Draw handle + theme.partId = flags & State_Horizontal ? SBP_THUMBBTNHORZ : SBP_THUMBBTNVERT; + theme.stateId = stateId; + d->drawBackground(theme); + } + } + } + break; + +#if QT_CONFIG(spinbox) + case CC_SpinBox: + if (const QStyleOptionSpinBox *sb = qstyleoption_cast<const QStyleOptionSpinBox *>(option)) { + QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::SpinTheme); + if (sb->frame && (sub & SC_SpinBoxFrame)) { + partId = EP_EDITBORDER_NOSCROLL; + if (!(flags & State_Enabled)) + stateId = ETS_DISABLED; + else if (flags & State_MouseOver) + stateId = ETS_HOT; + else if (flags & State_HasFocus) + stateId = ETS_SELECTED; + else + stateId = ETS_NORMAL; + + QWindowsThemeData ftheme(widget, painter, + QWindowsVistaStylePrivate::EditTheme, + partId, stateId, r); + // The spinbox in Windows QStyle is drawn with frameless QLineEdit inside it + // That however breaks with QtQuickControls where this results in transparent + // spinbox background, so if there's no "widget" passed (QtQuickControls case), + // let ftheme.noContent be false, which fixes the spinbox rendering in QQC + ftheme.noContent = (widget != nullptr); + d->drawBackground(ftheme); + } + if (sub & SC_SpinBoxUp) { + theme.rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxUp, widget).adjusted(0, 0, 0, 1); + partId = SPNP_UP; + if (!(sb->stepEnabled & QAbstractSpinBox::StepUpEnabled) || !(flags & State_Enabled)) + stateId = UPS_DISABLED; + else if (sb->activeSubControls == SC_SpinBoxUp && (sb->state & State_Sunken)) + stateId = UPS_PRESSED; + else if (sb->activeSubControls == SC_SpinBoxUp && (sb->state & State_MouseOver)) + stateId = UPS_HOT; + else + stateId = UPS_NORMAL; + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + if (sub & SC_SpinBoxDown) { + theme.rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxDown, widget); + partId = SPNP_DOWN; + if (!(sb->stepEnabled & QAbstractSpinBox::StepDownEnabled) || !(flags & State_Enabled)) + stateId = DNS_DISABLED; + else if (sb->activeSubControls == SC_SpinBoxDown && (sb->state & State_Sunken)) + stateId = DNS_PRESSED; + else if (sb->activeSubControls == SC_SpinBoxDown && (sb->state & State_MouseOver)) + stateId = DNS_HOT; + else + stateId = DNS_NORMAL; + theme.partId = partId; + theme.stateId = stateId; + d->drawBackground(theme); + } + } + break; +#endif // QT_CONFIG(spinbox) + + default: + QWindowsStyle::drawComplexControl(control, option, painter, widget); + break; + } +} + +/*! + \internal + */ +QSize QWindowsVistaStyle::sizeFromContents(ContentsType type, const QStyleOption *option, + const QSize &size, const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) + return QWindowsStyle::sizeFromContents(type, option, size, widget); + + QSize contentSize(size); + + switch (type) { + case CT_LineEdit: + case CT_ComboBox: { + QWindowsThemeData buttontheme(widget, nullptr, QWindowsVistaStylePrivate::ButtonTheme, BP_PUSHBUTTON, PBS_NORMAL); + if (buttontheme.isValid()) { + const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); + const QMarginsF borderSize = buttontheme.margins() * factor; + if (!borderSize.isNull()) { + const qreal margin = qreal(2) * factor; + contentSize.rwidth() += qRound(borderSize.left() + borderSize.right() - margin); + contentSize.rheight() += int(borderSize.bottom() + borderSize.top() - margin + + qreal(1) / factor - 1); + } + const int textMargins = 2*(proxy()->pixelMetric(PM_FocusFrameHMargin, option) + 1); + contentSize += QSize(qMax(pixelMetric(QStyle::PM_ScrollBarExtent, option, widget) + + textMargins, 23), 0); //arrow button + } + break; + } + + case CT_TabWidget: + contentSize += QSize(6, 6); + break; + + case CT_Menu: + contentSize += QSize(1, 0); + break; + +#if QT_CONFIG(menubar) + case CT_MenuBarItem: + if (!contentSize.isEmpty()) + contentSize += QSize(windowsItemHMargin * 5 + 1, 5); + break; +#endif + + case CT_MenuItem: { + contentSize = QWindowsStyle::sizeFromContents(type, option, size, widget); + QWindowsThemeData theme(widget, nullptr, + QWindowsVistaStylePrivate::MenuTheme, + MENU_POPUPCHECKBACKGROUND, MBI_HOT); + QWindowsThemeData themeSize = theme; + themeSize.partId = MENU_POPUPCHECK; + themeSize.stateId = 0; + const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + const QMarginsF margins = themeSize.margins() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + int minimumHeight = qMax(qRound(size.height() + margins.bottom() + margins.top()), contentSize.height()); + contentSize.rwidth() += qRound(size.width() + margins.left() + margins.right()); + if (const QStyleOptionMenuItem *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { + if (menuitem->menuItemType != QStyleOptionMenuItem::Separator) + contentSize.setHeight(minimumHeight); + } + break; + } + + case CT_MdiControls: { + contentSize.setHeight(int(QStyleHelper::dpiScaled(19, option))); + int width = 54; + if (const auto *styleOpt = qstyleoption_cast<const QStyleOptionComplex *>(option)) { + width = 0; + if (styleOpt->subControls & SC_MdiMinButton) + width += 17 + 1; + if (styleOpt->subControls & SC_MdiNormalButton) + width += 17 + 1; + if (styleOpt->subControls & SC_MdiCloseButton) + width += 17 + 1; + } + contentSize.setWidth(int(QStyleHelper::dpiScaled(width, option))); + break; + } + + case CT_ItemViewItem: + contentSize = QWindowsStyle::sizeFromContents(type, option, size, widget); + contentSize.rheight() += 2; + break; + + case CT_SpinBox: { + //Spinbox adds frame twice + contentSize = QWindowsStyle::sizeFromContents(type, option, size, widget); + int border = proxy()->pixelMetric(PM_SpinBoxFrameWidth, option, widget); + contentSize -= QSize(2*border, 2*border); + break; + } + + case CT_HeaderSection: + // When there is a sort indicator it adds to the width but it is shown + // above the text natively and not on the side + if (QStyleOptionHeader *hdr = qstyleoption_cast<QStyleOptionHeader *>(const_cast<QStyleOption *>(option))) { + QStyleOptionHeader::SortIndicator sortInd = hdr->sortIndicator; + hdr->sortIndicator = QStyleOptionHeader::None; + contentSize = QWindowsStyle::sizeFromContents(type, hdr, size, widget); + hdr->sortIndicator = sortInd; + } + break; + + default: + contentSize = QWindowsStyle::sizeFromContents(type, option, size, widget); + break; + } + + return contentSize; +} + +/*! + \internal + */ +QRect QWindowsVistaStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) + return QWindowsStyle::subElementRect(element, option, widget); + + QRect rect(option->rect); + + switch (element) { + case SE_PushButtonContents: + if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { + MARGINS borderSize; + const HTHEME theme = OpenThemeData(widget ? QWindowsVistaStylePrivate::winId(widget) : nullptr, L"Button"); + if (theme) { + int stateId = PBS_NORMAL; + if (!(option->state & State_Enabled)) + stateId = PBS_DISABLED; + else if (option->state & State_Sunken) + stateId = PBS_PRESSED; + else if (option->state & State_MouseOver) + stateId = PBS_HOT; + else if (btn->features & QStyleOptionButton::DefaultButton) + stateId = PBS_DEFAULTED; + + int border = proxy()->pixelMetric(PM_DefaultFrameWidth, btn, widget); + rect = option->rect.adjusted(border, border, -border, -border); + + if (SUCCEEDED(GetThemeMargins(theme, nullptr, BP_PUSHBUTTON, stateId, TMT_CONTENTMARGINS, nullptr, &borderSize))) { + rect.adjust(borderSize.cxLeftWidth, borderSize.cyTopHeight, + -borderSize.cxRightWidth, -borderSize.cyBottomHeight); + rect = visualRect(option->direction, option->rect, rect); + } + } + } + break; + + case SE_DockWidgetCloseButton: + case SE_DockWidgetFloatButton: + rect = QWindowsStyle::subElementRect(element, option, widget); + return rect.translated(0, 1); + + case SE_TabWidgetTabContents: + rect = QWindowsStyle::subElementRect(element, option, widget); + if (qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option)) { + rect = QWindowsStyle::subElementRect(element, option, widget); + if (const QTabWidget *tabWidget = qobject_cast<const QTabWidget *>(widget)) { + if (tabWidget->documentMode()) + break; + rect.adjust(0, 0, -2, -2); + } + } + break; + + case SE_TabWidgetTabBar: { + rect = QWindowsStyle::subElementRect(element, option, widget); + const auto *twfOption = qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option); + if (twfOption && twfOption->direction == Qt::RightToLeft + && (twfOption->shape == QTabBar::RoundedNorth + || twfOption->shape == QTabBar::RoundedSouth)) + { + QStyleOptionTab otherOption; + otherOption.shape = (twfOption->shape == QTabBar::RoundedNorth + ? QTabBar::RoundedEast : QTabBar::RoundedSouth); + int overlap = proxy()->pixelMetric(PM_TabBarBaseOverlap, &otherOption, widget); + int borderThickness = proxy()->pixelMetric(PM_DefaultFrameWidth, option, widget); + rect.adjust(-overlap + borderThickness, 0, -overlap + borderThickness, 0); + } + break; + } + + case SE_HeaderArrow: { + rect = QWindowsStyle::subElementRect(element, option, widget); + QRect r = rect; + int h = option->rect.height(); + int w = option->rect.width(); + int x = option->rect.x(); + int y = option->rect.y(); + int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget); + + QWindowsThemeData theme(widget, nullptr, + QWindowsVistaStylePrivate::HeaderTheme, + HP_HEADERSORTARROW, HSAS_SORTEDDOWN, option->rect); + + int arrowWidth = 13; + int arrowHeight = 5; + if (theme.isValid()) { + const QSizeF size = theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); + if (!size.isEmpty()) { + arrowWidth = qRound(size.width()); + arrowHeight = qRound(size.height()); + } + } + if (option->state & State_Horizontal) { + r.setRect(x + w/2 - arrowWidth/2, y , arrowWidth, arrowHeight); + } else { + int vert_size = w / 2; + r.setRect(x + 5, y + h - margin * 2 - vert_size, + w - margin * 2 - 5, vert_size); + } + rect = visualRect(option->direction, option->rect, r); + break; + } + + case SE_HeaderLabel: { + rect = QWindowsStyle::subElementRect(element, option, widget); + int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget); + QRect r = option->rect; + r.setRect(option->rect.x() + margin, option->rect.y() + margin, + option->rect.width() - margin * 2, option->rect.height() - margin * 2); + if (const QStyleOptionHeader *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { + // Subtract width needed for arrow, if there is one + if (header->sortIndicator != QStyleOptionHeader::None) { + if (!(option->state & State_Horizontal)) //horizontal arrows are positioned on top + r.setHeight(r.height() - (option->rect.width() / 2) - (margin * 2)); + } + } + rect = visualRect(option->direction, option->rect, r); + break; + } + + case SE_ProgressBarContents: + rect = QCommonStyle::subElementRect(SE_ProgressBarGroove, option, widget); + break; + + case SE_ItemViewItemDecoration: + rect = QWindowsStyle::subElementRect(element, option, widget); + if (qstyleoption_cast<const QStyleOptionViewItem *>(option)) + rect.adjust(-2, 0, 2, 0); + break; + + case SE_ItemViewItemFocusRect: + rect = QWindowsStyle::subElementRect(element, option, widget); + if (const QStyleOptionViewItem *vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { + QRect textRect = subElementRect(QStyle::SE_ItemViewItemText, option, widget); + QRect displayRect = subElementRect(QStyle::SE_ItemViewItemDecoration, option, widget); + if (!vopt->icon.isNull()) + rect = textRect.united(displayRect); + else + rect = textRect; + rect = rect.adjusted(1, 0, -1, 0); + } + break; + + default: + rect = QWindowsStyle::subElementRect(element, option, widget); + break; + } + + return rect; +} + +/*! + \internal + */ +QRect QWindowsVistaStyle::subControlRect(ComplexControl control, const QStyleOptionComplex *option, + SubControl subControl, const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) + return QWindowsStyle::subControlRect(control, option, subControl, widget); + + QRect rect; + + switch (control) { +#if QT_CONFIG(combobox) + case CC_ComboBox: + if (const auto *cb = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { + const int x = cb->rect.x(), y = cb->rect.y(), wi = cb->rect.width(), he = cb->rect.height(); + const int margin = cb->frame ? 3 : 0; + const int bmarg = cb->frame ? 2 : 0; + const int arrowWidth = qRound(QStyleHelper::dpiScaled(16, option)); + const int arrowButtonWidth = bmarg + arrowWidth; + const int xpos = x + wi - arrowButtonWidth; + + switch (subControl) { + case SC_ComboBoxFrame: + case SC_ComboBoxListBoxPopup: + rect = cb->rect; + break; + + case SC_ComboBoxArrow: { + rect.setRect(xpos, y , arrowButtonWidth, he); + } + break; + + case SC_ComboBoxEditField: { + rect.setRect(x + margin, y + margin, wi - 2 * margin - arrowWidth, he - 2 * margin); + } + break; + + default: + break; + } + } + break; +#endif // QT_CONFIG(combobox) + + case CC_TitleBar: + if (const QStyleOptionTitleBar *tb = qstyleoption_cast<const QStyleOptionTitleBar *>(option)) { + if (!buttonVisible(subControl, tb)) + return rect; + const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); + const bool isToolTitle = false; + const int height = tb->rect.height(); + const int width = tb->rect.width(); + + const int buttonMargin = int(QStyleHelper::dpiScaled(4, option)); + int buttonHeight = qRound(qreal(GetSystemMetrics(SM_CYSIZE)) * factor) + - buttonMargin; + const int buttonWidth = + qRound(qreal(GetSystemMetrics(SM_CXSIZE)) * factor - QStyleHelper::dpiScaled(4, option)); + + const int frameWidth = proxy()->pixelMetric(PM_MdiSubWindowFrameWidth, option, widget); + const bool sysmenuHint = (tb->titleBarFlags & Qt::WindowSystemMenuHint) != 0; + const bool minimizeHint = (tb->titleBarFlags & Qt::WindowMinimizeButtonHint) != 0; + const bool maximizeHint = (tb->titleBarFlags & Qt::WindowMaximizeButtonHint) != 0; + const bool contextHint = (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) != 0; + const bool shadeHint = (tb->titleBarFlags & Qt::WindowShadeButtonHint) != 0; + + bool isMinimized = tb->titleBarState & Qt::WindowMinimized; + bool isMaximized = tb->titleBarState & Qt::WindowMaximized; + int offset = 0; + const int delta = buttonWidth + 2; + int controlTop = option->rect.bottom() - buttonHeight - 2; + + switch (subControl) { + case SC_TitleBarLabel: { + rect = QRect(frameWidth, 0, width - (buttonWidth + frameWidth + 10), height); + if (isToolTitle) { + if (sysmenuHint) { + rect.adjust(0, 0, int(-buttonWidth - 3 * factor), 0); + } + if (minimizeHint || maximizeHint) + rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); + } else { + if (sysmenuHint) { + const int leftOffset = int(height - 8 * factor); + rect.adjust(leftOffset, 0, 0, int(4 * factor)); + } + if (minimizeHint) + rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); + if (maximizeHint) + rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); + if (contextHint) + rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); + if (shadeHint) + rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); + } + rect.translate(0, int(2 * factor)); + } + break; + + case SC_TitleBarContextHelpButton: + if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) + offset += delta; + Q_FALLTHROUGH(); + case SC_TitleBarMinButton: + if (!isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += delta; + else if (subControl == SC_TitleBarMinButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarNormalButton: + if (isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += delta; + else if (isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += delta; + else if (subControl == SC_TitleBarNormalButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarMaxButton: + if (!isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += delta; + else if (subControl == SC_TitleBarMaxButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarShadeButton: + if (!isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += delta; + else if (subControl == SC_TitleBarShadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarUnshadeButton: + if (isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += delta; + else if (subControl == SC_TitleBarUnshadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarCloseButton: + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) + offset += delta; + else if (subControl == SC_TitleBarCloseButton) + break; + + rect.setRect(width - offset - controlTop + 1, controlTop, + buttonWidth, buttonHeight); + break; + + case SC_TitleBarSysMenu: { + const int controlTop = int(6 * factor); + const int controlHeight = int(height - controlTop - 3 * factor); + int iconExtent = proxy()->pixelMetric(PM_SmallIconSize, option); + QSize iconSize = tb->icon.actualSize(QSize(iconExtent, iconExtent)); + if (tb->icon.isNull()) + iconSize = QSize(controlHeight, controlHeight); + int hPad = (controlHeight - iconSize.height())/2; + int vPad = (controlHeight - iconSize.width())/2; + rect = QRect(frameWidth + hPad, controlTop + vPad, iconSize.width(), iconSize.height()); + rect.translate(0, int(3 * factor)); + } + break; + + default: + break; + } + } + break; + +#if QT_CONFIG(mdiarea) + case CC_MdiControls: { + int numSubControls = 0; + if (option->subControls & SC_MdiCloseButton) + ++numSubControls; + if (option->subControls & SC_MdiMinButton) + ++numSubControls; + if (option->subControls & SC_MdiNormalButton) + ++numSubControls; + if (numSubControls == 0) + break; + + int buttonWidth = option->rect.width() / numSubControls; + int offset = 0; + + switch (subControl) { + case SC_MdiCloseButton: + // Only one sub control, no offset needed. + if (numSubControls == 1) + break; + offset += buttonWidth; + Q_FALLTHROUGH(); + case SC_MdiNormalButton: + // No offset needed if + // 1) There's only one sub control + // 2) We have a close button and a normal button (offset already added in SC_MdiClose) + if (numSubControls == 1 || (numSubControls == 2 && !(option->subControls & SC_MdiMinButton))) + break; + if (option->subControls & SC_MdiNormalButton) + offset += buttonWidth; + break; + default: + break; + } + + rect = QRect(offset, 0, buttonWidth, option->rect.height()); + break; + } +#endif // QT_CONFIG(mdiarea) + + default: + return QWindowsStyle::subControlRect(control, option, subControl, widget); + } + + return visualRect(option->direction, option->rect, rect); +} + +/*! + \internal + */ +QStyle::SubControl QWindowsVistaStyle::hitTestComplexControl(ComplexControl control, const QStyleOptionComplex *option, + const QPoint &pos, const QWidget *widget) const +{ + return QWindowsStyle::hitTestComplexControl(control, option, pos, widget); +} + +/*! + \internal + */ +int QWindowsVistaStyle::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) + return QWindowsStyle::pixelMetric(metric, option, widget); + + int ret = QWindowsVistaStylePrivate::fixedPixelMetric(metric); + if (ret != QWindowsStylePrivate::InvalidMetric) + return int(QStyleHelper::dpiScaled(ret, option)); + + int res = QWindowsVistaStylePrivate::pixelMetricFromSystemDp(metric, option, widget); + if (res != QWindowsStylePrivate::InvalidMetric) + return qRound(qreal(res) * QWindowsStylePrivate::nativeMetricScaleFactor(widget)); + + res = 0; + + switch (metric) { + case PM_MenuBarPanelWidth: + case PM_ButtonDefaultIndicator: + res = 0; + break; + + case PM_DefaultFrameWidth: + res = qobject_cast<const QListView*>(widget) ? 2 : 1; + break; + case PM_MenuPanelWidth: + case PM_SpinBoxFrameWidth: + res = 1; + break; + + case PM_TabBarTabOverlap: + case PM_MenuHMargin: + case PM_MenuVMargin: + res = 2; + break; + + case PM_TabBarBaseOverlap: + if (const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) { + switch (tab->shape) { + case QTabBar::RoundedNorth: + case QTabBar::TriangularNorth: + case QTabBar::RoundedWest: + case QTabBar::TriangularWest: + res = 1; + break; + case QTabBar::RoundedSouth: + case QTabBar::TriangularSouth: + res = 2; + break; + case QTabBar::RoundedEast: + case QTabBar::TriangularEast: + res = 3; + break; + } + } + break; + + case PM_SplitterWidth: + res = QStyleHelper::dpiScaled(5., option); + break; + + case PM_MdiSubWindowMinimizedWidth: + res = 160; + break; + +#if QT_CONFIG(toolbar) + case PM_ToolBarHandleExtent: + res = int(QStyleHelper::dpiScaled(8., option)); + break; + +#endif // QT_CONFIG(toolbar) + case PM_DockWidgetSeparatorExtent: + case PM_DockWidgetTitleMargin: + res = int(QStyleHelper::dpiScaled(4., option)); + break; + + case PM_ButtonShiftHorizontal: + case PM_ButtonShiftVertical: + res = qstyleoption_cast<const QStyleOptionToolButton *>(option) ? 1 : 0; + break; + + default: + res = QWindowsStyle::pixelMetric(metric, option, widget); + } + + return res; +} + +/*! + \internal + */ +void QWindowsVistaStyle::polish(QWidget *widget) +{ + QWindowsStyle::polish(widget); + if (!QWindowsVistaStylePrivate::useVista()) + return; + + if (false +#if QT_CONFIG(abstractbutton) + || qobject_cast<QAbstractButton*>(widget) +#endif // QT_CONFIG(abstractbutton) + || qobject_cast<QToolButton*>(widget) + || qobject_cast<QTabBar*>(widget) +#if QT_CONFIG(combobox) + || qobject_cast<QComboBox*>(widget) +#endif // QT_CONFIG(combobox) + || qobject_cast<QScrollBar*>(widget) + || qobject_cast<QSlider*>(widget) + || qobject_cast<QHeaderView*>(widget) +#if QT_CONFIG(spinbox) + || qobject_cast<QAbstractSpinBox*>(widget) + || qobject_cast<QSpinBox*>(widget) +#endif // QT_CONFIG(spinbox) + ) { + widget->setAttribute(Qt::WA_Hover); + } + +#if QT_CONFIG(rubberband) + if (qobject_cast<QRubberBand*>(widget)) + widget->setWindowOpacity(0.6); +#endif + +#if QT_CONFIG(lineedit) + if (qobject_cast<QLineEdit*>(widget)) + widget->setAttribute(Qt::WA_Hover); + else +#endif // QT_CONFIG(lineedit) + if (qobject_cast<QGroupBox*>(widget)) + widget->setAttribute(Qt::WA_Hover); +#if QT_CONFIG(commandlinkbutton) + else if (qobject_cast<QCommandLinkButton*>(widget)) { + widget->setProperty("_qt_usingVistaStyle", true); + QFont buttonFont = widget->font(); + buttonFont.setFamilies(QStringList{QLatin1String("Segoe UI")}); + widget->setFont(buttonFont); + QPalette pal = widget->palette(); + pal.setColor(QPalette::Active, QPalette::ButtonText, QColor(21, 28, 85)); + pal.setColor(QPalette::Active, QPalette::BrightText, QColor(7, 64, 229)); + widget->setPalette(pal); + } +#endif // QT_CONFIG(commandlinkbutton) + else if (widget->inherits("QTipLabel")) { + //note that since tooltips are not reused + //we do not have to care about unpolishing + widget->setContentsMargins(3, 0, 4, 0); + COLORREF bgRef; + HTHEME theme = OpenThemeData(widget ? QWindowsVistaStylePrivate::winId(widget) : nullptr, L"TOOLTIP"); + if (theme && SUCCEEDED(GetThemeColor(theme, TTP_STANDARD, TTSS_NORMAL, TMT_TEXTCOLOR, &bgRef))) { + QColor textColor = QColor::fromRgb(bgRef); + QPalette pal; + pal.setColor(QPalette::All, QPalette::ToolTipText, textColor); + pal.setResolveMask(0); + widget->setPalette(pal); + } + } else if (qobject_cast<QMessageBox *> (widget)) { + widget->setAttribute(Qt::WA_StyledBackground); +#if QT_CONFIG(dialogbuttonbox) + QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); + if (buttonBox) + buttonBox->setContentsMargins(0, 9, 0, 0); +#endif + } +#if QT_CONFIG(inputdialog) + else if (qobject_cast<QInputDialog *> (widget)) { + widget->setAttribute(Qt::WA_StyledBackground); +#if QT_CONFIG(dialogbuttonbox) + QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); + if (buttonBox) + buttonBox->setContentsMargins(0, 9, 0, 0); +#endif + } +#endif // QT_CONFIG(inputdialog) + else if (QTreeView *tree = qobject_cast<QTreeView *> (widget)) { + tree->viewport()->setAttribute(Qt::WA_Hover); + } + else if (QListView *list = qobject_cast<QListView *> (widget)) { + list->viewport()->setAttribute(Qt::WA_Hover); + } +} + +/*! + \internal + */ +void QWindowsVistaStyle::unpolish(QWidget *widget) +{ + Q_D(QWindowsVistaStyle); + +#if QT_CONFIG(rubberband) + if (qobject_cast<QRubberBand*>(widget)) + widget->setWindowOpacity(1.0); +#endif + + // Unpolish of widgets is the first thing that + // happens when a theme changes, or the theme + // engine is turned off. So we detect it here. + bool oldState = QWindowsVistaStylePrivate::useVista(); + bool newState = QWindowsVistaStylePrivate::useVista(true); + if ((oldState != newState) && newState) { + d->cleanup(true); + d->init(true); + } else { + // Cleanup handle map, if just changing style, + // or turning it on. In both cases the values + // already in the map might be old (other style). + d->cleanupHandleMap(); + } + if (false + #if QT_CONFIG(abstractbutton) + || qobject_cast<QAbstractButton*>(widget) + #endif + || qobject_cast<QToolButton*>(widget) + || qobject_cast<QTabBar*>(widget) + #if QT_CONFIG(combobox) + || qobject_cast<QComboBox*>(widget) + #endif // QT_CONFIG(combobox) + || qobject_cast<QScrollBar*>(widget) + || qobject_cast<QSlider*>(widget) + || qobject_cast<QHeaderView*>(widget) + #if QT_CONFIG(spinbox) + || qobject_cast<QAbstractSpinBox*>(widget) + || qobject_cast<QSpinBox*>(widget) + #endif // QT_CONFIG(spinbox) + ) { + widget->setAttribute(Qt::WA_Hover, false); + } + + QWindowsStyle::unpolish(widget); + + d->stopAnimation(widget); + +#if QT_CONFIG(lineedit) + if (qobject_cast<QLineEdit*>(widget)) + widget->setAttribute(Qt::WA_Hover, false); + else { +#endif // QT_CONFIG(lineedit) + if (qobject_cast<QGroupBox*>(widget)) + widget->setAttribute(Qt::WA_Hover, false); + else if (qobject_cast<QMessageBox *> (widget)) { + widget->setAttribute(Qt::WA_StyledBackground, false); +#if QT_CONFIG(dialogbuttonbox) + QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); + if (buttonBox) + buttonBox->setContentsMargins(0, 0, 0, 0); +#endif + } +#if QT_CONFIG(inputdialog) + else if (qobject_cast<QInputDialog *> (widget)) { + widget->setAttribute(Qt::WA_StyledBackground, false); +#if QT_CONFIG(dialogbuttonbox) + QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); + if (buttonBox) + buttonBox->setContentsMargins(0, 0, 0, 0); +#endif + } +#endif // QT_CONFIG(inputdialog) + else if (QTreeView *tree = qobject_cast<QTreeView *> (widget)) { + tree->viewport()->setAttribute(Qt::WA_Hover, false); + } +#if QT_CONFIG(commandlinkbutton) + else if (qobject_cast<QCommandLinkButton*>(widget)) { + QFont font = QApplication::font("QCommandLinkButton"); + QFont widgetFont = widget->font(); + widgetFont.setFamilies(font.families()); //Only family set by polish + widget->setFont(widgetFont); + } +#endif // QT_CONFIG(commandlinkbutton) + } +} + +/*! + \internal + */ +void QWindowsVistaStyle::polish(QPalette &pal) +{ + Q_D(QWindowsVistaStyle); + + if (QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark) { + // System runs in dark mode, but the Vista style cannot use a dark palette. + // Overwrite with the light system palette. + using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; + if (auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration())) + nativeWindowsApp->populateLightSystemPalette(pal); + } + + QPixmapCache::clear(); + d->alphaCache.clear(); + d->hasInitColors = false; + + if (!d->hasInitColors) { + // Get text color for group box labels + QWindowsThemeData theme(nullptr, nullptr, QWindowsVistaStylePrivate::ButtonTheme, 0, 0); + COLORREF cref; + GetThemeColor(theme.handle(), BP_GROUPBOX, GBS_NORMAL, TMT_TEXTCOLOR, &cref); + d->groupBoxTextColor = qRgb(GetRValue(cref), GetGValue(cref), GetBValue(cref)); + GetThemeColor(theme.handle(), BP_GROUPBOX, GBS_DISABLED, TMT_TEXTCOLOR, &cref); + d->groupBoxTextColorDisabled = qRgb(GetRValue(cref), GetGValue(cref), GetBValue(cref)); + // Where does this color come from? + //GetThemeColor(theme.handle(), TKP_TICS, TSS_NORMAL, TMT_COLOR, &cref); + d->sliderTickColor = qRgb(165, 162, 148); + d->hasInitColors = true; + } + + QWindowsStyle::polish(pal); + pal.setBrush(QPalette::AlternateBase, pal.base().color().darker(104)); +} + +/*! + \internal + */ +void QWindowsVistaStyle::polish(QApplication *app) +{ + // Override windows theme palettes to light + if (qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark) { + static const char* themedWidgets[] = { + "QToolButton", + "QAbstractButton", + "QCheckBox", + "QRadioButton", + "QHeaderView", + "QAbstractItemView", + "QMessageBoxLabel", + "QTabBar", + "QLabel", + "QGroupBox", + "QMenu", + "QMenuBar", + "QTextEdit", + "QTextControl", + "QLineEdit" + }; + for (const auto& themedWidget : std::as_const(themedWidgets)) { + auto defaultResolveMask = QApplication::palette().resolveMask(); + auto widgetResolveMask = QApplication::palette(themedWidget).resolveMask(); + if (widgetResolveMask != defaultResolveMask) + QApplication::setPalette(QApplication::palette(), themedWidget); + } + } + + QWindowsStyle::polish(app); +} + +/*! + \internal + */ +QPixmap QWindowsVistaStyle::standardPixmap(StandardPixmap standardPixmap, const QStyleOption *option, + const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) { + return QWindowsStyle::standardPixmap(standardPixmap, option, widget); + } + + switch (standardPixmap) { + case SP_TitleBarMaxButton: + case SP_TitleBarCloseButton: + if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { + if (widget && widget->isWindow()) { + QWindowsThemeData theme(widget, nullptr, QWindowsVistaStylePrivate::WindowTheme, WP_SMALLCLOSEBUTTON, CBS_NORMAL); + if (theme.isValid()) { + const QSize size = (theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); + return QIcon(QWindowsStyle::standardPixmap(standardPixmap, option, widget)).pixmap(size); + } + } + } + break; + + default: + break; + } + + return QWindowsStyle::standardPixmap(standardPixmap, option, widget); +} + +/*! +\reimp +*/ +QIcon QWindowsVistaStyle::standardIcon(StandardPixmap standardIcon, + const QStyleOption *option, + const QWidget *widget) const +{ + if (!QWindowsVistaStylePrivate::useVista()) { + return QWindowsStyle::standardIcon(standardIcon, option, widget); + } + + auto *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); + + switch (standardIcon) { + case SP_TitleBarMaxButton: + if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { + if (d->dockFloat.isNull()) { + QWindowsThemeData themeSize(nullptr, nullptr, QWindowsVistaStylePrivate::WindowTheme, + WP_SMALLCLOSEBUTTON, CBS_NORMAL); + QWindowsThemeData theme(nullptr, nullptr, QWindowsVistaStylePrivate::WindowTheme, + WP_MAXBUTTON, MAXBS_NORMAL); + if (theme.isValid()) { + const QSize size = (themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); + QPixmap pm(size); + pm.fill(Qt::transparent); + QPainter p(&pm); + theme.painter = &p; + theme.rect = QRect(QPoint(0, 0), size); + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal + pm.fill(Qt::transparent); + theme.stateId = MAXBS_PUSHED; + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed + pm.fill(Qt::transparent); + theme.stateId = MAXBS_HOT; + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover + pm.fill(Qt::transparent); + theme.stateId = MAXBS_INACTIVE; + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled + } + } + if (widget && widget->isWindow()) + return d->dockFloat; + } + break; + + case SP_TitleBarCloseButton: + if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { + if (d->dockClose.isNull()) { + QWindowsThemeData theme(nullptr, nullptr, QWindowsVistaStylePrivate::WindowTheme, + WP_SMALLCLOSEBUTTON, CBS_NORMAL); + if (theme.isValid()) { + const QSize size = (theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); + QPixmap pm(size); + pm.fill(Qt::transparent); + QPainter p(&pm); + theme.painter = &p; + theme.partId = WP_CLOSEBUTTON; // #### + theme.rect = QRect(QPoint(0, 0), size); + d->drawBackground(theme); + d->dockClose.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal + pm.fill(Qt::transparent); + theme.stateId = CBS_PUSHED; + d->drawBackground(theme); + d->dockClose.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed + pm.fill(Qt::transparent); + theme.stateId = CBS_HOT; + d->drawBackground(theme); + d->dockClose.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover + pm.fill(Qt::transparent); + theme.stateId = CBS_INACTIVE; + d->drawBackground(theme); + d->dockClose.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled + } + } + if (widget && widget->isWindow()) + return d->dockClose; + } + break; + + case SP_TitleBarNormalButton: + if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { + if (d->dockFloat.isNull()) { + QWindowsThemeData themeSize(nullptr, nullptr, QWindowsVistaStylePrivate::WindowTheme, + WP_SMALLCLOSEBUTTON, CBS_NORMAL); + QWindowsThemeData theme(nullptr, nullptr, QWindowsVistaStylePrivate::WindowTheme, + WP_RESTOREBUTTON, RBS_NORMAL); + if (theme.isValid()) { + const QSize size = (themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); + QPixmap pm(size); + pm.fill(Qt::transparent); + QPainter p(&pm); + theme.painter = &p; + theme.rect = QRect(QPoint(0, 0), size); + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal + pm.fill(Qt::transparent); + theme.stateId = RBS_PUSHED; + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed + pm.fill(Qt::transparent); + theme.stateId = RBS_HOT; + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover + pm.fill(Qt::transparent); + theme.stateId = RBS_INACTIVE; + d->drawBackground(theme); + d->dockFloat.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled + } + } + if (widget && widget->isWindow()) + return d->dockFloat; + } + break; + + case SP_CommandLink: { + QWindowsThemeData theme(nullptr, nullptr, QWindowsVistaStylePrivate::ButtonTheme, + BP_COMMANDLINKGLYPH, CMDLGS_NORMAL); + if (theme.isValid()) { + const QSize size = theme.size().toSize(); + QIcon linkGlyph; + QPixmap pm(size); + pm.fill(Qt::transparent); + QPainter p(&pm); + theme.painter = &p; + theme.rect = QRect(QPoint(0, 0), size); + d->drawBackground(theme); + linkGlyph.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal + pm.fill(Qt::transparent); + + theme.stateId = CMDLGS_PRESSED; + d->drawBackground(theme); + linkGlyph.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed + pm.fill(Qt::transparent); + + theme.stateId = CMDLGS_HOT; + d->drawBackground(theme); + linkGlyph.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover + pm.fill(Qt::transparent); + + theme.stateId = CMDLGS_DISABLED; + d->drawBackground(theme); + linkGlyph.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled + return linkGlyph; + } + break; + } + + default: + break; + } + + return QWindowsStyle::standardIcon(standardIcon, option, widget); +} + +QT_END_NAMESPACE diff --git a/src/plugins/styles/windowsvista/qwindowsvistastyle_p.h b/src/plugins/styles/modernwindows/qwindowsvistastyle_p.h index 9d31d5dfee..ff5300beca 100644 --- a/src/plugins/styles/windowsvista/qwindowsvistastyle_p.h +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle_p.h @@ -16,12 +16,12 @@ // #include <QtWidgets/private/qtwidgetsglobal_p.h> -#include "qwindowsxpstyle_p.h" +#include <QtWidgets/private/qwindowsstyle_p.h> QT_BEGIN_NAMESPACE class QWindowsVistaStylePrivate; -class QWindowsVistaStyle : public QWindowsXPStyle +class QWindowsVistaStyle : public QWindowsStyle { Q_OBJECT public: @@ -58,7 +58,9 @@ public: void polish(QWidget *widget) override; void unpolish(QWidget *widget) override; void polish(QPalette &pal) override; - + void polish(QApplication *app) override; +protected: + QWindowsVistaStyle(QWindowsVistaStylePrivate &dd); private: Q_DISABLE_COPY_MOVE(QWindowsVistaStyle) Q_DECLARE_PRIVATE(QWindowsVistaStyle) diff --git a/src/plugins/styles/modernwindows/qwindowsvistastyle_p_p.h b/src/plugins/styles/modernwindows/qwindowsvistastyle_p_p.h new file mode 100644 index 0000000000..053e98b68d --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle_p_p.h @@ -0,0 +1,176 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSVISTASTYLE_P_P_H +#define QWINDOWSVISTASTYLE_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of qapplication_*.cpp, qwidget*.cpp and qfiledialog.cpp. This header +// file may change from version to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtWidgets/private/qtwidgetsglobal_p.h> +#include "qwindowsvistastyle_p.h" +#include "qwindowsthemedata_p.h" +#include <private/qpaintengine_raster_p.h> +#include <qpaintengine.h> +#include <qwidget.h> +#include <qapplication.h> +#include <qpixmapcache.h> +#include <qstyleoption.h> +#include <QtWidgets/private/qwindowsstyle_p_p.h> +#include <qmap.h> + +#if QT_CONFIG(pushbutton) +#include <qpushbutton.h> +#endif +#include <qradiobutton.h> +#if QT_CONFIG(lineedit) +#include <qlineedit.h> +#endif +#include <qgroupbox.h> +#if QT_CONFIG(toolbutton) +#include <qtoolbutton.h> +#endif +#if QT_CONFIG(spinbox) +#include <qspinbox.h> +#endif +#if QT_CONFIG(toolbar) +#include <qtoolbar.h> +#endif +#if QT_CONFIG(combobox) +#include <qcombobox.h> +#endif +#if QT_CONFIG(scrollbar) +#include <qscrollbar.h> +#endif +#if QT_CONFIG(progressbar) +#include <qprogressbar.h> +#endif +#if QT_CONFIG(dockwidget) +#include <qdockwidget.h> +#endif +#if QT_CONFIG(listview) +#include <qlistview.h> +#endif +#if QT_CONFIG(treeview) +#include <qtreeview.h> +#endif +#include <qtextedit.h> +#include <qmessagebox.h> +#if QT_CONFIG(dialogbuttonbox) +#include <qdialogbuttonbox.h> +#endif +#include <qinputdialog.h> +#if QT_CONFIG(tableview) +#include <qtableview.h> +#endif +#include <qdatetime.h> +#if QT_CONFIG(commandlinkbutton) +#include <qcommandlinkbutton.h> +#endif +#include <qlabel.h> +#include <qheaderview.h> +#include <uxtheme.h> + +QT_BEGIN_NAMESPACE + +class QWindowsVistaStylePrivate : public QWindowsStylePrivate +{ + Q_DECLARE_PUBLIC(QWindowsVistaStyle) + +public: + enum Theme { + ButtonTheme, + ComboboxTheme, + EditTheme, + HeaderTheme, + ListViewTheme, + MenuTheme, + ProgressTheme, + RebarTheme, + ScrollBarTheme, + SpinTheme, + TabTheme, + TaskDialogTheme, + ToolBarTheme, + ToolTipTheme, + TrackBarTheme, + WindowTheme, + StatusTheme, + VistaTreeViewTheme, // arrow shape treeview indicators (Vista) obtained from "explorer" theme. + NThemes + }; + + QWindowsVistaStylePrivate() + { init(); } + + ~QWindowsVistaStylePrivate() + { cleanup(); } + + static HTHEME createTheme(int theme, HWND hwnd); + static QString themeName(int theme); + static bool isItemViewDelegateLineEdit(const QWidget *widget); + static int pixelMetricFromSystemDp(QStyle::PixelMetric pm, const QStyleOption *option = nullptr, const QWidget *widget = nullptr); + static int fixedPixelMetric(QStyle::PixelMetric pm); + static bool isLineEditBaseColorSet(const QStyleOption *option, const QWidget *widget); + static HWND winId(const QWidget *widget); + static bool useVista(bool update = false); + static QBackingStore *backingStoreForWidget(const QWidget *widget); + static HDC hdcForWidgetBackingStore(const QWidget *widget); + + void init(bool force = false); + void cleanup(bool force = false); + void cleanupHandleMap(); + + HBITMAP buffer(int w = 0, int h = 0); + HDC bufferHDC() + { return bufferDC; } + + bool isTransparent(QWindowsThemeData &QWindowsThemeData); + QRegion region(QWindowsThemeData &QWindowsThemeData); + + bool drawBackground(QWindowsThemeData &QWindowsThemeData, qreal correctionFactor = 1); + bool drawBackgroundThruNativeBuffer(QWindowsThemeData &QWindowsThemeData, qreal aditionalDevicePixelRatio, qreal correctionFactor); + bool drawBackgroundDirectly(HDC dc, QWindowsThemeData &QWindowsThemeData, qreal aditionalDevicePixelRatio); + + bool hasAlphaChannel(const QRect &rect); + bool fixAlphaChannel(const QRect &rect); + bool swapAlphaChannel(const QRect &rect, bool allPixels = false); + + QRgb groupBoxTextColor = 0; + QRgb groupBoxTextColorDisabled = 0; + QRgb sliderTickColor = 0; + bool hasInitColors = false; + QIcon dockFloat, dockClose; + + QTime animationTime() const; + bool transitionsEnabled() const; + +private: + static bool initVistaTreeViewTheming(); + static void cleanupVistaTreeViewTheming(); + + static QBasicAtomicInt ref; + static bool useVistaTheme; + + QHash<ThemeMapKey, ThemeMapData> alphaCache; + HDC bufferDC = nullptr; + HBITMAP bufferBitmap = nullptr; + HBITMAP nullBitmap = nullptr; + uchar *bufferPixels = nullptr; + int bufferW = 0; + int bufferH = 0; + + static HWND m_vistaTreeViewHelper; +}; + +QT_END_NAMESPACE + +#endif // QWINDOWSVISTASTYLE_P_P_H diff --git a/src/plugins/styles/windowsvista/main.cpp b/src/plugins/styles/windowsvista/main.cpp deleted file mode 100644 index af832be0a8..0000000000 --- a/src/plugins/styles/windowsvista/main.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) 2017 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include <QtWidgets/private/qtwidgetsglobal_p.h> -#include <QtWidgets/qstyleplugin.h> -#include "qwindowsvistastyle_p.h" - -QT_BEGIN_NAMESPACE - -class QWindowsVistaStylePlugin : public QStylePlugin -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QStyleFactoryInterface" FILE "windowsvistastyle.json") -public: - QStyle *create(const QString &key) override; -}; - -QStyle *QWindowsVistaStylePlugin::create(const QString &key) -{ - if (key.compare(QLatin1String("windowsvista"), Qt::CaseInsensitive) == 0) - return new QWindowsVistaStyle(); - - return nullptr; -} - -QT_END_NAMESPACE - -#include "main.moc" diff --git a/src/plugins/styles/windowsvista/qwindowsvistastyle.cpp b/src/plugins/styles/windowsvista/qwindowsvistastyle.cpp deleted file mode 100644 index e15a19f9c1..0000000000 --- a/src/plugins/styles/windowsvista/qwindowsvistastyle.cpp +++ /dev/null @@ -1,2443 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qwindowsvistastyle_p.h" -#include "qwindowsvistastyle_p_p.h" -#include <qoperatingsystemversion.h> -#include <qscreen.h> -#include <qwindow.h> -#include <private/qstyleanimation_p.h> -#include <private/qstylehelper_p.h> -#include <qpa/qplatformnativeinterface.h> - -QT_BEGIN_NAMESPACE - -static const int windowsItemFrame = 2; // menu item frame width -static const int windowsItemHMargin = 3; // menu item hor text margin -static const int windowsItemVMargin = 4; // menu item ver text margin -static const int windowsArrowHMargin = 6; // arrow horizontal margin -static const int windowsRightBorder = 15; // right border on windows - -#ifndef TMT_CONTENTMARGINS -# define TMT_CONTENTMARGINS 3602 -#endif -#ifndef TMT_SIZINGMARGINS -# define TMT_SIZINGMARGINS 3601 -#endif -#ifndef LISS_NORMAL -# define LISS_NORMAL 1 -# define LISS_HOT 2 -# define LISS_SELECTED 3 -# define LISS_DISABLED 4 -# define LISS_SELECTEDNOTFOCUS 5 -# define LISS_HOTSELECTED 6 -#endif -#ifndef BP_COMMANDLINK -# define BP_COMMANDLINK 6 -# define BP_COMMANDLINKGLYPH 7 -# define CMDLGS_NORMAL 1 -# define CMDLGS_HOT 2 -# define CMDLGS_PRESSED 3 -# define CMDLGS_DISABLED 4 -#endif - -/* \internal - Checks if we should use Vista style , or if we should - fall back to Windows style. -*/ -bool QWindowsVistaStylePrivate::useVista() -{ - return QWindowsVistaStylePrivate::useXP(); -} - -/*! - \internal - - Animations are started at a frame that is based on the current time, - which makes it impossible to run baseline tests with this style. Allow - overriding through a dynamic property. -*/ -QTime QWindowsVistaStylePrivate::animationTime() const -{ - Q_Q(const QWindowsVistaStyle); - static bool animationTimeOverride = q->dynamicPropertyNames().contains("_qt_animation_time"); - if (animationTimeOverride) - return q->property("_qt_animation_time").toTime(); - return QTime::currentTime(); -} - -/* \internal - Checks and returns the style object -*/ -inline QObject *styleObject(const QStyleOption *option) { - return option ? option->styleObject : nullptr; -} - -/* \internal - Checks if we can animate on a style option -*/ -bool canAnimate(const QStyleOption *option) { - return option - && option->styleObject - && !option->styleObject->property("_q_no_animation").toBool(); -} - -static inline QImage createAnimationBuffer(const QStyleOption *option, const QWidget *widget) -{ - const qreal devicePixelRatio = widget - ? widget->devicePixelRatioF() : qApp->devicePixelRatio(); - QImage result(option->rect.size() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(devicePixelRatio); - result.fill(0); - return result; -} - -/* \internal - Used by animations to clone a styleoption and shift its offset -*/ -QStyleOption *clonedAnimationStyleOption(const QStyleOption*option) { - QStyleOption *styleOption = nullptr; - if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider*>(option)) - styleOption = new QStyleOptionSlider(*slider); - else if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox*>(option)) - styleOption = new QStyleOptionSpinBox(*spinbox); - else if (const QStyleOptionGroupBox *groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option)) - styleOption = new QStyleOptionGroupBox(*groupBox); - else if (const QStyleOptionComboBox *combo = qstyleoption_cast<const QStyleOptionComboBox*>(option)) - styleOption = new QStyleOptionComboBox(*combo); - else if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton*>(option)) - styleOption = new QStyleOptionButton(*button); - else - styleOption = new QStyleOption(*option); - styleOption->rect = QRect(QPoint(0,0), option->rect.size()); - return styleOption; -} - -/* \internal - Used by animations to delete cloned styleoption -*/ -void deleteClonedAnimationStyleOption(const QStyleOption *option) -{ - if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider*>(option)) - delete slider; - else if (const QStyleOptionSpinBox *spinbox = qstyleoption_cast<const QStyleOptionSpinBox*>(option)) - delete spinbox; - else if (const QStyleOptionGroupBox *groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option)) - delete groupBox; - else if (const QStyleOptionComboBox *combo = qstyleoption_cast<const QStyleOptionComboBox*>(option)) - delete combo; - else if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton*>(option)) - delete button; - else - delete option; -} - -/*! - \class QWindowsVistaStyle - \brief The QWindowsVistaStyle class provides a look and feel suitable for applications on Microsoft Windows Vista. - \since 4.3 - \ingroup appearance - \inmodule QtWidgets - \internal - - \warning This style is only available on the Windows Vista platform - because it makes use of Windows Vista's style engine. - - \sa QMacStyle, QWindowsXPStyle, QFusionStyle -*/ - -/*! - Constructs a QWindowsVistaStyle object. -*/ -QWindowsVistaStyle::QWindowsVistaStyle() - : QWindowsXPStyle(*new QWindowsVistaStylePrivate) -{ -} - -/*! - Destructor. -*/ -QWindowsVistaStyle::~QWindowsVistaStyle() = default; - -//convert Qt state flags to uxtheme button states -static int buttonStateId(int flags, int partId) -{ - int stateId = 0; - if (partId == BP_RADIOBUTTON || partId == BP_CHECKBOX) { - if (!(flags & QStyle::State_Enabled)) - stateId = RBS_UNCHECKEDDISABLED; - else if (flags & QStyle::State_Sunken) - stateId = RBS_UNCHECKEDPRESSED; - else if (flags & QStyle::State_MouseOver) - stateId = RBS_UNCHECKEDHOT; - else - stateId = RBS_UNCHECKEDNORMAL; - - if (flags & QStyle::State_On) - stateId += RBS_CHECKEDNORMAL-1; - - } else if (partId == BP_PUSHBUTTON) { - if (!(flags & QStyle::State_Enabled)) - stateId = PBS_DISABLED; - else if (flags & (QStyle::State_Sunken | QStyle::State_On)) - stateId = PBS_PRESSED; - else if (flags & QStyle::State_MouseOver) - stateId = PBS_HOT; - else - stateId = PBS_NORMAL; - } else { - Q_ASSERT(1); - } - return stateId; -} - -bool QWindowsVistaAnimation::isUpdateNeeded() const -{ - return QWindowsVistaStylePrivate::useVista(); -} - -void QWindowsVistaAnimation::paint(QPainter *painter, const QStyleOption *option) -{ - painter->drawImage(option->rect, currentImage()); -} - -static inline bool supportsStateTransition(QStyle::PrimitiveElement element, - const QStyleOption *option, - const QWidget *widget) -{ - bool result = false; - switch (element) { - case QStyle::PE_IndicatorRadioButton: - case QStyle::PE_IndicatorCheckBox: - result = true; - break; - // QTBUG-40634, do not animate when color is set in palette for PE_PanelLineEdit. - case QStyle::PE_FrameLineEdit: - result = !QWindowsXPStylePrivate::isLineEditBaseColorSet(option, widget); - break; - default: - break; - } - return result; -} - -/*! - \internal - - Animations are used for some state transitions on specific widgets. - - Only one running animation can exist for a widget at any specific - time. Animations can be added through - QWindowsVistaStylePrivate::startAnimation(Animation *) and any - existing animation on a widget can be retrieved with - QWindowsVistaStylePrivate::widgetAnimation(Widget *). - - Once an animation has been started, - QWindowsVistaStylePrivate::timerEvent(QTimerEvent *) will - continuously call update() on the widget until it is stopped, - meaning that drawPrimitive will be called many times until the - transition has completed. During this time, the result will be - retrieved by the Animation::paint(...) function and not by the style - itself. - - To determine if a transition should occur, the style needs to know - the previous state of the widget as well as the current one. This is - solved by updating dynamic properties on the widget every time the - function is called. - - Transitions interrupting existing transitions should always be - smooth, so whenever a hover-transition is started on a pulsating - button, it uses the current frame of the pulse-animation as the - starting image for the hover transition. - - */ - -void QWindowsVistaStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option, - QPainter *painter, const QWidget *widget) const -{ - QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); - - int state = option->state; - if (!QWindowsVistaStylePrivate::useVista()) { - QWindowsStyle::drawPrimitive(element, option, painter, widget); - return; - } - - if ((option->state & State_Enabled) && d->transitionsEnabled() && canAnimate(option)) { - { - QRect oldRect; - QRect newRect; - - if (supportsStateTransition(element, option, widget)) { - // Retrieve and update the dynamic properties tracking - // the previous state of the widget: - QObject *styleObject = option->styleObject; - styleObject->setProperty("_q_no_animation", true); - - int oldState = styleObject->property("_q_stylestate").toInt(); - oldRect = styleObject->property("_q_stylerect").toRect(); - newRect = option->rect; - styleObject->setProperty("_q_stylestate", int(option->state)); - styleObject->setProperty("_q_stylerect", option->rect); - - bool doTransition = oldState && - ((state & State_Sunken) != (oldState & State_Sunken) || - (state & State_On) != (oldState & State_On) || - (state & State_MouseOver) != (oldState & State_MouseOver)); - - if (oldRect != newRect || - (state & State_Enabled) != (oldState & State_Enabled) || - (state & State_Active) != (oldState & State_Active)) - d->stopAnimation(styleObject); - - if (option->state & State_ReadOnly && element == PE_FrameLineEdit) // Do not animate read only line edits - doTransition = false; - - if (doTransition) { - QStyleOption *styleOption = clonedAnimationStyleOption(option); - styleOption->state = QStyle::State(oldState); - - QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); - QWindowsVistaTransition *t = new QWindowsVistaTransition(styleObject); - - // We create separate images for the initial and final transition states and store them in the - // Transition object. - QImage startImage = createAnimationBuffer(option, widget); - QPainter startPainter(&startImage); - - QImage endImage = createAnimationBuffer(option, widget); - QPainter endPainter(&endImage); - - // If we have a running animation on the widget already, we will use that to paint the initial - // state of the new transition, this ensures a smooth transition from a current animation such as a - // pulsating default button into the intended target state. - if (!anim) - proxy()->drawPrimitive(element, styleOption, &startPainter, widget); - else - anim->paint(&startPainter, styleOption); - - t->setStartImage(startImage); - - // The end state of the transition is simply the result we would have painted - // if the style was not animated. - styleOption->styleObject = nullptr; - styleOption->state = option->state; - proxy()->drawPrimitive(element, styleOption, &endPainter, widget); - - - t->setEndImage(endImage); - - HTHEME theme; - int partId; - DWORD duration; - int fromState = 0; - int toState = 0; - - //translate state flags to UXTHEME states : - if (element == PE_FrameLineEdit) { - theme = OpenThemeData(nullptr, L"Edit"); - partId = EP_EDITBORDER_NOSCROLL; - - if (oldState & State_MouseOver) - fromState = ETS_HOT; - else if (oldState & State_HasFocus) - fromState = ETS_FOCUSED; - else - fromState = ETS_NORMAL; - - if (state & State_MouseOver) - toState = ETS_HOT; - else if (state & State_HasFocus) - toState = ETS_FOCUSED; - else - toState = ETS_NORMAL; - - } else { - theme = OpenThemeData(nullptr, L"Button"); - if (element == PE_IndicatorRadioButton) - partId = BP_RADIOBUTTON; - else if (element == PE_IndicatorCheckBox) - partId = BP_CHECKBOX; - else - partId = BP_PUSHBUTTON; - - fromState = buttonStateId(oldState, partId); - toState = buttonStateId(option->state, partId); - } - - // Retrieve the transition time between the states from the system. - if (theme - && SUCCEEDED(GetThemeTransitionDuration(theme, partId, fromState, toState, - TMT_TRANSITIONDURATIONS, &duration))) { - t->setDuration(int(duration)); - } - t->setStartTime(d->animationTime()); - - deleteClonedAnimationStyleOption(styleOption); - d->startAnimation(t); - } - styleObject->setProperty("_q_no_animation", false); - } - - } // End of animation part - } - - QRect rect = option->rect; - - switch (element) { - case PE_IndicatorHeaderArrow: - if (const QStyleOptionHeader *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { - int stateId = HSAS_SORTEDDOWN; - if (header->sortIndicator & QStyleOptionHeader::SortDown) - stateId = HSAS_SORTEDUP; //note that the uxtheme sort down indicator is the inverse of ours - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::HeaderTheme, - HP_HEADERSORTARROW, stateId, option->rect); - d->drawBackground(theme); - } - break; - - case PE_IndicatorBranch: - { - XPThemeData theme(widget, painter, QWindowsXPStylePrivate::VistaTreeViewTheme); - static int decoration_size = 0; - if (!decoration_size && theme.isValid()) { - XPThemeData themeSize = theme; - themeSize.partId = TVP_HOTGLYPH; - themeSize.stateId = GLPS_OPENED; - const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - decoration_size = qRound(qMax(size.width(), size.height())); - } - int mid_h = option->rect.x() + option->rect.width() / 2; - int mid_v = option->rect.y() + option->rect.height() / 2; - int bef_h = mid_h; - int bef_v = mid_v; - int aft_h = mid_h; - int aft_v = mid_v; - if (option->state & State_Children) { - int delta = decoration_size / 2; - theme.rect = QRect(bef_h - delta, bef_v - delta, decoration_size, decoration_size); - theme.partId = option->state & State_MouseOver ? TVP_HOTGLYPH : TVP_GLYPH; - theme.stateId = option->state & QStyle::State_Open ? GLPS_OPENED : GLPS_CLOSED; - if (option->direction == Qt::RightToLeft) - theme.mirrorHorizontally = true; - d->drawBackground(theme); - bef_h -= delta + 2; - bef_v -= delta + 2; - aft_h += delta - 2; - aft_v += delta - 2; - } -#if 0 - QBrush brush(option->palette.dark().color(), Qt::Dense4Pattern); - if (option->state & State_Item) { - if (option->direction == Qt::RightToLeft) - painter->fillRect(option->rect.left(), mid_v, bef_h - option->rect.left(), 1, brush); - else - painter->fillRect(aft_h, mid_v, option->rect.right() - aft_h + 1, 1, brush); - } - if (option->state & State_Sibling && option->rect.bottom() > aft_v) - painter->fillRect(mid_h, aft_v, 1, option->rect.bottom() - aft_v + 1, brush); - if (option->state & (State_Open | State_Children | State_Item | State_Sibling) && (bef_v > option->rect.y())) - painter->fillRect(mid_h, option->rect.y(), 1, bef_v - option->rect.y(), brush); -#endif - } - break; - - case PE_PanelButtonBevel: - case PE_IndicatorCheckBox: - case PE_IndicatorRadioButton: - { - if (QWindowsVistaAnimation *a = - qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option)))){ - a->paint(painter, option); - } else { - QWindowsXPStyle::drawPrimitive(element, option, painter, widget); - } - } - break; - - case PE_FrameMenu: - { - int stateId = option->state & State_Active ? MB_ACTIVE : MB_INACTIVE; - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPBORDERS, stateId, option->rect); - d->drawBackground(theme); - } - break; - case PE_Frame: { -#if QT_CONFIG(accessibility) - if (QStyleHelper::isInstanceOf(option->styleObject, QAccessible::EditableText) - || QStyleHelper::isInstanceOf(option->styleObject, QAccessible::StaticText) || -#else - if ( -#endif - (widget && widget->inherits("QTextEdit"))) { - painter->save(); - int stateId = ETS_NORMAL; - if (!(state & State_Enabled)) - stateId = ETS_DISABLED; - else if (state & State_ReadOnly) - stateId = ETS_READONLY; - else if (state & State_HasFocus) - stateId = ETS_SELECTED; - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::EditTheme, - EP_EDITBORDER_HVSCROLL, stateId, option->rect); - // Since EP_EDITBORDER_HVSCROLL does not us borderfill, theme.noContent cannot be used for clipping - int borderSize = 1; - GetThemeInt(theme.handle(), theme.partId, theme.stateId, TMT_BORDERSIZE, &borderSize); - QRegion clipRegion = option->rect; - QRegion content = option->rect.adjusted(borderSize, borderSize, -borderSize, -borderSize); - clipRegion ^= content; - painter->setClipRegion(clipRegion); - d->drawBackground(theme); - painter->restore(); - } else { - QWindowsXPStyle::drawPrimitive(element, option, painter, widget); - } - } - break; - - case PE_PanelLineEdit: - if (const QStyleOptionFrame *panel = qstyleoption_cast<const QStyleOptionFrame *>(option)) { - bool isEnabled = option->state & State_Enabled; - if (QWindowsXPStylePrivate::isLineEditBaseColorSet(option, widget)) { - painter->fillRect(panel->rect, panel->palette.brush(QPalette::Base)); - } else { - int partId = EP_BACKGROUND; - int stateId = EBS_NORMAL; - if (!isEnabled) - stateId = EBS_DISABLED; - else if (state & State_ReadOnly) - stateId = EBS_READONLY; - else if (state & State_MouseOver) - stateId = EBS_HOT; - - XPThemeData theme(nullptr, painter, QWindowsXPStylePrivate::EditTheme, - partId, stateId, rect); - if (!theme.isValid()) { - QWindowsStyle::drawPrimitive(element, option, painter, widget); - return; - } - int bgType; - GetThemeEnumValue(theme.handle(), partId, stateId, TMT_BGTYPE, &bgType); - if ( bgType == BT_IMAGEFILE ) { - d->drawBackground(theme); - } else { - QBrush fillColor = option->palette.brush(QPalette::Base); - if (!isEnabled) { - PROPERTYORIGIN origin = PO_NOTFOUND; - GetThemePropertyOrigin(theme.handle(), theme.partId, theme.stateId, TMT_FILLCOLOR, &origin); - // Use only if the fill property comes from our part - if ((origin == PO_PART || origin == PO_STATE)) { - COLORREF bgRef; - GetThemeColor(theme.handle(), partId, stateId, TMT_FILLCOLOR, &bgRef); - fillColor = QBrush(qRgb(GetRValue(bgRef), GetGValue(bgRef), GetBValue(bgRef))); - } - } - painter->fillRect(option->rect, fillColor); - } - } - if (panel->lineWidth > 0) - proxy()->drawPrimitive(PE_FrameLineEdit, panel, painter, widget); - return; - } - break; - - case PE_FrameLineEdit: - if (QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option)))) { - anim->paint(painter, option); - } else { - QPainter *p = painter; - if (QWindowsXPStylePrivate::isItemViewDelegateLineEdit(widget)) { - // we try to check if this lineedit is a delegate on a QAbstractItemView-derived class. - QPen oldPen = p->pen(); - // Inner white border - p->setPen(QPen(option->palette.base().color(), 1)); - p->drawRect(option->rect.adjusted(1, 1, -2, -2)); - // Outer dark border - p->setPen(QPen(option->palette.shadow().color(), 1)); - p->drawRect(option->rect.adjusted(0, 0, -1, -1)); - p->setPen(oldPen); - return; - } - int stateId = ETS_NORMAL; - if (!(state & State_Enabled)) - stateId = ETS_DISABLED; - else if (state & State_ReadOnly) - stateId = ETS_READONLY; - else if (state & State_MouseOver) - stateId = ETS_HOT; - else if (state & State_HasFocus) - stateId = ETS_SELECTED; - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::EditTheme, - EP_EDITBORDER_NOSCROLL, stateId, option->rect); - theme.noContent = true; - painter->save(); - QRegion clipRegion = option->rect; - clipRegion -= option->rect.adjusted(2, 2, -2, -2); - painter->setClipRegion(clipRegion); - d->drawBackground(theme); - painter->restore(); - } - break; - - case PE_IndicatorToolBarHandle: - { - XPThemeData theme; - QRect rect; - if (option->state & State_Horizontal) { - theme = XPThemeData(widget, painter, - QWindowsXPStylePrivate::RebarTheme, - RP_GRIPPER, ETS_NORMAL, option->rect.adjusted(0, 1, -2, -2)); - rect = option->rect.adjusted(0, 1, 0, -2); - rect.setWidth(4); - } else { - theme = XPThemeData(widget, painter, QWindowsXPStylePrivate::RebarTheme, - RP_GRIPPERVERT, ETS_NORMAL, option->rect.adjusted(0, 1, -2, -2)); - rect = option->rect.adjusted(1, 0, -1, 0); - rect.setHeight(4); - } - theme.rect = rect; - d->drawBackground(theme); - } - break; - - case PE_IndicatorToolBarSeparator: - { - QPen pen = painter->pen(); - int margin = 3; - painter->setPen(option->palette.window().color().darker(114)); - if (option->state & State_Horizontal) { - int x1 = option->rect.center().x(); - painter->drawLine(QPoint(x1, option->rect.top() + margin), QPoint(x1, option->rect.bottom() - margin)); - } else { - int y1 = option->rect.center().y(); - painter->drawLine(QPoint(option->rect.left() + margin, y1), QPoint(option->rect.right() - margin, y1)); - } - painter->setPen(pen); - } - break; - - case PE_PanelTipLabel: { - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::ToolTipTheme, - TTP_STANDARD, TTSS_NORMAL, option->rect); - d->drawBackground(theme); - break; - } - - case PE_PanelItemViewItem: - { - const QStyleOptionViewItem *vopt; - bool newStyle = true; - QAbstractItemView::SelectionBehavior selectionBehavior = QAbstractItemView::SelectRows; - QAbstractItemView::SelectionMode selectionMode = QAbstractItemView::NoSelection; - if (const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(widget)) { - newStyle = !qobject_cast<const QTableView*>(view); - selectionBehavior = view->selectionBehavior(); - selectionMode = view->selectionMode(); -#if QT_CONFIG(accessibility) - } else if (!widget) { - newStyle = !QStyleHelper::hasAncestor(option->styleObject, QAccessible::MenuItem) ; -#endif - } - - if (newStyle && (vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option))) { - bool selected = vopt->state & QStyle::State_Selected; - const bool hover = selectionMode != QAbstractItemView::NoSelection && (vopt->state & QStyle::State_MouseOver); - bool active = vopt->state & QStyle::State_Active; - - if (vopt->features & QStyleOptionViewItem::Alternate) - painter->fillRect(vopt->rect, vopt->palette.alternateBase()); - - QPalette::ColorGroup cg = vopt->state & QStyle::State_Enabled - ? QPalette::Normal : QPalette::Disabled; - if (cg == QPalette::Normal && !(vopt->state & QStyle::State_Active)) - cg = QPalette::Inactive; - - QRect itemRect = subElementRect(QStyle::SE_ItemViewItemFocusRect, option, widget).adjusted(-1, 0, 1, 0); - itemRect.setTop(vopt->rect.top()); - itemRect.setBottom(vopt->rect.bottom()); - - QSize sectionSize = itemRect.size(); - if (vopt->showDecorationSelected) - sectionSize = vopt->rect.size(); - - if (selectionBehavior == QAbstractItemView::SelectRows) - sectionSize.setWidth(vopt->rect.width()); - QPixmap pixmap; - - if (vopt->backgroundBrush.style() != Qt::NoBrush) { - const QPointF oldBrushOrigin = painter->brushOrigin(); - painter->setBrushOrigin(vopt->rect.topLeft()); - painter->fillRect(vopt->rect, vopt->backgroundBrush); - painter->setBrushOrigin(oldBrushOrigin); - } - - if (hover || selected) { - if (sectionSize.width() > 0 && sectionSize.height() > 0) { - QString key = QString::fromLatin1("qvdelegate-%1-%2-%3-%4-%5").arg(sectionSize.width()) - .arg(sectionSize.height()).arg(selected).arg(active).arg(hover); - if (!QPixmapCache::find(key, &pixmap)) { - pixmap = QPixmap(sectionSize); - pixmap.fill(Qt::transparent); - - int state; - if (selected && hover) - state = LISS_HOTSELECTED; - else if (selected && !active) - state = LISS_SELECTEDNOTFOCUS; - else if (selected) - state = LISS_SELECTED; - else - state = LISS_HOT; - - QPainter pixmapPainter(&pixmap); - XPThemeData theme(widget, &pixmapPainter, - QWindowsXPStylePrivate::VistaTreeViewTheme, - LVP_LISTITEM, state, QRect(0, 0, sectionSize.width(), sectionSize.height())); - if (theme.isValid()) { - d->drawBackground(theme); - } else { - QWindowsXPStyle::drawPrimitive(PE_PanelItemViewItem, option, painter, widget); - break; - } - QPixmapCache::insert(key, pixmap); - } - } - - if (vopt->showDecorationSelected) { - const int frame = 2; //Assumes a 2 pixel pixmap border - QRect srcRect = QRect(0, 0, sectionSize.width(), sectionSize.height()); - QRect pixmapRect = vopt->rect; - bool reverse = vopt->direction == Qt::RightToLeft; - bool leftSection = vopt->viewItemPosition == QStyleOptionViewItem::Beginning; - bool rightSection = vopt->viewItemPosition == QStyleOptionViewItem::End; - if (vopt->viewItemPosition == QStyleOptionViewItem::OnlyOne - || vopt->viewItemPosition == QStyleOptionViewItem::Invalid) - painter->drawPixmap(pixmapRect.topLeft(), pixmap); - else if (reverse ? rightSection : leftSection){ - painter->drawPixmap(QRect(pixmapRect.topLeft(), - QSize(frame, pixmapRect.height())), pixmap, - QRect(QPoint(0, 0), QSize(frame, pixmapRect.height()))); - painter->drawPixmap(pixmapRect.adjusted(frame, 0, 0, 0), - pixmap, srcRect.adjusted(frame, 0, -frame, 0)); - } else if (reverse ? leftSection : rightSection) { - painter->drawPixmap(QRect(pixmapRect.topRight() - QPoint(frame - 1, 0), - QSize(frame, pixmapRect.height())), pixmap, - QRect(QPoint(pixmapRect.width() - frame, 0), - QSize(frame, pixmapRect.height()))); - painter->drawPixmap(pixmapRect.adjusted(0, 0, -frame, 0), - pixmap, srcRect.adjusted(frame, 0, -frame, 0)); - } else if (vopt->viewItemPosition == QStyleOptionViewItem::Middle) - painter->drawPixmap(pixmapRect, pixmap, - srcRect.adjusted(frame, 0, -frame, 0)); - } else { - if (vopt->text.isEmpty() && vopt->icon.isNull()) - break; - painter->drawPixmap(itemRect.topLeft(), pixmap); - } - } - } else { - QWindowsXPStyle::drawPrimitive(element, option, painter, widget); - } - break; - } - case PE_Widget: - { -#if QT_CONFIG(dialogbuttonbox) - const QDialogButtonBox *buttonBox = nullptr; - - if (qobject_cast<const QMessageBox *> (widget)) - buttonBox = widget->findChild<const QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); -#if QT_CONFIG(inputdialog) - else if (qobject_cast<const QInputDialog *> (widget)) - buttonBox = widget->findChild<const QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); -#endif // QT_CONFIG(inputdialog) - - if (buttonBox) { - //draw white panel part - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::TaskDialogTheme, - TDLG_PRIMARYPANEL, 0, option->rect); - QRect toprect = option->rect; - toprect.setBottom(buttonBox->geometry().top()); - theme.rect = toprect; - d->drawBackground(theme); - - //draw bottom panel part - QRect buttonRect = option->rect; - buttonRect.setTop(buttonBox->geometry().top()); - theme.rect = buttonRect; - theme.partId = TDLG_SECONDARYPANEL; - d->drawBackground(theme); - } -#endif - } - break; - default: - QWindowsXPStyle::drawPrimitive(element, option, painter, widget); - break; - } -} - - -/*! - \internal - - see drawPrimitive for comments on the animation support - */ -void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption *option, - QPainter *painter, const QWidget *widget) const -{ - QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); - - if (!QWindowsVistaStylePrivate::useVista()) { - QWindowsStyle::drawControl(element, option, painter, widget); - return; - } - - bool selected = option->state & State_Selected; - bool pressed = option->state & State_Sunken; - bool disabled = !(option->state & State_Enabled); - - int state = option->state; - int themeNumber = -1; - - QRect rect(option->rect); - State flags = option->state; - int partId = 0; - int stateId = 0; - - if (d->transitionsEnabled() && canAnimate(option)) - { - if (element == CE_PushButtonBevel) { - QRect oldRect; - QRect newRect; - - QObject *styleObject = option->styleObject; - - int oldState = styleObject->property("_q_stylestate").toInt(); - oldRect = styleObject->property("_q_stylerect").toRect(); - newRect = option->rect; - styleObject->setProperty("_q_stylestate", int(option->state)); - styleObject->setProperty("_q_stylerect", option->rect); - - bool wasDefault = false; - bool isDefault = false; - if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton *>(option)) { - wasDefault = styleObject->property("_q_isdefault").toBool(); - isDefault = button->features & QStyleOptionButton::DefaultButton; - styleObject->setProperty("_q_isdefault", isDefault); - } - - bool doTransition = ((state & State_Sunken) != (oldState & State_Sunken) || - (state & State_On) != (oldState & State_On) || - (state & State_MouseOver) != (oldState & State_MouseOver)); - - if (oldRect != newRect || (wasDefault && !isDefault)) { - doTransition = false; - d->stopAnimation(styleObject); - } - - if (doTransition) { - styleObject->setProperty("_q_no_animation", true); - - QWindowsVistaTransition *t = new QWindowsVistaTransition(styleObject); - QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); - QStyleOption *styleOption = clonedAnimationStyleOption(option); - styleOption->state = QStyle::State(oldState); - - QImage startImage = createAnimationBuffer(option, widget); - QPainter startPainter(&startImage); - - // Use current state of existing animation if already one is running - if (!anim) { - proxy()->drawControl(element, styleOption, &startPainter, widget); - } else { - anim->paint(&startPainter, styleOption); - d->stopAnimation(styleObject); - } - - t->setStartImage(startImage); - QImage endImage = createAnimationBuffer(option, widget); - QPainter endPainter(&endImage); - styleOption->state = option->state; - proxy()->drawControl(element, styleOption, &endPainter, widget); - t->setEndImage(endImage); - - - DWORD duration = 0; - const HTHEME theme = OpenThemeData(nullptr, L"Button"); - - int fromState = buttonStateId(oldState, BP_PUSHBUTTON); - int toState = buttonStateId(option->state, BP_PUSHBUTTON); - if (GetThemeTransitionDuration(theme, BP_PUSHBUTTON, fromState, toState, TMT_TRANSITIONDURATIONS, &duration) == S_OK) - t->setDuration(int(duration)); - else - t->setDuration(0); - t->setStartTime(d->animationTime()); - styleObject->setProperty("_q_no_animation", false); - - deleteClonedAnimationStyleOption(styleOption); - d->startAnimation(t); - } - - QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); - if (anim) { - anim->paint(painter, option); - return; - } - - } - } - switch (element) { - case CE_PushButtonBevel: - if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) - { - themeNumber = QWindowsXPStylePrivate::ButtonTheme; - partId = BP_PUSHBUTTON; - if (btn->features & QStyleOptionButton::CommandLinkButton) - partId = BP_COMMANDLINK; - bool justFlat = (btn->features & QStyleOptionButton::Flat) && !(flags & (State_On|State_Sunken)); - if (!(flags & State_Enabled) && !(btn->features & QStyleOptionButton::Flat)) - stateId = PBS_DISABLED; - else if (justFlat) - ; - else if (flags & (State_Sunken | State_On)) - stateId = PBS_PRESSED; - else if (flags & State_MouseOver) - stateId = PBS_HOT; - else if (btn->features & QStyleOptionButton::DefaultButton && (state & State_Active)) - stateId = PBS_DEFAULTED; - else - stateId = PBS_NORMAL; - - if (!justFlat) { - - if (d->transitionsEnabled() && (btn->features & QStyleOptionButton::DefaultButton) && - !(state & (State_Sunken | State_On)) && !(state & State_MouseOver) && - (state & State_Enabled) && (state & State_Active)) - { - QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject(option))); - - if (!anim) { - QImage startImage = createAnimationBuffer(option, widget); - QImage alternateImage = createAnimationBuffer(option, widget); - - QWindowsVistaPulse *pulse = new QWindowsVistaPulse(styleObject(option)); - - QPainter startPainter(&startImage); - stateId = PBS_DEFAULTED; - XPThemeData theme(widget, &startPainter, themeNumber, partId, stateId, rect); - d->drawBackground(theme); - - QPainter alternatePainter(&alternateImage); - theme.stateId = PBS_DEFAULTED_ANIMATING; - theme.painter = &alternatePainter; - d->drawBackground(theme); - pulse->setStartImage(startImage); - pulse->setEndImage(alternateImage); - pulse->setStartTime(d->animationTime()); - pulse->setDuration(2000); - d->startAnimation(pulse); - anim = pulse; - } - - if (anim) - anim->paint(painter, option); - else { - XPThemeData theme(widget, painter, themeNumber, partId, stateId, rect); - d->drawBackground(theme); - } - } - else { - XPThemeData theme(widget, painter, themeNumber, partId, stateId, rect); - d->drawBackground(theme); - } - } - - if (btn->features & QStyleOptionButton::HasMenu) { - int mbiw = 0, mbih = 0; - XPThemeData theme(widget, nullptr, QWindowsXPStylePrivate::ToolBarTheme, - TP_DROPDOWNBUTTON); - if (theme.isValid()) { - const QSizeF size = theme.size() * QStyleHelper::dpiScaled(1, option); - if (!size.isEmpty()) { - mbiw = qRound(size.width()); - mbih = qRound(size.height()); - } - } - QRect ir = subElementRect(SE_PushButtonContents, option, nullptr); - QStyleOptionButton newBtn = *btn; - newBtn.rect = QStyle::visualRect(option->direction, option->rect, - QRect(ir.right() - mbiw - 2, - option->rect.top() + (option->rect.height()/2) - (mbih/2), - mbiw + 1, mbih + 1)); - proxy()->drawPrimitive(PE_IndicatorArrowDown, &newBtn, painter, widget); - } - return; - } - break; - - case CE_ProgressBarContents: - if (const QStyleOptionProgressBar *bar - = qstyleoption_cast<const QStyleOptionProgressBar *>(option)) { - bool isIndeterminate = (bar->minimum == 0 && bar->maximum == 0); - const bool vertical = !(bar->state & QStyle::State_Horizontal); - const bool inverted = bar->invertedAppearance; - - if (isIndeterminate || (bar->progress > 0 && (bar->progress < bar->maximum) && d->transitionsEnabled())) { - if (!d->animation(styleObject(option))) - d->startAnimation(new QProgressStyleAnimation(d->animationFps, styleObject(option))); - } else { - d->stopAnimation(styleObject(option)); - } - - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::ProgressTheme, - vertical ? PP_FILLVERT : PP_FILL); - theme.rect = option->rect; - bool reverse = (bar->direction == Qt::LeftToRight && inverted) || (bar->direction == Qt::RightToLeft && !inverted); - QTime current = d->animationTime(); - - if (isIndeterminate) { - if (QProgressStyleAnimation *a = qobject_cast<QProgressStyleAnimation *>(d->animation(styleObject(option)))) { - int glowSize = 120; - int animationWidth = glowSize * 2 + (vertical ? theme.rect.height() : theme.rect.width()); - int animOffset = a->startTime().msecsTo(current) / 4; - if (animOffset > animationWidth) - a->setStartTime(d->animationTime()); - painter->save(); - painter->setClipRect(theme.rect); - QRect animRect; - QSize pixmapSize(14, 14); - if (vertical) { - animRect = QRect(theme.rect.left(), - inverted ? rect.top() - glowSize + animOffset : - rect.bottom() + glowSize - animOffset, - rect.width(), glowSize); - pixmapSize.setHeight(animRect.height()); - } else { - animRect = QRect(rect.left() - glowSize + animOffset, - rect.top(), glowSize, rect.height()); - animRect = QStyle::visualRect(reverse ? Qt::RightToLeft : Qt::LeftToRight, - option->rect, animRect); - pixmapSize.setWidth(animRect.width()); - } - QString name = QString::fromLatin1("qiprogress-%1-%2").arg(pixmapSize.width()).arg(pixmapSize.height()); - QPixmap pixmap; - if (!QPixmapCache::find(name, &pixmap)) { - QImage image(pixmapSize, QImage::Format_ARGB32); - image.fill(Qt::transparent); - QPainter imagePainter(&image); - theme.painter = &imagePainter; - theme.partId = vertical ? PP_FILLVERT : PP_FILL; - theme.rect = QRect(QPoint(0,0), animRect.size()); - QLinearGradient alphaGradient(0, 0, vertical ? 0 : image.width(), - vertical ? image.height() : 0); - alphaGradient.setColorAt(0, QColor(0, 0, 0, 0)); - alphaGradient.setColorAt(0.5, QColor(0, 0, 0, 220)); - alphaGradient.setColorAt(1, QColor(0, 0, 0, 0)); - imagePainter.fillRect(image.rect(), alphaGradient); - imagePainter.setCompositionMode(QPainter::CompositionMode_SourceIn); - d->drawBackground(theme); - imagePainter.end(); - pixmap = QPixmap::fromImage(image); - QPixmapCache::insert(name, pixmap); - } - painter->drawPixmap(animRect, pixmap); - painter->restore(); - } - } - else { - qint64 progress = qMax<qint64>(bar->progress, bar->minimum); // workaround for bug in QProgressBar - - if (vertical) { - int maxHeight = option->rect.height(); - int minHeight = 0; - double vc6_workaround = ((progress - qint64(bar->minimum)) / qMax(double(1.0), double(qint64(bar->maximum) - qint64(bar->minimum))) * maxHeight); - int height = isIndeterminate ? maxHeight: qMax(int(vc6_workaround), minHeight); - theme.rect.setHeight(height); - if (!inverted) - theme.rect.moveTop(rect.height() - theme.rect.height()); - } else { - int maxWidth = option->rect.width(); - int minWidth = 0; - double vc6_workaround = ((progress - qint64(bar->minimum)) / qMax(double(1.0), double(qint64(bar->maximum) - qint64(bar->minimum))) * maxWidth); - int width = isIndeterminate ? maxWidth : qMax(int(vc6_workaround), minWidth); - theme.rect.setWidth(width); - theme.rect = QStyle::visualRect(reverse ? Qt::RightToLeft : Qt::LeftToRight, - option->rect, theme.rect); - } - d->drawBackground(theme); - - if (QProgressStyleAnimation *a = qobject_cast<QProgressStyleAnimation *>(d->animation(styleObject(option)))) { - int glowSize = 140; - int animationWidth = glowSize * 2 + (vertical ? theme.rect.height() : theme.rect.width()); - int animOffset = a->startTime().msecsTo(current) / 4; - theme.partId = vertical ? PP_MOVEOVERLAYVERT : PP_MOVEOVERLAY; - if (animOffset > animationWidth) { - if (bar->progress < bar->maximum) - a->setStartTime(d->animationTime()); - else - d->stopAnimation(styleObject(option)); //we stop the glow motion only after it has - //moved out of view - } - painter->save(); - painter->setClipRect(theme.rect); - if (vertical) { - theme.rect = QRect(theme.rect.left(), - inverted ? rect.top() - glowSize + animOffset : - rect.bottom() + glowSize - animOffset, - rect.width(), glowSize); - } else { - theme.rect = QRect(rect.left() - glowSize + animOffset,rect.top(), glowSize, rect.height()); - theme.rect = QStyle::visualRect(reverse ? Qt::RightToLeft : Qt::LeftToRight, option->rect, theme.rect); - } - d->drawBackground(theme); - painter->restore(); - } - } - } - break; - - case CE_MenuBarItem: - { - - if (const QStyleOptionMenuItem *mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) - { - if (mbi->menuItemType == QStyleOptionMenuItem::DefaultItem) - break; - - QPalette::ColorRole textRole = disabled ? QPalette::Text : QPalette::ButtonText; - QPixmap pix = mbi->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), QIcon::Normal); - - int alignment = Qt::AlignCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; - if (!proxy()->styleHint(SH_UnderlineShortcut, mbi, widget)) - alignment |= Qt::TextHideMnemonic; - - if (widget && mbi->palette.color(QPalette::Window) != Qt::transparent) { // Not needed for QtQuick Controls - //The rect adjustment is a workaround for the menu not really filling its background. - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_BARBACKGROUND, 0, option->rect.adjusted(-1, 0, 2, 1)); - d->drawBackground(theme); - } - - int stateId = MBI_NORMAL; - if (disabled) - stateId = MBI_DISABLED; - else if (pressed) - stateId = MBI_PUSHED; - else if (selected) - stateId = MBI_HOT; - - XPThemeData theme2(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_BARITEM, stateId, option->rect); - d->drawBackground(theme2); - - if (!pix.isNull()) - drawItemPixmap(painter, mbi->rect, alignment, pix); - else - drawItemText(painter, mbi->rect, alignment, mbi->palette, mbi->state & State_Enabled, mbi->text, textRole); - } - } - break; -#if QT_CONFIG(menu) - case CE_MenuItem: - if (const QStyleOptionMenuItem *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { - // windows always has a check column, regardless whether we have an icon or not - const qreal factor = QWindowsXPStylePrivate::nativeMetricScaleFactor(widget); - int checkcol = qRound(qreal(25) * factor); - const int gutterWidth = qRound(qreal(3) * factor); - { - XPThemeData theme(widget, nullptr, QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPCHECKBACKGROUND, MBI_HOT); - XPThemeData themeSize = theme; - themeSize.partId = MENU_POPUPCHECK; - themeSize.stateId = 0; - const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - const QMarginsF margins = themeSize.margins() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - checkcol = qMax(menuitem->maxIconWidth, qRound(gutterWidth + size.width() + margins.left() + margins.right())); - } - QRect rect = option->rect; - - //draw vertical menu line - if (option->direction == Qt::LeftToRight) - checkcol += rect.x(); - QPoint p1 = QStyle::visualPos(option->direction, menuitem->rect, QPoint(checkcol, rect.top())); - QPoint p2 = QStyle::visualPos(option->direction, menuitem->rect, QPoint(checkcol, rect.bottom())); - QRect gutterRect(p1.x(), p1.y(), gutterWidth, p2.y() - p1.y() + 1); - XPThemeData theme2(widget, painter, QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPGUTTER, stateId, gutterRect); - d->drawBackground(theme2); - - int x, y, w, h; - menuitem->rect.getRect(&x, &y, &w, &h); - int tab = menuitem->reservedShortcutWidth; - bool dis = !(menuitem->state & State_Enabled); - bool checked = menuitem->checkType != QStyleOptionMenuItem::NotCheckable - ? menuitem->checked : false; - bool act = menuitem->state & State_Selected; - - if (menuitem->menuItemType == QStyleOptionMenuItem::Separator) { - int yoff = y-2 + h / 2; - const int separatorSize = qRound(qreal(6) * QWindowsStylePrivate::nativeMetricScaleFactor(widget)); - QPoint p1 = QPoint(x + checkcol, yoff); - QPoint p2 = QPoint(x + w + separatorSize, yoff); - stateId = MBI_HOT; - QRect subRect(p1.x() + (gutterWidth - menuitem->rect.x()), p1.y(), - p2.x() - p1.x(), separatorSize); - subRect = QStyle::visualRect(option->direction, option->rect, subRect ); - XPThemeData theme2(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPSEPARATOR, stateId, subRect); - d->drawBackground(theme2); - return; - } - - QRect vCheckRect = visualRect(option->direction, menuitem->rect, QRect(menuitem->rect.x(), - menuitem->rect.y(), checkcol - (gutterWidth + menuitem->rect.x()), menuitem->rect.height())); - - if (act) { - stateId = dis ? MBI_DISABLED : MBI_HOT; - XPThemeData theme2(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPITEM, stateId, option->rect); - d->drawBackground(theme2); - } - - if (checked) { - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPCHECKBACKGROUND, - menuitem->icon.isNull() ? MBI_HOT : MBI_PUSHED, vCheckRect); - XPThemeData themeSize = theme; - themeSize.partId = MENU_POPUPCHECK; - themeSize.stateId = 0; - const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - const QMarginsF margins = themeSize.margins() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - QRect checkRect(0, 0, qRound(size.width() + margins.left() + margins.right()), - qRound(size.height() + margins.bottom() + margins.top())); - checkRect.moveCenter(vCheckRect.center()); - theme.rect = checkRect; - - d->drawBackground(theme); - - if (menuitem->icon.isNull()) { - checkRect = QRect(QPoint(0, 0), size.toSize()); - checkRect.moveCenter(theme.rect.center()); - theme.rect = checkRect; - - theme.partId = MENU_POPUPCHECK; - bool bullet = menuitem->checkType & QStyleOptionMenuItem::Exclusive; - if (dis) - theme.stateId = bullet ? MC_BULLETDISABLED: MC_CHECKMARKDISABLED; - else - theme.stateId = bullet ? MC_BULLETNORMAL: MC_CHECKMARKNORMAL; - d->drawBackground(theme); - } - } - - if (!menuitem->icon.isNull()) { - QIcon::Mode mode = dis ? QIcon::Disabled : QIcon::Normal; - if (act && !dis) - mode = QIcon::Active; - QPixmap pixmap; - if (checked) - pixmap = menuitem->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), mode, QIcon::On); - else - pixmap = menuitem->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), mode); - QRect pmr(QPoint(0, 0), pixmap.deviceIndependentSize().toSize()); - pmr.moveCenter(vCheckRect.center()); - painter->setPen(menuitem->palette.text().color()); - painter->drawPixmap(pmr.topLeft(), pixmap); - } - - painter->setPen(menuitem->palette.buttonText().color()); - - const QColor textColor = menuitem->palette.text().color(); - if (dis) - painter->setPen(textColor); - - int xm = windowsItemFrame + checkcol + windowsItemHMargin + (gutterWidth - menuitem->rect.x()) - 1; - int xpos = menuitem->rect.x() + xm; - QRect textRect(xpos, y + windowsItemVMargin, w - xm - windowsRightBorder - tab + 1, h - 2 * windowsItemVMargin); - QRect vTextRect = visualRect(option->direction, menuitem->rect, textRect); - QString s = menuitem->text; - if (!s.isEmpty()) { // draw text - painter->save(); - int t = s.indexOf(QLatin1Char('\t')); - int text_flags = Qt::AlignVCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; - if (!proxy()->styleHint(SH_UnderlineShortcut, menuitem, widget)) - text_flags |= Qt::TextHideMnemonic; - text_flags |= Qt::AlignLeft; - if (t >= 0) { - QRect vShortcutRect = visualRect(option->direction, menuitem->rect, - QRect(textRect.topRight(), QPoint(menuitem->rect.right(), textRect.bottom()))); - painter->drawText(vShortcutRect, text_flags, s.mid(t + 1)); - s = s.left(t); - } - QFont font = menuitem->font; - if (menuitem->menuItemType == QStyleOptionMenuItem::DefaultItem) - font.setBold(true); - painter->setFont(font); - painter->setPen(textColor); - painter->drawText(vTextRect, text_flags, s.left(t)); - painter->restore(); - } - if (menuitem->menuItemType == QStyleOptionMenuItem::SubMenu) {// draw sub menu arrow - int dim = (h - 2 * windowsItemFrame) / 2; - PrimitiveElement arrow; - arrow = (option->direction == Qt::RightToLeft) ? PE_IndicatorArrowLeft : PE_IndicatorArrowRight; - xpos = x + w - windowsArrowHMargin - windowsItemFrame - dim; - QRect vSubMenuRect = visualRect(option->direction, menuitem->rect, QRect(xpos, y + h / 2 - dim / 2, dim, dim)); - QStyleOptionMenuItem newMI = *menuitem; - newMI.rect = vSubMenuRect; - newMI.state = dis ? State_None : State_Enabled; - proxy()->drawPrimitive(arrow, &newMI, painter, widget); - } - } - break; -#endif // QT_CONFIG(menu) - case CE_HeaderSection: - if (const QStyleOptionHeader *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { - partId = HP_HEADERITEM; - if (flags & State_Sunken) - stateId = HIS_PRESSED; - else if (flags & State_MouseOver) - stateId = HIS_HOT; - else - stateId = HIS_NORMAL; - - if (header->sortIndicator != QStyleOptionHeader::None) - stateId += 3; - - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::HeaderTheme, - partId, stateId, option->rect); - d->drawBackground(theme); - } - break; - case CE_MenuBarEmptyArea: - { - stateId = MBI_NORMAL; - if (!(state & State_Enabled)) - stateId = MBI_DISABLED; - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::MenuTheme, - MENU_BARBACKGROUND, stateId, option->rect); - d->drawBackground(theme); - } - break; - case CE_ToolBar: - if (const QStyleOptionToolBar *toolbar = qstyleoption_cast<const QStyleOptionToolBar *>(option)) { - QPalette pal = option->palette; - pal.setColor(QPalette::Dark, option->palette.window().color().darker(130)); - QStyleOptionToolBar copyOpt = *toolbar; - copyOpt.palette = pal; - QWindowsStyle::drawControl(element, ©Opt, painter, widget); - } - break; -#if QT_CONFIG(dockwidget) - case CE_DockWidgetTitle: - if (const QStyleOptionDockWidget *dwOpt = qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { - const QDockWidget *dockWidget = qobject_cast<const QDockWidget *>(widget); - QRect rect = option->rect; - if (dockWidget && dockWidget->isFloating()) { - QWindowsXPStyle::drawControl(element, option, painter, widget); - break; //otherwise fall through - } - - const bool verticalTitleBar = dwOpt->verticalTitleBar; - - if (verticalTitleBar) { - rect = rect.transposed(); - - painter->translate(rect.left() - 1, rect.top() + rect.width()); - painter->rotate(-90); - painter->translate(-rect.left() + 1, -rect.top()); - } - - painter->setBrush(option->palette.window().color().darker(110)); - painter->setPen(option->palette.window().color().darker(130)); - painter->drawRect(rect.adjusted(0, 1, -1, -3)); - - int buttonMargin = 4; - int mw = proxy()->pixelMetric(QStyle::PM_DockWidgetTitleMargin, dwOpt, widget); - int fw = proxy()->pixelMetric(PM_DockWidgetFrameWidth, dwOpt, widget); - const QDockWidget *dw = qobject_cast<const QDockWidget *>(widget); - bool isFloating = dw && dw->isFloating(); - - QRect r = option->rect.adjusted(0, 2, -1, -3); - QRect titleRect = r; - - if (dwOpt->closable) { - QSize sz = proxy()->standardIcon(QStyle::SP_TitleBarCloseButton, dwOpt, widget).actualSize(QSize(10, 10)); - titleRect.adjust(0, 0, -sz.width() - mw - buttonMargin, 0); - } - - if (dwOpt->floatable) { - QSize sz = proxy()->standardIcon(QStyle::SP_TitleBarMaxButton, dwOpt, widget).actualSize(QSize(10, 10)); - titleRect.adjust(0, 0, -sz.width() - mw - buttonMargin, 0); - } - - if (isFloating) { - titleRect.adjust(0, -fw, 0, 0); - if (widget && widget->windowIcon().cacheKey() != QApplication::windowIcon().cacheKey()) - titleRect.adjust(titleRect.height() + mw, 0, 0, 0); - } else { - titleRect.adjust(mw, 0, 0, 0); - if (!dwOpt->floatable && !dwOpt->closable) - titleRect.adjust(0, 0, -mw, 0); - } - if (!verticalTitleBar) - titleRect = visualRect(dwOpt->direction, r, titleRect); - - if (!dwOpt->title.isEmpty()) { - QString titleText = painter->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, - verticalTitleBar ? titleRect.height() : titleRect.width()); - const int indent = 4; - drawItemText(painter, rect.adjusted(indent + 1, 1, -indent - 1, -1), - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextShowMnemonic, - dwOpt->palette, - dwOpt->state & State_Enabled, titleText, - QPalette::WindowText); - } - } - break; -#endif // QT_CONFIG(dockwidget) -#if QT_CONFIG(itemviews) - case CE_ItemViewItem: - { - const QStyleOptionViewItem *vopt; - - const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(widget); - bool newStyle = true; - - if (qobject_cast<const QTableView*>(widget)) - newStyle = false; - - if (newStyle && view && (vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option))) { - /* - // We cannot currently get the correct selection color for "explorer style" views - COLORREF cref = 0; - XPThemeData theme(d->treeViewHelper(), 0, QLatin1String("LISTVIEW"), 0, 0); - unsigned int res = GetThemeColor(theme.handle(), LVP_LISTITEM, LISS_SELECTED, TMT_TEXTCOLOR, &cref); - QColor textColor(GetRValue(cref), GetGValue(cref), GetBValue(cref)); - */ - QPalette palette = vopt->palette; - palette.setColor(QPalette::All, QPalette::HighlightedText, palette.color(QPalette::Active, QPalette::Text)); - // Note that setting a saturated color here results in ugly XOR colors in the focus rect - palette.setColor(QPalette::All, QPalette::Highlight, palette.base().color().darker(108)); - QStyleOptionViewItem adjustedOption = *vopt; - adjustedOption.palette = palette; - // We hide the focusrect in singleselection as it is not required - if ((view->selectionMode() == QAbstractItemView::SingleSelection) - && !(vopt->state & State_KeyboardFocusChange)) - adjustedOption.state &= ~State_HasFocus; - QWindowsXPStyle::drawControl(element, &adjustedOption, painter, widget); - } else { - QWindowsXPStyle::drawControl(element, option, painter, widget); - } - break; - } -#endif // QT_CONFIG(itemviews) -#if QT_CONFIG(combobox) - case CE_ComboBoxLabel: - QCommonStyle::drawControl(element, option, painter, widget); - break; -#endif // QT_CONFIG(combobox) - default: - QWindowsXPStyle::drawControl(element, option, painter, widget); - break; - } -} - -/*! - \internal - - see drawPrimitive for comments on the animation support - - */ -void QWindowsVistaStyle::drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, - QPainter *painter, const QWidget *widget) const -{ - QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); - if (!QWindowsVistaStylePrivate::useVista()) { - QWindowsStyle::drawComplexControl(control, option, painter, widget); - return; - } - - State state = option->state; - SubControls sub = option->subControls; - QRect r = option->rect; - - int partId = 0; - int stateId = 0; - - State flags = option->state; - if (widget && widget->testAttribute(Qt::WA_UnderMouse) && widget->isActiveWindow()) - flags |= State_MouseOver; - - if (d->transitionsEnabled() && canAnimate(option)) - { - - if (control == CC_ScrollBar || control == CC_SpinBox || control == CC_ComboBox) { - - QObject *styleObject = option->styleObject; // Can be widget or qquickitem - - int oldState = styleObject->property("_q_stylestate").toInt(); - int oldActiveControls = styleObject->property("_q_stylecontrols").toInt(); - - QRect oldRect = styleObject->property("_q_stylerect").toRect(); - styleObject->setProperty("_q_stylestate", int(option->state)); - styleObject->setProperty("_q_stylecontrols", int(option->activeSubControls)); - styleObject->setProperty("_q_stylerect", option->rect); - - bool doTransition = ((state & State_Sunken) != (oldState & State_Sunken) || - (state & State_On) != (oldState & State_On) || - (state & State_MouseOver) != (oldState & State_MouseOver) || - oldActiveControls != int(option->activeSubControls)); - - if (qstyleoption_cast<const QStyleOptionSlider *>(option)) { - QRect oldSliderPos = styleObject->property("_q_stylesliderpos").toRect(); - QRect currentPos = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); - styleObject->setProperty("_q_stylesliderpos", currentPos); - if (oldSliderPos != currentPos) { - doTransition = false; - d->stopAnimation(styleObject); - } - } else if (control == CC_SpinBox) { - //spinboxes have a transition when focus changes - if (!doTransition) - doTransition = (state & State_HasFocus) != (oldState & State_HasFocus); - } - - if (oldRect != option->rect) { - doTransition = false; - d->stopAnimation(styleObject); - } - - if (doTransition) { - QImage startImage = createAnimationBuffer(option, widget); - QPainter startPainter(&startImage); - - QImage endImage = createAnimationBuffer(option, widget); - QPainter endPainter(&endImage); - - QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject)); - QWindowsVistaTransition *t = new QWindowsVistaTransition(styleObject); - - // Draw the image that ends the animation by using the current styleoption - QStyleOptionComplex *styleOption = qstyleoption_cast<QStyleOptionComplex*>(clonedAnimationStyleOption(option)); - - styleObject->setProperty("_q_no_animation", true); - - // Draw transition source - if (!anim) { - styleOption->state = QStyle::State(oldState); - styleOption->activeSubControls = QStyle::SubControl(oldActiveControls); - proxy()->drawComplexControl(control, styleOption, &startPainter, widget); - } else { - anim->paint(&startPainter, option); - } - t->setStartImage(startImage); - - // Draw transition target - styleOption->state = option->state; - styleOption->activeSubControls = option->activeSubControls; - proxy()->drawComplexControl(control, styleOption, &endPainter, widget); - - styleObject->setProperty("_q_no_animation", false); - - t->setEndImage(endImage); - t->setStartTime(d->animationTime()); - - if (option->state & State_MouseOver || option->state & State_Sunken) - t->setDuration(150); - else - t->setDuration(500); - - deleteClonedAnimationStyleOption(styleOption); - d->startAnimation(t); - } - if (QWindowsVistaAnimation *anim = qobject_cast<QWindowsVistaAnimation *>(d->animation(styleObject))) { - anim->paint(painter, option); - return; - } - } - } - - switch (control) { - case CC_ComboBox: - if (const QStyleOptionComboBox *cmb = qstyleoption_cast<const QStyleOptionComboBox *>(option)) - { - if (cmb->editable) { - if (sub & SC_ComboBoxEditField) { - partId = EP_EDITBORDER_NOSCROLL; - if (!(flags & State_Enabled)) - stateId = ETS_DISABLED; - else if (flags & State_MouseOver) - stateId = ETS_HOT; - else if (flags & State_HasFocus) - stateId = ETS_FOCUSED; - else - stateId = ETS_NORMAL; - - XPThemeData theme(widget, painter, - QWindowsXPStylePrivate::EditTheme, - partId, stateId, r); - - d->drawBackground(theme); - } - if (sub & SC_ComboBoxArrow) { - QRect subRect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxArrow, widget); - XPThemeData theme(widget, painter, QWindowsXPStylePrivate::ComboboxTheme); - theme.rect = subRect; - partId = option->direction == Qt::RightToLeft ? CP_DROPDOWNBUTTONLEFT : CP_DROPDOWNBUTTONRIGHT; - - if (!(cmb->state & State_Enabled)) - stateId = CBXS_DISABLED; - else if (cmb->state & State_Sunken || cmb->state & State_On) - stateId = CBXS_PRESSED; - else if (cmb->state & State_MouseOver && option->activeSubControls & SC_ComboBoxArrow) - stateId = CBXS_HOT; - else - stateId = CBXS_NORMAL; - - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - - } else { - if (sub & SC_ComboBoxFrame) { - XPThemeData theme(widget, painter, QWindowsXPStylePrivate::ComboboxTheme); - theme.rect = option->rect; - theme.partId = CP_READONLY; - if (!(cmb->state & State_Enabled)) - theme.stateId = CBXS_DISABLED; - else if (cmb->state & State_Sunken || cmb->state & State_On) - theme.stateId = CBXS_PRESSED; - else if (cmb->state & State_MouseOver) - theme.stateId = CBXS_HOT; - else - theme.stateId = CBXS_NORMAL; - d->drawBackground(theme); - } - if (sub & SC_ComboBoxArrow) { - XPThemeData theme(widget, painter, QWindowsXPStylePrivate::ComboboxTheme); - theme.rect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxArrow, widget); - theme.partId = option->direction == Qt::RightToLeft ? CP_DROPDOWNBUTTONLEFT : CP_DROPDOWNBUTTONRIGHT; - if (!(cmb->state & State_Enabled)) - theme.stateId = CBXS_DISABLED; - else - theme.stateId = CBXS_NORMAL; - d->drawBackground(theme); - } - if ((sub & SC_ComboBoxEditField) && (flags & State_HasFocus)) { - QStyleOptionFocusRect fropt; - fropt.QStyleOption::operator=(*cmb); - fropt.rect = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxEditField, widget); - proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); - } - } - } - break; - case CC_ScrollBar: - if (const QStyleOptionSlider *scrollbar = qstyleoption_cast<const QStyleOptionSlider *>(option)) - { - XPThemeData theme(widget, painter, QWindowsXPStylePrivate::ScrollBarTheme); - bool maxedOut = (scrollbar->maximum == scrollbar->minimum); - if (maxedOut) - flags &= ~State_Enabled; - - bool isHorz = flags & State_Horizontal; - bool isRTL = option->direction == Qt::RightToLeft; - if (sub & SC_ScrollBarAddLine) { - theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddLine, widget); - partId = SBP_ARROWBTN; - if (!(flags & State_Enabled)) - stateId = (isHorz ? (isRTL ? ABS_LEFTDISABLED : ABS_RIGHTDISABLED) : ABS_DOWNDISABLED); - else if (scrollbar->activeSubControls & SC_ScrollBarAddLine && (scrollbar->state & State_Sunken)) - stateId = (isHorz ? (isRTL ? ABS_LEFTPRESSED : ABS_RIGHTPRESSED) : ABS_DOWNPRESSED); - else if (scrollbar->activeSubControls & SC_ScrollBarAddLine && (scrollbar->state & State_MouseOver)) - stateId = (isHorz ? (isRTL ? ABS_LEFTHOT : ABS_RIGHTHOT) : ABS_DOWNHOT); - else if (scrollbar->state & State_MouseOver) - stateId = (isHorz ? (isRTL ? ABS_LEFTHOVER : ABS_RIGHTHOVER) : ABS_DOWNHOVER); - else - stateId = (isHorz ? (isRTL ? ABS_LEFTNORMAL : ABS_RIGHTNORMAL) : ABS_DOWNNORMAL); - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - if (sub & SC_ScrollBarSubLine) { - theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubLine, widget); - partId = SBP_ARROWBTN; - if (!(flags & State_Enabled)) - stateId = (isHorz ? (isRTL ? ABS_RIGHTDISABLED : ABS_LEFTDISABLED) : ABS_UPDISABLED); - else if (scrollbar->activeSubControls & SC_ScrollBarSubLine && (scrollbar->state & State_Sunken)) - stateId = (isHorz ? (isRTL ? ABS_RIGHTPRESSED : ABS_LEFTPRESSED) : ABS_UPPRESSED); - else if (scrollbar->activeSubControls & SC_ScrollBarSubLine && (scrollbar->state & State_MouseOver)) - stateId = (isHorz ? (isRTL ? ABS_RIGHTHOT : ABS_LEFTHOT) : ABS_UPHOT); - else if (scrollbar->state & State_MouseOver) - stateId = (isHorz ? (isRTL ? ABS_RIGHTHOVER : ABS_LEFTHOVER) : ABS_UPHOVER); - else - stateId = (isHorz ? (isRTL ? ABS_RIGHTNORMAL : ABS_LEFTNORMAL) : ABS_UPNORMAL); - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - if (maxedOut) { - theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); - theme.rect = theme.rect.united(proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubPage, widget)); - theme.rect = theme.rect.united(proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddPage, widget)); - partId = flags & State_Horizontal ? SBP_LOWERTRACKHORZ : SBP_LOWERTRACKVERT; - stateId = SCRBS_DISABLED; - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } else { - if (sub & SC_ScrollBarSubPage) { - theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSubPage, widget); - partId = flags & State_Horizontal ? SBP_UPPERTRACKHORZ : SBP_UPPERTRACKVERT; - if (!(flags & State_Enabled)) - stateId = SCRBS_DISABLED; - else if (scrollbar->activeSubControls & SC_ScrollBarSubPage && (scrollbar->state & State_Sunken)) - stateId = SCRBS_PRESSED; - else if (scrollbar->activeSubControls & SC_ScrollBarSubPage && (scrollbar->state & State_MouseOver)) - stateId = SCRBS_HOT; - else - stateId = SCRBS_NORMAL; - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - if (sub & SC_ScrollBarAddPage) { - theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarAddPage, widget); - partId = flags & State_Horizontal ? SBP_LOWERTRACKHORZ : SBP_LOWERTRACKVERT; - if (!(flags & State_Enabled)) - stateId = SCRBS_DISABLED; - else if (scrollbar->activeSubControls & SC_ScrollBarAddPage && (scrollbar->state & State_Sunken)) - stateId = SCRBS_PRESSED; - else if (scrollbar->activeSubControls & SC_ScrollBarAddPage && (scrollbar->state & State_MouseOver)) - stateId = SCRBS_HOT; - else - stateId = SCRBS_NORMAL; - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - if (sub & SC_ScrollBarSlider) { - theme.rect = proxy()->subControlRect(CC_ScrollBar, option, SC_ScrollBarSlider, widget); - if (!(flags & State_Enabled)) - stateId = SCRBS_DISABLED; - else if (scrollbar->activeSubControls & SC_ScrollBarSlider && (scrollbar->state & State_Sunken)) - stateId = SCRBS_PRESSED; - else if (scrollbar->activeSubControls & SC_ScrollBarSlider && (scrollbar->state & State_MouseOver)) - stateId = SCRBS_HOT; - else if (option->state & State_MouseOver) - stateId = SCRBS_HOVER; - else - stateId = SCRBS_NORMAL; - - // Draw handle - theme.partId = flags & State_Horizontal ? SBP_THUMBBTNHORZ : SBP_THUMBBTNVERT; - theme.stateId = stateId; - d->drawBackground(theme); - } - } - } - break; -#if QT_CONFIG(spinbox) - case CC_SpinBox: - if (const QStyleOptionSpinBox *sb = qstyleoption_cast<const QStyleOptionSpinBox *>(option)) - { - XPThemeData theme(widget, painter, QWindowsXPStylePrivate::SpinTheme); - if (sb->frame && (sub & SC_SpinBoxFrame)) { - partId = EP_EDITBORDER_NOSCROLL; - if (!(flags & State_Enabled)) - stateId = ETS_DISABLED; - else if (flags & State_MouseOver) - stateId = ETS_HOT; - else if (flags & State_HasFocus) - stateId = ETS_SELECTED; - else - stateId = ETS_NORMAL; - - XPThemeData ftheme(widget, painter, - QWindowsXPStylePrivate::EditTheme, - partId, stateId, r); - // The spinbox in Windows QStyle is drawn with frameless QLineEdit inside it - // That however breaks with QtQuickControls where this results in transparent - // spinbox background, so if there's no "widget" passed (QtQuickControls case), - // let ftheme.noContent be false, which fixes the spinbox rendering in QQC - ftheme.noContent = (widget != nullptr); - d->drawBackground(ftheme); - } - if (sub & SC_SpinBoxUp) { - theme.rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxUp, widget).adjusted(0, 0, 0, 1); - partId = SPNP_UP; - if (!(sb->stepEnabled & QAbstractSpinBox::StepUpEnabled) || !(flags & State_Enabled)) - stateId = UPS_DISABLED; - else if (sb->activeSubControls == SC_SpinBoxUp && (sb->state & State_Sunken)) - stateId = UPS_PRESSED; - else if (sb->activeSubControls == SC_SpinBoxUp && (sb->state & State_MouseOver)) - stateId = UPS_HOT; - else - stateId = UPS_NORMAL; - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - if (sub & SC_SpinBoxDown) { - theme.rect = proxy()->subControlRect(CC_SpinBox, option, SC_SpinBoxDown, widget); - partId = SPNP_DOWN; - if (!(sb->stepEnabled & QAbstractSpinBox::StepDownEnabled) || !(flags & State_Enabled)) - stateId = DNS_DISABLED; - else if (sb->activeSubControls == SC_SpinBoxDown && (sb->state & State_Sunken)) - stateId = DNS_PRESSED; - else if (sb->activeSubControls == SC_SpinBoxDown && (sb->state & State_MouseOver)) - stateId = DNS_HOT; - else - stateId = DNS_NORMAL; - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - } - break; -#endif // QT_CONFIG(spinbox) - default: - QWindowsXPStyle::drawComplexControl(control, option, painter, widget); - break; - } -} - -/*! - \internal - */ -QSize QWindowsVistaStyle::sizeFromContents(ContentsType type, const QStyleOption *option, - const QSize &size, const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) - return QWindowsStyle::sizeFromContents(type, option, size, widget); - - QSize sz(size); - switch (type) { - case CT_MenuItem: - sz = QWindowsXPStyle::sizeFromContents(type, option, size, widget); - int minimumHeight; - { - XPThemeData theme(widget, nullptr, - QWindowsXPStylePrivate::MenuTheme, - MENU_POPUPCHECKBACKGROUND, MBI_HOT); - XPThemeData themeSize = theme; - themeSize.partId = MENU_POPUPCHECK; - themeSize.stateId = 0; - const QSizeF size = themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - const QMarginsF margins = themeSize.margins() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - minimumHeight = qMax(qRound(size.height() + margins.bottom() + margins.top()), sz.height()); - sz.rwidth() += qRound(size.width() + margins.left() + margins.right()); - } - - if (const QStyleOptionMenuItem *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { - if (menuitem->menuItemType != QStyleOptionMenuItem::Separator) - sz.setHeight(minimumHeight); - } - return sz; -#if QT_CONFIG(menubar) - case CT_MenuBarItem: - if (!sz.isEmpty()) - sz += QSize(windowsItemHMargin * 5 + 1, 5); - return sz; -#endif - case CT_ItemViewItem: - sz = QWindowsXPStyle::sizeFromContents(type, option, size, widget); - sz.rheight() += 2; - return sz; - case CT_SpinBox: - { - //Spinbox adds frame twice - sz = QWindowsStyle::sizeFromContents(type, option, size, widget); - int border = proxy()->pixelMetric(PM_SpinBoxFrameWidth, option, widget); - sz -= QSize(2*border, 2*border); - } - return sz; - case CT_HeaderSection: - { - // When there is a sort indicator it adds to the width but it is shown - // above the text natively and not on the side - if (QStyleOptionHeader *hdr = qstyleoption_cast<QStyleOptionHeader *>(const_cast<QStyleOption *>(option))) { - QStyleOptionHeader::SortIndicator sortInd = hdr->sortIndicator; - hdr->sortIndicator = QStyleOptionHeader::None; - sz = QWindowsXPStyle::sizeFromContents(type, hdr, size, widget); - hdr->sortIndicator = sortInd; - return sz; - } - break; - } - default: - break; - } - return QWindowsXPStyle::sizeFromContents(type, option, size, widget); -} - -/*! - \internal - */ -QRect QWindowsVistaStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) - return QWindowsStyle::subElementRect(element, option, widget); - - QRect rect = QWindowsXPStyle::subElementRect(element, option, widget); - switch (element) { - - case SE_PushButtonContents: - if (const QStyleOptionButton *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { - MARGINS borderSize; - const HTHEME theme = OpenThemeData(widget ? QWindowsVistaStylePrivate::winId(widget) : nullptr, L"Button"); - if (theme) { - int stateId = PBS_NORMAL; - if (!(option->state & State_Enabled)) - stateId = PBS_DISABLED; - else if (option->state & State_Sunken) - stateId = PBS_PRESSED; - else if (option->state & State_MouseOver) - stateId = PBS_HOT; - else if (btn->features & QStyleOptionButton::DefaultButton) - stateId = PBS_DEFAULTED; - - int border = proxy()->pixelMetric(PM_DefaultFrameWidth, btn, widget); - rect = option->rect.adjusted(border, border, -border, -border); - - if (SUCCEEDED(GetThemeMargins(theme, nullptr, BP_PUSHBUTTON, stateId, TMT_CONTENTMARGINS, nullptr, &borderSize))) { - rect.adjust(borderSize.cxLeftWidth, borderSize.cyTopHeight, - -borderSize.cxRightWidth, -borderSize.cyBottomHeight); - rect = visualRect(option->direction, option->rect, rect); - } - } - } - break; - - case SE_HeaderArrow: - { - QRect r = rect; - int h = option->rect.height(); - int w = option->rect.width(); - int x = option->rect.x(); - int y = option->rect.y(); - int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget); - - XPThemeData theme(widget, nullptr, - QWindowsXPStylePrivate::HeaderTheme, - HP_HEADERSORTARROW, HSAS_SORTEDDOWN, option->rect); - - int arrowWidth = 13; - int arrowHeight = 5; - if (theme.isValid()) { - const QSizeF size = theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget); - if (!size.isEmpty()) { - arrowWidth = qRound(size.width()); - arrowHeight = qRound(size.height()); - } - } - if (option->state & State_Horizontal) { - r.setRect(x + w/2 - arrowWidth/2, y , arrowWidth, arrowHeight); - } else { - int vert_size = w / 2; - r.setRect(x + 5, y + h - margin * 2 - vert_size, - w - margin * 2 - 5, vert_size); - } - rect = visualRect(option->direction, option->rect, r); - } - break; - - case SE_HeaderLabel: - { - int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget); - QRect r = option->rect; - r.setRect(option->rect.x() + margin, option->rect.y() + margin, - option->rect.width() - margin * 2, option->rect.height() - margin * 2); - if (const QStyleOptionHeader *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { - // Subtract width needed for arrow, if there is one - if (header->sortIndicator != QStyleOptionHeader::None) { - if (!(option->state & State_Horizontal)) //horizontal arrows are positioned on top - r.setHeight(r.height() - (option->rect.width() / 2) - (margin * 2)); - } - } - rect = visualRect(option->direction, option->rect, r); - } - break; - case SE_ProgressBarContents: - rect = QCommonStyle::subElementRect(SE_ProgressBarGroove, option, widget); - break; - case SE_ItemViewItemDecoration: - if (qstyleoption_cast<const QStyleOptionViewItem *>(option)) - rect.adjust(-2, 0, 2, 0); - break; - case SE_ItemViewItemFocusRect: - if (const QStyleOptionViewItem *vopt = qstyleoption_cast<const QStyleOptionViewItem *>(option)) { - QRect textRect = subElementRect(QStyle::SE_ItemViewItemText, option, widget); - QRect displayRect = subElementRect(QStyle::SE_ItemViewItemDecoration, option, widget); - if (!vopt->icon.isNull()) - rect = textRect.united(displayRect); - else - rect = textRect; - rect = rect.adjusted(1, 0, -1, 0); - } - break; - default: - break; - } - return rect; -} - - -/* - This function is used by subControlRect to check if a button - should be drawn for the given subControl given a set of window flags. -*/ -static bool buttonVisible(const QStyle::SubControl sc, const QStyleOptionTitleBar *tb){ - - bool isMinimized = tb->titleBarState & Qt::WindowMinimized; - bool isMaximized = tb->titleBarState & Qt::WindowMaximized; - const auto flags = tb->titleBarFlags; - bool retVal = false; - switch (sc) { - case QStyle::SC_TitleBarContextHelpButton: - if (flags & Qt::WindowContextHelpButtonHint) - retVal = true; - break; - case QStyle::SC_TitleBarMinButton: - if (!isMinimized && (flags & Qt::WindowMinimizeButtonHint)) - retVal = true; - break; - case QStyle::SC_TitleBarNormalButton: - if (isMinimized && (flags & Qt::WindowMinimizeButtonHint)) - retVal = true; - else if (isMaximized && (flags & Qt::WindowMaximizeButtonHint)) - retVal = true; - break; - case QStyle::SC_TitleBarMaxButton: - if (!isMaximized && (flags & Qt::WindowMaximizeButtonHint)) - retVal = true; - break; - case QStyle::SC_TitleBarShadeButton: - if (!isMinimized && flags & Qt::WindowShadeButtonHint) - retVal = true; - break; - case QStyle::SC_TitleBarUnshadeButton: - if (isMinimized && flags & Qt::WindowShadeButtonHint) - retVal = true; - break; - case QStyle::SC_TitleBarCloseButton: - if (flags & Qt::WindowSystemMenuHint) - retVal = true; - break; - case QStyle::SC_TitleBarSysMenu: - if (flags & Qt::WindowSystemMenuHint) - retVal = true; - break; - default : - retVal = true; - } - return retVal; -} - - -/*! \internal */ -int QWindowsVistaStyle::styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, - QStyleHintReturn *returnData) const -{ - QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate*>(d_func()); - int ret = 0; - switch (hint) { - case SH_MessageBox_CenterButtons: - ret = false; - break; - case SH_ToolTip_Mask: - if (option) { - if (QStyleHintReturnMask *mask = qstyleoption_cast<QStyleHintReturnMask*>(returnData)) { - ret = true; - XPThemeData themeData(widget, nullptr, - QWindowsXPStylePrivate::ToolTipTheme, - TTP_STANDARD, TTSS_NORMAL, option->rect); - mask->region = d->region(themeData); - } - } - break; - case SH_Table_GridLineColor: - if (option) - ret = int(option->palette.color(QPalette::Base).darker(118).rgba()); - else - ret = -1; - break; - case SH_Header_ArrowAlignment: - ret = Qt::AlignTop | Qt::AlignHCenter; - break; - default: - ret = QWindowsXPStyle::styleHint(hint, option, widget, returnData); - break; - } - return ret; -} - - -/*! - \internal - */ -QRect QWindowsVistaStyle::subControlRect(ComplexControl control, const QStyleOptionComplex *option, - SubControl subControl, const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) - return QWindowsStyle::subControlRect(control, option, subControl, widget); - - QRect rect = QWindowsXPStyle::subControlRect(control, option, subControl, widget); - switch (control) { -#if QT_CONFIG(combobox) - case CC_ComboBox: - if (const QStyleOptionComboBox *cb = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { - const int x = cb->rect.x(), y = cb->rect.y(), wi = cb->rect.width(), he = cb->rect.height(); - const int margin = cb->frame ? 3 : 0; - const int bmarg = cb->frame ? 2 : 0; - const int arrowWidth = qRound(QStyleHelper::dpiScaled(16, option)); - const int arrowButtonWidth = bmarg + arrowWidth; - const int xpos = x + wi - arrowButtonWidth; - - switch (subControl) { - case SC_ComboBoxFrame: - rect = cb->rect; - break; - case SC_ComboBoxArrow: - rect.setRect(xpos, y , arrowButtonWidth, he); - break; - case SC_ComboBoxEditField: - rect.setRect(x + margin, y + margin, wi - 2 * margin - arrowWidth, he - 2 * margin); - break; - case SC_ComboBoxListBoxPopup: - rect = cb->rect; - break; - default: - break; - } - rect = visualRect(cb->direction, cb->rect, rect); - return rect; - } - break; -#endif // QT_CONFIG(combobox) - case CC_TitleBar: - if (const QStyleOptionTitleBar *tb = qstyleoption_cast<const QStyleOptionTitleBar *>(option)) { - if (!buttonVisible(subControl, tb)) - return rect; - const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); - const bool isToolTitle = false; - const int height = tb->rect.height(); - const int width = tb->rect.width(); - const int buttonWidth = - qRound(qreal(GetSystemMetrics(SM_CXSIZE)) * factor - QStyleHelper::dpiScaled(4, option)); - - const int frameWidth = proxy()->pixelMetric(PM_MdiSubWindowFrameWidth, option, widget); - const bool sysmenuHint = (tb->titleBarFlags & Qt::WindowSystemMenuHint) != 0; - const bool minimizeHint = (tb->titleBarFlags & Qt::WindowMinimizeButtonHint) != 0; - const bool maximizeHint = (tb->titleBarFlags & Qt::WindowMaximizeButtonHint) != 0; - const bool contextHint = (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) != 0; - const bool shadeHint = (tb->titleBarFlags & Qt::WindowShadeButtonHint) != 0; - - switch (subControl) { - case SC_TitleBarLabel: - rect = QRect(frameWidth, 0, width - (buttonWidth + frameWidth + 10), height); - if (isToolTitle) { - if (sysmenuHint) { - rect.adjust(0, 0, int(-buttonWidth - 3 * factor), 0); - } - if (minimizeHint || maximizeHint) - rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); - } else { - if (sysmenuHint) { - const int leftOffset = int(height - 8 * factor); - rect.adjust(leftOffset, 0, 0, int(4 * factor)); - } - if (minimizeHint) - rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); - if (maximizeHint) - rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); - if (contextHint) - rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); - if (shadeHint) - rect.adjust(0, 0, int(-buttonWidth - 2 * factor), 0); - } - rect.translate(0, int(2 * factor)); - rect = visualRect(option->direction, option->rect, rect); - break; - case SC_TitleBarSysMenu: - { - const int controlTop = int(6 * factor); - const int controlHeight = int(height - controlTop - 3 * factor); - int iconExtent = proxy()->pixelMetric(PM_SmallIconSize, option); - QSize iconSize = tb->icon.actualSize(QSize(iconExtent, iconExtent)); - if (tb->icon.isNull()) - iconSize = QSize(controlHeight, controlHeight); - int hPad = (controlHeight - iconSize.height())/2; - int vPad = (controlHeight - iconSize.width())/2; - rect = QRect(frameWidth + hPad, controlTop + vPad, iconSize.width(), iconSize.height()); - rect.translate(0, int(3 * factor)); - rect = visualRect(option->direction, option->rect, rect); - } - break; - default: - break; - } - } - break; - default: - break; - } - return rect; -} - -/*! - \internal - */ -QStyle::SubControl QWindowsVistaStyle::hitTestComplexControl(ComplexControl control, const QStyleOptionComplex *option, - const QPoint &pos, const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) { - return QWindowsStyle::hitTestComplexControl(control, option, pos, widget); - } - return QWindowsXPStyle::hitTestComplexControl(control, option, pos, widget); -} - -int QWindowsVistaStylePrivate::fixedPixelMetric(QStyle::PixelMetric pm) -{ - switch (pm) { - case QStyle::PM_DockWidgetTitleBarButtonMargin: - return 5; - case QStyle::PM_ScrollBarSliderMin: - return 18; - case QStyle::PM_MenuHMargin: - case QStyle::PM_MenuVMargin: - return 0; - case QStyle::PM_MenuPanelWidth: - return 3; - default: - break; - } - return QWindowsVistaStylePrivate::InvalidMetric; -} - -/*! - \internal - */ -int QWindowsVistaStyle::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) - return QWindowsStyle::pixelMetric(metric, option, widget); - - int ret = QWindowsVistaStylePrivate::fixedPixelMetric(metric); - if (ret != QWindowsStylePrivate::InvalidMetric) - return int(QStyleHelper::dpiScaled(ret, option)); - - return QWindowsXPStyle::pixelMetric(metric, option, widget); -} - -/*! - \internal - */ -void QWindowsVistaStyle::polish(QWidget *widget) -{ - QWindowsXPStyle::polish(widget); -#if QT_CONFIG(lineedit) - if (qobject_cast<QLineEdit*>(widget)) - widget->setAttribute(Qt::WA_Hover); - else -#endif // QT_CONFIG(lineedit) - if (qobject_cast<QGroupBox*>(widget)) - widget->setAttribute(Qt::WA_Hover); -#if QT_CONFIG(commandlinkbutton) - else if (qobject_cast<QCommandLinkButton*>(widget)) { - QFont buttonFont = widget->font(); - buttonFont.setFamilies(QStringList{QLatin1String("Segoe UI")}); - widget->setFont(buttonFont); - } -#endif // QT_CONFIG(commandlinkbutton) - else if (widget->inherits("QTipLabel")){ - //note that since tooltips are not reused - //we do not have to care about unpolishing - widget->setContentsMargins(3, 0, 4, 0); - COLORREF bgRef; - HTHEME theme = OpenThemeData(widget ? QWindowsVistaStylePrivate::winId(widget) : nullptr, L"TOOLTIP"); - if (theme && SUCCEEDED(GetThemeColor(theme, TTP_STANDARD, TTSS_NORMAL, TMT_TEXTCOLOR, &bgRef))) { - QColor textColor = QColor::fromRgb(bgRef); - QPalette pal; - pal.setColor(QPalette::All, QPalette::ToolTipText, textColor); - widget->setPalette(pal); - } - } else if (qobject_cast<QMessageBox *> (widget)) { - widget->setAttribute(Qt::WA_StyledBackground); -#if QT_CONFIG(dialogbuttonbox) - QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); - if (buttonBox) - buttonBox->setContentsMargins(0, 9, 0, 0); -#endif - } -#if QT_CONFIG(inputdialog) - else if (qobject_cast<QInputDialog *> (widget)) { - widget->setAttribute(Qt::WA_StyledBackground); -#if QT_CONFIG(dialogbuttonbox) - QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); - if (buttonBox) - buttonBox->setContentsMargins(0, 9, 0, 0); -#endif - } -#endif // QT_CONFIG(inputdialog) - else if (QTreeView *tree = qobject_cast<QTreeView *> (widget)) { - tree->viewport()->setAttribute(Qt::WA_Hover); - } - else if (QListView *list = qobject_cast<QListView *> (widget)) { - list->viewport()->setAttribute(Qt::WA_Hover); - } -} - -/*! - \internal - */ -void QWindowsVistaStyle::unpolish(QWidget *widget) -{ - QWindowsXPStyle::unpolish(widget); - - QWindowsVistaStylePrivate *d = d_func(); - - d->stopAnimation(widget); - -#if QT_CONFIG(lineedit) - if (qobject_cast<QLineEdit*>(widget)) - widget->setAttribute(Qt::WA_Hover, false); - else -#endif // QT_CONFIG(lineedit) - if (qobject_cast<QGroupBox*>(widget)) - widget->setAttribute(Qt::WA_Hover, false); - else if (qobject_cast<QMessageBox *> (widget)) { - widget->setAttribute(Qt::WA_StyledBackground, false); -#if QT_CONFIG(dialogbuttonbox) - QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_msgbox_buttonbox")); - if (buttonBox) - buttonBox->setContentsMargins(0, 0, 0, 0); -#endif - } -#if QT_CONFIG(inputdialog) - else if (qobject_cast<QInputDialog *> (widget)) { - widget->setAttribute(Qt::WA_StyledBackground, false); -#if QT_CONFIG(dialogbuttonbox) - QDialogButtonBox *buttonBox = widget->findChild<QDialogButtonBox *>(QLatin1String("qt_inputdlg_buttonbox")); - if (buttonBox) - buttonBox->setContentsMargins(0, 0, 0, 0); -#endif - } -#endif // QT_CONFIG(inputdialog) - else if (QTreeView *tree = qobject_cast<QTreeView *> (widget)) { - tree->viewport()->setAttribute(Qt::WA_Hover, false); - } -#if QT_CONFIG(commandlinkbutton) - else if (qobject_cast<QCommandLinkButton*>(widget)) { - QFont font = QApplication::font("QCommandLinkButton"); - QFont widgetFont = widget->font(); - widgetFont.setFamilies(font.families()); //Only family set by polish - widget->setFont(widgetFont); - } -#endif // QT_CONFIG(commandlinkbutton) -} - -/*! - \internal - */ -void QWindowsVistaStyle::polish(QPalette &pal) -{ - QWindowsStyle::polish(pal); - pal.setBrush(QPalette::AlternateBase, pal.base().color().darker(104)); -} - -/*! - \internal - */ -QPixmap QWindowsVistaStyle::standardPixmap(StandardPixmap standardPixmap, const QStyleOption *option, - const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) { - return QWindowsStyle::standardPixmap(standardPixmap, option, widget); - } - return QWindowsXPStyle::standardPixmap(standardPixmap, option, widget); -} - -bool QWindowsVistaStylePrivate::transitionsEnabled() const -{ - BOOL animEnabled = false; - if (SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &animEnabled, 0)) - { - if (animEnabled) - return true; - } - return false; -} - -/*! -\reimp -*/ -QIcon QWindowsVistaStyle::standardIcon(StandardPixmap standardIcon, - const QStyleOption *option, - const QWidget *widget) const -{ - if (!QWindowsVistaStylePrivate::useVista()) { - return QWindowsStyle::standardIcon(standardIcon, option, widget); - } - - QWindowsVistaStylePrivate *d = const_cast<QWindowsVistaStylePrivate *>(d_func()); - switch(standardIcon) { - case SP_CommandLink: - { - XPThemeData theme(nullptr, nullptr, - QWindowsXPStylePrivate::ButtonTheme, - BP_COMMANDLINKGLYPH, CMDLGS_NORMAL); - if (theme.isValid()) { - const QSize size = theme.size().toSize(); - QIcon linkGlyph; - QPixmap pm(size); - pm.fill(Qt::transparent); - QPainter p(&pm); - theme.painter = &p; - theme.rect = QRect(QPoint(0, 0), size); - d->drawBackground(theme); - linkGlyph.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal - pm.fill(Qt::transparent); - - theme.stateId = CMDLGS_PRESSED; - d->drawBackground(theme); - linkGlyph.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed - pm.fill(Qt::transparent); - - theme.stateId = CMDLGS_HOT; - d->drawBackground(theme); - linkGlyph.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover - pm.fill(Qt::transparent); - - theme.stateId = CMDLGS_DISABLED; - d->drawBackground(theme); - linkGlyph.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled - return linkGlyph; - } - } - break; - default: - break; - } - return QWindowsXPStyle::standardIcon(standardIcon, option, widget); -} - -QT_END_NAMESPACE diff --git a/src/plugins/styles/windowsvista/qwindowsvistastyle_p_p.h b/src/plugins/styles/windowsvista/qwindowsvistastyle_p_p.h deleted file mode 100644 index 03aa4fdf9a..0000000000 --- a/src/plugins/styles/windowsvista/qwindowsvistastyle_p_p.h +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QWINDOWSVISTASTYLE_P_P_H -#define QWINDOWSVISTASTYLE_P_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of qapplication_*.cpp, qwidget*.cpp and qfiledialog.cpp. This header -// file may change from version to version without notice, or even be removed. -// -// We mean it. -// - -#include <QtWidgets/private/qtwidgetsglobal_p.h> -#include "qwindowsvistastyle_p.h" -#include "qwindowsxpstyle_p_p.h" -#include <private/qstyleanimation_p.h> -#include <private/qpaintengine_raster_p.h> -#include <qpaintengine.h> -#include <qwidget.h> -#include <qapplication.h> -#include <qpixmapcache.h> -#include <qstyleoption.h> -#if QT_CONFIG(pushbutton) -#include <qpushbutton.h> -#endif -#include <qradiobutton.h> -#if QT_CONFIG(lineedit) -#include <qlineedit.h> -#endif -#include <qgroupbox.h> -#if QT_CONFIG(toolbutton) -#include <qtoolbutton.h> -#endif -#if QT_CONFIG(spinbox) -#include <qspinbox.h> -#endif -#if QT_CONFIG(toolbar) -#include <qtoolbar.h> -#endif -#if QT_CONFIG(combobox) -#include <qcombobox.h> -#endif -#if QT_CONFIG(scrollbar) -#include <qscrollbar.h> -#endif -#if QT_CONFIG(progressbar) -#include <qprogressbar.h> -#endif -#if QT_CONFIG(dockwidget) -#include <qdockwidget.h> -#endif -#if QT_CONFIG(listview) -#include <qlistview.h> -#endif -#if QT_CONFIG(treeview) -#include <qtreeview.h> -#endif -#include <qtextedit.h> -#include <qmessagebox.h> -#if QT_CONFIG(dialogbuttonbox) -#include <qdialogbuttonbox.h> -#endif -#include <qinputdialog.h> -#if QT_CONFIG(tableview) -#include <qtableview.h> -#endif -#include <qdatetime.h> -#if QT_CONFIG(commandlinkbutton) -#include <qcommandlinkbutton.h> -#endif - -QT_BEGIN_NAMESPACE - -#if !defined(SCHEMA_VERIFY_VSSYM32) -#define TMT_ANIMATIONDURATION 5006 -#define TMT_TRANSITIONDURATIONS 6000 -#define EP_EDITBORDER_NOSCROLL 6 -#define EP_EDITBORDER_HVSCROLL 9 -#define EP_BACKGROUND 3 -#define EBS_NORMAL 1 -#define EBS_HOT 2 -#define EBS_DISABLED 3 -#define EBS_READONLY 5 -#define PBS_DEFAULTED_ANIMATING 6 -#define MBI_NORMAL 1 -#define MBI_HOT 2 -#define MBI_PUSHED 3 -#define MBI_DISABLED 4 -#define MB_ACTIVE 1 -#define MB_INACTIVE 2 -#define PP_FILL 5 -#define PP_FILLVERT 6 -#define PP_MOVEOVERLAY 8 -#define PP_MOVEOVERLAYVERT 10 -#define MENU_BARBACKGROUND 7 -#define MENU_BARITEM 8 -#define MENU_POPUPCHECK 11 -#define MENU_POPUPCHECKBACKGROUND 12 -#define MENU_POPUPGUTTER 13 -#define MENU_POPUPITEM 14 -#define MENU_POPUPBORDERS 10 -#define MENU_POPUPSEPARATOR 15 -#define MC_CHECKMARKNORMAL 1 -#define MC_CHECKMARKDISABLED 2 -#define MC_BULLETNORMAL 3 -#define MC_BULLETDISABLED 4 -#define ABS_UPHOVER 17 -#define ABS_DOWNHOVER 18 -#define ABS_LEFTHOVER 19 -#define ABS_RIGHTHOVER 20 -#define CP_DROPDOWNBUTTONRIGHT 6 -#define CP_DROPDOWNBUTTONLEFT 7 -#define SCRBS_HOVER 5 -#define TVP_HOTGLYPH 4 -#define SPI_GETCLIENTAREAANIMATION 0x1042 -#define TDLG_PRIMARYPANEL 1 -#define TDLG_SECONDARYPANEL 8 -#endif - -class QWindowsVistaAnimation : public QBlendStyleAnimation -{ - Q_OBJECT -public: - QWindowsVistaAnimation(Type type, QObject *target) : QBlendStyleAnimation(type, target) { } - - bool isUpdateNeeded() const override; - void paint(QPainter *painter, const QStyleOption *option); -}; - - -// Handles state transition animations -class QWindowsVistaTransition : public QWindowsVistaAnimation -{ - Q_OBJECT -public: - QWindowsVistaTransition(QObject *target) : QWindowsVistaAnimation(Transition, target) {} -}; - - -// Handles pulse animations (default buttons) -class QWindowsVistaPulse: public QWindowsVistaAnimation -{ - Q_OBJECT -public: - QWindowsVistaPulse(QObject *target) : QWindowsVistaAnimation(Pulse, target) {} -}; - - -class QWindowsVistaStylePrivate : public QWindowsXPStylePrivate -{ - Q_DECLARE_PUBLIC(QWindowsVistaStyle) - -public: - static int fixedPixelMetric(QStyle::PixelMetric pm); - static inline bool useVista(); - QTime animationTime() const; - bool transitionsEnabled() const; -}; - -QT_END_NAMESPACE - -#endif // QWINDOWSVISTASTYLE_P_P_H diff --git a/src/plugins/styles/windowsvista/qwindowsxpstyle.cpp b/src/plugins/styles/windowsvista/qwindowsxpstyle.cpp deleted file mode 100644 index 1b80f66af2..0000000000 --- a/src/plugins/styles/windowsvista/qwindowsxpstyle.cpp +++ /dev/null @@ -1,3669 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#include "qwindowsxpstyle_p.h" -#include "qwindowsxpstyle_p_p.h" - -#include <private/qobject_p.h> -#include <private/qpaintengine_raster_p.h> -#include <private/qapplication_p.h> -#include <private/qstylehelper_p.h> -#include <private/qwidget_p.h> -#include <qpainter.h> -#include <qpaintengine.h> -#include <qwidget.h> -#include <qbackingstore.h> -#include <qapplication.h> -#include <qpixmapcache.h> -#include <private/qapplication_p.h> -#include <qpa/qplatformintegration.h> - -#if QT_CONFIG(toolbutton) -#include <qtoolbutton.h> -#endif -#if QT_CONFIG(tabbar) -#include <qtabbar.h> -#endif -#if QT_CONFIG(combobox) -#include <qcombobox.h> -#endif -#if QT_CONFIG(scrollbar) -#include <qscrollbar.h> -#endif -#include <qheaderview.h> -#if QT_CONFIG(spinbox) -#include <qspinbox.h> -#endif -#if QT_CONFIG(listview) -#include <qlistview.h> -#endif -#if QT_CONFIG(stackedwidget) -#include <qstackedwidget.h> -#endif -#if QT_CONFIG(pushbutton) -#include <qpushbutton.h> -#endif -#if QT_CONFIG(toolbar) -#include <qtoolbar.h> -#endif -#include <qlabel.h> -#include <qvarlengtharray.h> -#include <qdebug.h> - -#include <algorithm> - -QT_BEGIN_NAMESPACE - -// General const values -static const int windowsItemFrame = 2; // menu item frame width -static const int windowsItemHMargin = 3; // menu item hor text margin -static const int windowsItemVMargin = 0; // menu item ver text margin -static const int windowsArrowHMargin = 6; // arrow horizontal margin -static const int windowsRightBorder = 12; // right border on windows - -// Theme names matching the QWindowsXPStylePrivate::Theme enumeration. -static const wchar_t *themeNames[QWindowsXPStylePrivate::NThemes] = -{ - L"BUTTON", L"COMBOBOX", L"EDIT", L"HEADER", L"LISTVIEW", - L"MENU", L"PROGRESS", L"REBAR", L"SCROLLBAR", L"SPIN", - L"TAB", L"TASKDIALOG", L"TOOLBAR", L"TOOLTIP", L"TRACKBAR", - L"TREEVIEW", L"WINDOW", L"STATUS", L"TREEVIEW" -}; - -static inline QBackingStore *backingStoreForWidget(const QWidget *widget) -{ - if (QBackingStore *backingStore = widget->backingStore()) - return backingStore; - if (const QWidget *topLevel = widget->nativeParentWidget()) - if (QBackingStore *topLevelBackingStore = topLevel->backingStore()) - return topLevelBackingStore; - return nullptr; -} - -static inline HDC hdcForWidgetBackingStore(const QWidget *widget) -{ - if (QBackingStore *backingStore = backingStoreForWidget(widget)) { - QPlatformNativeInterface *nativeInterface = QGuiApplication::platformNativeInterface(); - if (nativeInterface) - return static_cast<HDC>(nativeInterface->nativeResourceForBackingStore(QByteArrayLiteral("getDC"), backingStore)); - } - return nullptr; -} - -// Theme data helper ------------------------------------------------------------------------------ -/* \internal - Returns \c true if the themedata is valid for use. -*/ -bool XPThemeData::isValid() -{ - return QWindowsXPStylePrivate::useXP() && theme >= 0 && handle(); -} - - -/* \internal - Returns the theme engine handle to the specific class. - If the handle hasn't been opened before, it opens the data, and - adds it to a static map, for caching. -*/ -HTHEME XPThemeData::handle() -{ - if (!QWindowsXPStylePrivate::useXP()) - return nullptr; - - if (!htheme) - htheme = QWindowsXPStylePrivate::createTheme(theme, QWindowsXPStylePrivate::winId(widget)); - return htheme; -} - -/* \internal - Converts a QRect to the native RECT structure. -*/ -RECT XPThemeData::toRECT(const QRect &qr) -{ - RECT r; - r.left = qr.x(); - r.right = qr.x() + qr.width(); - r.top = qr.y(); - r.bottom = qr.y() + qr.height(); - return r; -} - -/* \internal - Returns the native region of a part, if the part is considered - transparent. The region is scaled to the parts size (rect). -*/ -HRGN XPThemeData::mask(QWidget *widget) -{ - if (!IsThemeBackgroundPartiallyTransparent(handle(), partId, stateId)) - return nullptr; - - HRGN hrgn; - HDC dc = nullptr; - if (widget) - dc = hdcForWidgetBackingStore(widget); - RECT nativeRect = toRECT(rect); - GetThemeBackgroundRegion(handle(), dc, partId, stateId, &nativeRect, &hrgn); - return hrgn; -} - -// QWindowsXPStylePrivate ------------------------------------------------------------------------- -// Static initializations -HWND QWindowsXPStylePrivate::m_vistaTreeViewHelper = nullptr; -HTHEME QWindowsXPStylePrivate::m_themes[NThemes]; -bool QWindowsXPStylePrivate::use_xp = false; -QBasicAtomicInt QWindowsXPStylePrivate::ref = Q_BASIC_ATOMIC_INITIALIZER(-1); // -1 based refcounting - -static void qt_add_rect(HRGN &winRegion, QRect r) -{ - HRGN rgn = CreateRectRgn(r.left(), r.top(), r.x() + r.width(), r.y() + r.height()); - if (rgn) { - HRGN dest = CreateRectRgn(0,0,0,0); - int result = CombineRgn(dest, winRegion, rgn, RGN_OR); - if (result) { - DeleteObject(winRegion); - winRegion = dest; - } - DeleteObject(rgn); - } -} - -static HRGN qt_hrgn_from_qregion(const QRegion ®ion) -{ - HRGN hRegion = CreateRectRgn(0,0,0,0); - if (region.rectCount() == 1) { - qt_add_rect(hRegion, region.boundingRect()); - return hRegion; - } - for (const QRect &rect : region) - qt_add_rect(hRegion, rect); - return hRegion; -} - -/* \internal - Checks if the theme engine can/should be used, or if we should fall back - to Windows style. For Windows 10, this will still return false for the - High Contrast themes. -*/ -bool QWindowsXPStylePrivate::useXP(bool update) -{ - if (update) { - use_xp = IsThemeActive() && (IsAppThemed() || !QCoreApplication::instance()) - && !QWindowsStylePrivate::isDarkMode(); - } - return use_xp; -} - -/* \internal - Handles refcounting, and queries the theme engine for usage. -*/ -void QWindowsXPStylePrivate::init(bool force) -{ - if (ref.ref() && !force) - return; - if (!force) // -1 based atomic refcounting - ref.ref(); - - useXP(true); - std::fill(m_themes, m_themes + NThemes, nullptr); -} - -/* \internal - Cleans up all static data. -*/ -void QWindowsXPStylePrivate::cleanup(bool force) -{ - if (bufferBitmap) { - if (bufferDC && nullBitmap) - SelectObject(bufferDC, nullBitmap); - DeleteObject(bufferBitmap); - bufferBitmap = nullptr; - } - - if (bufferDC) - DeleteDC(bufferDC); - bufferDC = nullptr; - - if (ref.deref() && !force) - return; - if (!force) // -1 based atomic refcounting - ref.deref(); - - use_xp = false; - cleanupHandleMap(); -} - -/* In order to obtain the correct VistaTreeViewTheme (arrows for PE_IndicatorBranch), - * we need to set the windows "explorer" theme explicitly on a native - * window and open the "TREEVIEW" theme handle passing its window handle - * in order to get Vista-style item view themes (particularly drawBackground() - * for selected items needs this). - * We invoke a service of the native Windows interface to create - * a non-visible window handle, open the theme on it and insert it into - * the cache so that it is found by XPThemeData::handle() first. - */ - -static inline HWND createTreeViewHelperWindow() -{ - using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; - - HWND result = nullptr; - if (auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration())) - result = nativeWindowsApp->createMessageWindow(QStringLiteral("QTreeViewThemeHelperWindowClass"), - QStringLiteral("QTreeViewThemeHelperWindow")); - return result; -} - -bool QWindowsXPStylePrivate::initVistaTreeViewTheming() -{ - if (m_vistaTreeViewHelper) - return true; - - m_vistaTreeViewHelper = createTreeViewHelperWindow(); - if (!m_vistaTreeViewHelper) { - qWarning("Unable to create the treeview helper window."); - return false; - } - if (FAILED(SetWindowTheme(m_vistaTreeViewHelper, L"explorer", nullptr))) { - qErrnoWarning("SetWindowTheme() failed."); - cleanupVistaTreeViewTheming(); - return false; - } - return true; -} - -void QWindowsXPStylePrivate::cleanupVistaTreeViewTheming() -{ - if (m_vistaTreeViewHelper) { - DestroyWindow(m_vistaTreeViewHelper); - m_vistaTreeViewHelper = nullptr; - } -} - -/* \internal - Closes all open theme data handles to ensure that we don't leak - resources, and that we don't refer to old handles when for - example the user changes the theme style. -*/ -void QWindowsXPStylePrivate::cleanupHandleMap() -{ - for (auto &theme : m_themes) { - if (theme) { - CloseThemeData(theme); - theme = nullptr; - } - } - QWindowsXPStylePrivate::cleanupVistaTreeViewTheming(); -} - -HTHEME QWindowsXPStylePrivate::createTheme(int theme, HWND hwnd) -{ - if (Q_UNLIKELY(theme < 0 || theme >= NThemes || !hwnd)) { - qWarning("Invalid parameters #%d, %p", theme, hwnd); - return nullptr; - } - if (!m_themes[theme]) { - const wchar_t *name = themeNames[theme]; - if (theme == VistaTreeViewTheme && QWindowsXPStylePrivate::initVistaTreeViewTheming()) - hwnd = QWindowsXPStylePrivate::m_vistaTreeViewHelper; - m_themes[theme] = OpenThemeData(hwnd, name); - if (Q_UNLIKELY(!m_themes[theme])) - qErrnoWarning("OpenThemeData() failed for theme %d (%s).", - theme, qPrintable(themeName(theme))); - } - return m_themes[theme]; -} - -QString QWindowsXPStylePrivate::themeName(int theme) -{ - return theme >= 0 && theme < NThemes - ? QString::fromWCharArray(themeNames[theme]) : QString(); -} - -bool QWindowsXPStylePrivate::isItemViewDelegateLineEdit(const QWidget *widget) -{ - if (!widget) - return false; - const QWidget *parent1 = widget->parentWidget(); - // Exclude dialogs or other toplevels parented on item views. - if (!parent1 || parent1->isWindow()) - return false; - const QWidget *parent2 = parent1->parentWidget(); - return parent2 && widget->inherits("QLineEdit") - && parent2->inherits("QAbstractItemView"); -} - -// Returns whether base color is set for this widget -bool QWindowsXPStylePrivate::isLineEditBaseColorSet(const QStyleOption *option, const QWidget *widget) -{ - uint resolveMask = option->palette.resolveMask(); - if (widget) { - // Since spin box includes a line edit we need to resolve the palette mask also from - // the parent, as while the color is always correct on the palette supplied by panel, - // the mask can still be empty. If either mask specifies custom base color, use that. -#if QT_CONFIG(spinbox) - if (const QAbstractSpinBox *spinbox = qobject_cast<QAbstractSpinBox*>(widget->parentWidget())) - resolveMask |= spinbox->palette().resolveMask(); -#endif // QT_CONFIG(spinbox) - } - return (resolveMask & (1 << QPalette::Base)) != 0; -} - -static inline Qt::Orientation progressBarOrientation(const QStyleOption *option = nullptr) -{ - if (const auto *pb = qstyleoption_cast<const QStyleOptionProgressBar *>(option)) - return pb->state & QStyle::State_Horizontal ? Qt::Horizontal : Qt::Vertical; - return Qt::Horizontal; -} - -/*! \internal - This function will always return a valid window handle, and might - create a limbo widget to do so. - We often need a window handle to for example open theme data, so - this function ensures that we get one. -*/ -HWND QWindowsXPStylePrivate::winId(const QWidget *widget) -{ - if (widget) { - if (const HWND hwnd = QApplicationPrivate::getHWNDForWidget(const_cast<QWidget *>(widget))) - return hwnd; - } - - // Find top level with native window (there might be dialogs that do not have one). - const auto allWindows = QGuiApplication::allWindows(); - for (const QWindow *window : allWindows) { - if (window->isTopLevel() && window->type() != Qt::Desktop && window->handle() != nullptr) - return reinterpret_cast<HWND>(window->winId()); - } - - return GetDesktopWindow(); -} - -/*! \internal - Returns a native buffer (DIB section) of at least the size of - ( \a x , \a y ). The buffer has a 32 bit depth, to not lose - the alpha values on proper alpha-pixmaps. -*/ -HBITMAP QWindowsXPStylePrivate::buffer(int w, int h) -{ - // If we already have a HBITMAP which is of adequate size, just return that - if (bufferBitmap) { - if (bufferW >= w && bufferH >= h) - return bufferBitmap; - // Not big enough, discard the old one - if (bufferDC && nullBitmap) - SelectObject(bufferDC, nullBitmap); - DeleteObject(bufferBitmap); - bufferBitmap = nullptr; - } - - w = qMax(bufferW, w); - h = qMax(bufferH, h); - - if (!bufferDC) { - HDC displayDC = GetDC(nullptr); - bufferDC = CreateCompatibleDC(displayDC); - ReleaseDC(nullptr, displayDC); - } - - // Define the header - BITMAPINFO bmi; - memset(&bmi, 0, sizeof(bmi)); - bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bmi.bmiHeader.biWidth = w; - bmi.bmiHeader.biHeight = -h; - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; - - // Create the pixmap - bufferPixels = nullptr; - bufferBitmap = CreateDIBSection(bufferDC, &bmi, DIB_RGB_COLORS, reinterpret_cast<void **>(&bufferPixels), nullptr, 0); - GdiFlush(); - nullBitmap = static_cast<HBITMAP>(SelectObject(bufferDC, bufferBitmap)); - - if (Q_UNLIKELY(!bufferBitmap)) { - qErrnoWarning("QWindowsXPStylePrivate::buffer(%dx%d), CreateDIBSection() failed.", w, h); - bufferW = 0; - bufferH = 0; - return nullptr; - } - if (Q_UNLIKELY(!bufferPixels)) { - qErrnoWarning("QWindowsXPStylePrivate::buffer(%dx%d), CreateDIBSection() did not allocate pixel data.", w, h); - bufferW = 0; - bufferH = 0; - return nullptr; - } - bufferW = w; - bufferH = h; -#ifdef DEBUG_XP_STYLE - qDebug("Creating new dib section (%d, %d)", w, h); -#endif - return bufferBitmap; -} - -/*! \internal - Returns \c true if the part contains any transparency at all. This does - not indicate what kind of transparency we're dealing with. It can be - - Alpha transparency - - Masked transparency -*/ -bool QWindowsXPStylePrivate::isTransparent(XPThemeData &themeData) -{ - return IsThemeBackgroundPartiallyTransparent(themeData.handle(), themeData.partId, - themeData.stateId); -} - - -/*! \internal - Returns a QRegion of the region of the part -*/ -QRegion QWindowsXPStylePrivate::region(XPThemeData &themeData) -{ - HRGN hRgn = nullptr; - const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(themeData.widget); - RECT rect = themeData.toRECT(QRect(themeData.rect.topLeft() / factor, themeData.rect.size() / factor)); - if (!SUCCEEDED(GetThemeBackgroundRegion(themeData.handle(), bufferHDC(), themeData.partId, - themeData.stateId, &rect, &hRgn))) { - return QRegion(); - } - - HRGN dest = CreateRectRgn(0, 0, 0, 0); - const bool success = CombineRgn(dest, hRgn, nullptr, RGN_COPY) != ERROR; - - QRegion region; - - if (success) { - const auto numBytes = GetRegionData(dest, 0, nullptr); - if (numBytes == 0) - return QRegion(); - - char *buf = new (std::nothrow) char[numBytes]; - if (!buf) - return QRegion(); - - RGNDATA *rd = reinterpret_cast<RGNDATA*>(buf); - if (GetRegionData(dest, numBytes, rd) == 0) { - delete [] buf; - return QRegion(); - } - - RECT *r = reinterpret_cast<RECT*>(rd->Buffer); - for (uint i = 0; i < rd->rdh.nCount; ++i) { - QRect rect; - rect.setCoords(int(r->left * factor), int(r->top * factor), int((r->right - 1) * factor), int((r->bottom - 1) * factor)); - ++r; - region |= rect; - } - - delete [] buf; - } - - DeleteObject(hRgn); - DeleteObject(dest); - - return region; -} - -/*! \internal - Returns \c true if the native doublebuffer contains pixels with - varying alpha value. -*/ -bool QWindowsXPStylePrivate::hasAlphaChannel(const QRect &rect) -{ - const int startX = rect.left(); - const int startY = rect.top(); - const int w = rect.width(); - const int h = rect.height(); - - int firstAlpha = -1; - for (int y = startY; y < h/2; ++y) { - auto buffer = reinterpret_cast<const DWORD *>(bufferPixels) + (y * bufferW); - for (int x = startX; x < w; ++x, ++buffer) { - int alpha = (*buffer) >> 24; - if (firstAlpha == -1) - firstAlpha = alpha; - else if (alpha != firstAlpha) - return true; - } - } - return false; -} - -/*! \internal - When the theme engine paints both a true alpha pixmap and a glyph - into our buffer, the glyph might not contain a proper alpha value. - The rule of thumb for premultiplied pixmaps is that the color - values of a pixel can never be higher than the alpha values, so - we use this to our advantage here, and fix all instances where - this occurs. -*/ -bool QWindowsXPStylePrivate::fixAlphaChannel(const QRect &rect) -{ - const int startX = rect.left(); - const int startY = rect.top(); - const int w = rect.width(); - const int h = rect.height(); - bool hasFixedAlphaValue = false; - - for (int y = startY; y < h; ++y) { - auto buffer = reinterpret_cast<DWORD *>(bufferPixels) + (y * bufferW); - for (int x = startX; x < w; ++x, ++buffer) { - uint pixel = *buffer; - int alpha = qAlpha(pixel); - if (qRed(pixel) > alpha || qGreen(pixel) > alpha || qBlue(pixel) > alpha) { - *buffer |= 0xff000000; - hasFixedAlphaValue = true; - } - } - } - return hasFixedAlphaValue; -} - -/*! \internal - Swaps the alpha values on certain pixels: - 0xFF?????? -> 0x00?????? - 0x00?????? -> 0xFF?????? - Used to determine the mask of a non-alpha transparent pixmap in - the native doublebuffer, and swap the alphas so we may paint - the image as a Premultiplied QImage with drawImage(), and obtain - the mask transparency. -*/ -bool QWindowsXPStylePrivate::swapAlphaChannel(const QRect &rect, bool allPixels) -{ - const int startX = rect.left(); - const int startY = rect.top(); - const int w = rect.width(); - const int h = rect.height(); - bool valueChange = false; - - // Flip the alphas, so that 255-alpha pixels are 0, and 0-alpha are 255. - for (int y = startY; y < h; ++y) { - auto buffer = reinterpret_cast<DWORD *>(bufferPixels) + (y * bufferW); - for (int x = startX; x < w; ++x, ++buffer) { - if (allPixels) { - *buffer |= 0xFF000000; - continue; - } - unsigned int alphaValue = (*buffer) & 0xFF000000; - if (alphaValue == 0xFF000000) { - *buffer = 0; - valueChange = true; - } else if (alphaValue == 0) { - *buffer |= 0xFF000000; - valueChange = true; - } - } - } - return valueChange; -} - -enum TransformType { SimpleTransform, HighDpiScalingTransform, ComplexTransform }; - -static inline TransformType transformType(const QTransform &transform, qreal devicePixelRatio) -{ - if (transform.type() <= QTransform::TxTranslate) - return SimpleTransform; - if (transform.type() > QTransform::TxScale) - return ComplexTransform; - return qFuzzyCompare(transform.m11(), devicePixelRatio) - && qFuzzyCompare(transform.m22(), devicePixelRatio) - ? HighDpiScalingTransform : ComplexTransform; -} - -// QTBUG-60571: Exclude known fully opaque theme parts which produce values -// invalid in ARGB32_Premultiplied (for example, 0x00ffffff). -static inline bool isFullyOpaque(const XPThemeData &themeData) -{ - return themeData.theme == QWindowsXPStylePrivate::TaskDialogTheme && themeData.partId == TDLG_PRIMARYPANEL; -} - -/*! \internal - Main theme drawing function. - Determines the correct lowlevel drawing method depending on several - factors. - Use drawBackgroundThruNativeBuffer() if: - - Painter does not have an HDC - - Theme part is flipped (mirrored horizontally) - else use drawBackgroundDirectly(). - \note drawBackgroundThruNativeBuffer() can return false for large - sizes due to buffer()/CreateDIBSection() failing. -*/ -bool QWindowsXPStylePrivate::drawBackground(XPThemeData &themeData, qreal correctionFactor) -{ - if (themeData.rect.isEmpty()) - return true; - - QPainter *painter = themeData.painter; - Q_ASSERT_X(painter != nullptr, "QWindowsXPStylePrivate::drawBackground()", "Trying to draw a theme part without a painter"); - if (!painter || !painter->isActive()) - return false; - - painter->save(); - - // Access paintDevice via engine since the painter may - // return the clip device which can still be a widget device in case of grabWidget(). - - bool translucentToplevel = false; - const QPaintDevice *paintDevice = painter->device(); - const qreal aditionalDevicePixelRatio = themeData.widget ? themeData.widget->devicePixelRatio() : qreal(1); - if (paintDevice->devType() == QInternal::Widget) { - const QWidget *window = static_cast<const QWidget *>(paintDevice)->window(); - translucentToplevel = window->testAttribute(Qt::WA_TranslucentBackground); - } - - const TransformType tt = transformType(painter->deviceTransform(), aditionalDevicePixelRatio); - - bool canDrawDirectly = false; - if (themeData.widget && painter->opacity() == 1.0 && !themeData.rotate - && !isFullyOpaque(themeData) - && tt != ComplexTransform && !themeData.mirrorVertically - && !translucentToplevel) { - // Draw on backing store DC only for real widgets or backing store images. - const QPaintDevice *enginePaintDevice = painter->paintEngine()->paintDevice(); - switch (enginePaintDevice->devType()) { - case QInternal::Widget: - canDrawDirectly = true; - break; - case QInternal::Image: - // Ensure the backing store has received as resize and is initialized. - if (QBackingStore *bs = backingStoreForWidget(themeData.widget)) { - if (bs->size().isValid() && bs->paintDevice() == enginePaintDevice) - canDrawDirectly = true; - } - } - } - - const HDC dc = canDrawDirectly ? hdcForWidgetBackingStore(themeData.widget) : nullptr; - const bool result = dc && qFuzzyCompare(correctionFactor, qreal(1)) - ? drawBackgroundDirectly(dc, themeData, aditionalDevicePixelRatio) - : drawBackgroundThruNativeBuffer(themeData, aditionalDevicePixelRatio, correctionFactor); - painter->restore(); - return result; -} - -static inline QRectF scaleRect(const QRectF &r, qreal factor) -{ - return r.isValid() && factor > 1 - ? QRectF(r.topLeft() * factor, r.size() * factor) : r; -} - -static QRegion scaleRegion(const QRegion ®ion, qreal factor) -{ - if (region.isEmpty() || qFuzzyCompare(factor, qreal(1))) - return region; - QRegion result; - for (const QRect &rect : region) - result += QRectF(QPointF(rect.topLeft()) * factor, QSizeF(rect.size() * factor)).toRect(); - return result; -} - -/*! \internal - This function draws the theme parts directly to the paintengines HDC. - Do not use this if you need to perform other transformations on the - resulting data. -*/ -bool QWindowsXPStylePrivate::drawBackgroundDirectly(HDC dc, XPThemeData &themeData, qreal additionalDevicePixelRatio) -{ - QPainter *painter = themeData.painter; - - const auto &deviceTransform = painter->deviceTransform(); - const QPointF redirectionDelta(deviceTransform.dx(), deviceTransform.dy()); - const QRect area = scaleRect(QRectF(themeData.rect), additionalDevicePixelRatio).translated(redirectionDelta).toRect(); - - QRegion sysRgn = painter->paintEngine()->systemClip(); - if (sysRgn.isEmpty()) - sysRgn = area; - else - sysRgn &= area; - if (painter->hasClipping()) - sysRgn &= scaleRegion(painter->clipRegion(), additionalDevicePixelRatio).translated(redirectionDelta.toPoint()); - HRGN hrgn = qt_hrgn_from_qregion(sysRgn); - SelectClipRgn(dc, hrgn); - -#ifdef DEBUG_XP_STYLE - printf("---[ DIRECT PAINTING ]------------------> Name(%-10s) Part(%d) State(%d)\n", - qPrintable(themeData.name), themeData.partId, themeData.stateId); - showProperties(themeData); -#endif - - RECT drawRECT = themeData.toRECT(area); - DTBGOPTS drawOptions; - memset(&drawOptions, 0, sizeof(drawOptions)); - drawOptions.dwSize = sizeof(drawOptions); - drawOptions.rcClip = themeData.toRECT(sysRgn.boundingRect()); - drawOptions.dwFlags = DTBG_CLIPRECT - | (themeData.noBorder ? DTBG_OMITBORDER : 0) - | (themeData.noContent ? DTBG_OMITCONTENT : 0) - | (themeData.mirrorHorizontally ? DTBG_MIRRORDC : 0); - - const HRESULT result = DrawThemeBackgroundEx(themeData.handle(), dc, themeData.partId, themeData.stateId, &(drawRECT), &drawOptions); - SelectClipRgn(dc, nullptr); - DeleteObject(hrgn); - return SUCCEEDED(result); -} - -/*! \internal - This function uses a secondary Native doublebuffer for painting parts. - It should only be used when the painteengine doesn't provide a proper - HDC for direct painting (e.g. when doing a grabWidget(), painting to - other pixmaps etc), or when special transformations are needed (e.g. - flips (horizontal mirroring only, vertical are handled by the theme - engine). - - \a correctionFactor is an additional factor used to scale up controls - that are too small on High DPI screens, as has been observed for - WP_MDICLOSEBUTTON, WP_MDIRESTOREBUTTON, WP_MDIMINBUTTON (QTBUG-75927). -*/ -bool QWindowsXPStylePrivate::drawBackgroundThruNativeBuffer(XPThemeData &themeData, - qreal additionalDevicePixelRatio, - qreal correctionFactor) -{ - QPainter *painter = themeData.painter; - QRectF rectF = scaleRect(QRectF(themeData.rect), additionalDevicePixelRatio); - - if ((themeData.rotate + 90) % 180 == 0) { // Catch 90,270,etc.. degree flips. - rectF = QRectF(0, 0, rectF.height(), rectF.width()); - } - rectF.moveTo(0, 0); - - const bool hasCorrectionFactor = !qFuzzyCompare(correctionFactor, qreal(1)); - QRect rect = rectF.toRect(); - const QRect drawRect = hasCorrectionFactor - ? QRectF(rectF.topLeft() / correctionFactor, rectF.size() / correctionFactor).toRect() - : rect; - int partId = themeData.partId; - int stateId = themeData.stateId; - int w = rect.width(); - int h = rect.height(); - - // Values initialized later, either from cached values, or from function calls - AlphaChannelType alphaType = UnknownAlpha; - bool stateHasData = true; // We assume so; - bool hasAlpha = false; - bool partIsTransparent; - bool potentialInvalidAlpha; - - QString pixmapCacheKey = QStringLiteral("$qt_xp_"); - pixmapCacheKey.append(themeName(themeData.theme)); - pixmapCacheKey.append(QLatin1Char('p')); - pixmapCacheKey.append(QString::number(partId)); - pixmapCacheKey.append(QLatin1Char('s')); - pixmapCacheKey.append(QString::number(stateId)); - pixmapCacheKey.append(QLatin1Char('s')); - pixmapCacheKey.append(themeData.noBorder ? QLatin1Char('0') : QLatin1Char('1')); - pixmapCacheKey.append(QLatin1Char('b')); - pixmapCacheKey.append(themeData.noContent ? QLatin1Char('0') : QLatin1Char('1')); - pixmapCacheKey.append(QString::number(w)); - pixmapCacheKey.append(QLatin1Char('w')); - pixmapCacheKey.append(QString::number(h)); - pixmapCacheKey.append(QLatin1Char('h')); - pixmapCacheKey.append(QString::number(additionalDevicePixelRatio)); - pixmapCacheKey.append(QLatin1Char('d')); - if (hasCorrectionFactor) { - pixmapCacheKey.append(QLatin1Char('c')); - pixmapCacheKey.append(QString::number(correctionFactor)); - } - - QPixmap cachedPixmap; - ThemeMapKey key(themeData); - ThemeMapData data = alphaCache.value(key); - - bool haveCachedPixmap = false; - bool isCached = data.dataValid; - if (isCached) { - partIsTransparent = data.partIsTransparent; - hasAlpha = data.hasAlphaChannel; - alphaType = data.alphaType; - potentialInvalidAlpha = data.hadInvalidAlpha; - - haveCachedPixmap = QPixmapCache::find(pixmapCacheKey, &cachedPixmap); - -#ifdef DEBUG_XP_STYLE - char buf[25]; - ::sprintf(buf, "+ Pixmap(%3d, %3d) ]", w, h); - printf("---[ CACHED %s--------> Name(%-10s) Part(%d) State(%d)\n", - haveCachedPixmap ? buf : "]-------------------", - qPrintable(themeData.name), themeData.partId, themeData.stateId); -#endif - } else { - // Not cached, so get values from Theme Engine - BOOL tmt_borderonly = false; - COLORREF tmt_transparentcolor = 0x0; - PROPERTYORIGIN proporigin = PO_NOTFOUND; - GetThemeBool(themeData.handle(), themeData.partId, themeData.stateId, TMT_BORDERONLY, &tmt_borderonly); - GetThemeColor(themeData.handle(), themeData.partId, themeData.stateId, TMT_TRANSPARENTCOLOR, &tmt_transparentcolor); - GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, TMT_CAPTIONMARGINS, &proporigin); - - partIsTransparent = isTransparent(themeData); - - potentialInvalidAlpha = false; - GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, TMT_GLYPHTYPE, &proporigin); - if (proporigin == PO_PART || proporigin == PO_STATE) { - int tmt_glyphtype = GT_NONE; - GetThemeEnumValue(themeData.handle(), themeData.partId, themeData.stateId, TMT_GLYPHTYPE, &tmt_glyphtype); - potentialInvalidAlpha = partIsTransparent && tmt_glyphtype == GT_IMAGEGLYPH; - } - -#ifdef DEBUG_XP_STYLE - printf("---[ NOT CACHED ]-----------------------> Name(%-10s) Part(%d) State(%d)\n", - qPrintable(themeData.name), themeData.partId, themeData.stateId); - printf("-->partIsTransparen = %d\n", partIsTransparent); - printf("-->potentialInvalidAlpha = %d\n", potentialInvalidAlpha); - showProperties(themeData); -#endif - } - bool wasAlphaSwapped = false; - bool wasAlphaFixed = false; - - // OLD PSDK Workaround ------------------------------------------------------------------------ - // See if we need extra clipping for the older PSDK, which does - // not have a DrawThemeBackgroundEx function for DTGB_OMITBORDER - // and DTGB_OMITCONTENT - bool addBorderContentClipping = false; - QRegion extraClip; - QRect area = drawRect; - if (themeData.noBorder || themeData.noContent) { - extraClip = area; - // We are running on a system where the uxtheme.dll does not have - // the DrawThemeBackgroundEx function, so we need to clip away - // borders or contents manually. - - int borderSize = 0; - PROPERTYORIGIN origin = PO_NOTFOUND; - GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, TMT_BORDERSIZE, &origin); - GetThemeInt(themeData.handle(), themeData.partId, themeData.stateId, TMT_BORDERSIZE, &borderSize); - borderSize *= additionalDevicePixelRatio; - - // Clip away border region - if ((origin == PO_CLASS || origin == PO_PART || origin == PO_STATE) && borderSize > 0) { - if (themeData.noBorder) { - extraClip &= area; - area = area.adjusted(-borderSize, -borderSize, borderSize, borderSize); - } - - // Clip away content region - if (themeData.noContent) { - QRegion content = area.adjusted(borderSize, borderSize, -borderSize, -borderSize); - extraClip ^= content; - } - } - addBorderContentClipping = (themeData.noBorder | themeData.noContent); - } - - QImage img; - if (!haveCachedPixmap) { // If the pixmap is not cached, generate it! ------------------------- - if (!buffer(drawRect.width(), drawRect.height())) // Ensure a buffer of at least (w, h) in size - return false; - HDC dc = bufferHDC(); - - // Clear the buffer - if (alphaType != NoAlpha) { - // Consider have separate "memset" function for small chunks for more speedup - memset(bufferPixels, 0x00, bufferW * drawRect.height() * 4); - } - - // Difference between area and rect - int dx = area.x() - drawRect.x(); - int dy = area.y() - drawRect.y(); - - // Adjust so painting rect starts from Origo - rect.moveTo(0,0); - area.moveTo(dx,dy); - DTBGOPTS drawOptions; - drawOptions.dwSize = sizeof(drawOptions); - drawOptions.rcClip = themeData.toRECT(rect); - drawOptions.dwFlags = DTBG_CLIPRECT - | (themeData.noBorder ? DTBG_OMITBORDER : 0) - | (themeData.noContent ? DTBG_OMITCONTENT : 0); - - // Drawing the part into the backing store - RECT wRect(themeData.toRECT(area)); - DrawThemeBackgroundEx(themeData.handle(), dc, themeData.partId, themeData.stateId, &wRect, &drawOptions); - - // If not cached, analyze the buffer data to figure - // out alpha type, and if it contains data - if (!isCached) { - // SHORTCUT: If the part's state has no data, cache it for NOOP later - if (!stateHasData) { - memset(static_cast<void *>(&data), 0, sizeof(data)); - data.dataValid = true; - alphaCache.insert(key, data); - return true; - } - hasAlpha = hasAlphaChannel(rect); - if (!hasAlpha && partIsTransparent) - potentialInvalidAlpha = true; -#if defined(DEBUG_XP_STYLE) && 1 - dumpNativeDIB(drawRect.width(), drawRect.height()); -#endif - } - - // Fix alpha values, if needed - if (potentialInvalidAlpha) - wasAlphaFixed = fixAlphaChannel(drawRect); - - QImage::Format format; - if ((partIsTransparent && !wasAlphaSwapped) || (!partIsTransparent && hasAlpha)) { - format = QImage::Format_ARGB32_Premultiplied; - alphaType = RealAlpha; - } else if (wasAlphaSwapped) { - format = QImage::Format_ARGB32_Premultiplied; - alphaType = MaskAlpha; - } else { - format = QImage::Format_RGB32; - // The image data we got from the theme engine does not have any transparency, - // thus the alpha channel is set to 0. - // However, Format_RGB32 requires the alpha part to be set to 0xff, thus - // we must flip it from 0x00 to 0xff - swapAlphaChannel(rect, true); - alphaType = NoAlpha; - } -#if defined(DEBUG_XP_STYLE) && 1 - printf("Image format is: %s\n", alphaType == RealAlpha ? "Real Alpha" : alphaType == MaskAlpha ? "Masked Alpha" : "No Alpha"); -#endif - img = QImage(bufferPixels, bufferW, bufferH, format); - if (hasCorrectionFactor) - img = img.scaled(img.size() * correctionFactor, Qt::KeepAspectRatio, Qt::SmoothTransformation); - img.setDevicePixelRatio(additionalDevicePixelRatio); - } - - // Blitting backing store - bool useRegion = partIsTransparent && !hasAlpha && !wasAlphaSwapped; - - QRegion newRegion; - QRegion oldRegion; - if (useRegion) { - newRegion = region(themeData); - oldRegion = painter->clipRegion(); - painter->setClipRegion(newRegion); -#if defined(DEBUG_XP_STYLE) && 0 - printf("Using region:\n"); - for (const QRect &r : newRegion) - printf(" (%d, %d, %d, %d)\n", r.x(), r.y(), r.right(), r.bottom()); -#endif - } - - if (addBorderContentClipping) - painter->setClipRegion(scaleRegion(extraClip, 1.0 / additionalDevicePixelRatio), Qt::IntersectClip); - - if (!themeData.mirrorHorizontally && !themeData.mirrorVertically && !themeData.rotate) { - if (!haveCachedPixmap) - painter->drawImage(themeData.rect, img, rect); - else - painter->drawPixmap(themeData.rect, cachedPixmap); - } else { - // This is _slow_! - // Make a copy containing only the necessary data, and mirror - // on all wanted axes. Then draw the copy. - // If cached, the normal pixmap is cached, instead of caching - // all possible orientations for each part and state. - QImage imgCopy; - if (!haveCachedPixmap) - imgCopy = img.copy(rect); - else - imgCopy = cachedPixmap.toImage(); - - if (themeData.rotate) { - QTransform rotMatrix; - rotMatrix.rotate(themeData.rotate); - imgCopy = imgCopy.transformed(rotMatrix); - } - if (themeData.mirrorHorizontally || themeData.mirrorVertically) - imgCopy = imgCopy.mirrored(themeData.mirrorHorizontally, themeData.mirrorVertically); - painter->drawImage(themeData.rect, imgCopy); - } - - if (useRegion || addBorderContentClipping) { - if (oldRegion.isEmpty()) - painter->setClipping(false); - else - painter->setClipRegion(oldRegion); - } - - // Cache the pixmap to avoid expensive swapAlphaChannel() calls - if (!haveCachedPixmap && w && h) { - QPixmap pix = QPixmap::fromImage(img).copy(rect); - QPixmapCache::insert(pixmapCacheKey, pix); -#ifdef DEBUG_XP_STYLE - printf("+++Adding pixmap to cache, size(%d, %d), wasAlphaSwapped(%d), wasAlphaFixed(%d), name(%s)\n", - w, h, wasAlphaSwapped, wasAlphaFixed, qPrintable(pixmapCacheKey)); -#endif - } - - // Add to theme part cache - if (!isCached) { - memset(static_cast<void *>(&data), 0, sizeof(data)); - data.dataValid = true; - data.partIsTransparent = partIsTransparent; - data.alphaType = alphaType; - data.hasAlphaChannel = hasAlpha; - data.wasAlphaSwapped = wasAlphaSwapped; - data.hadInvalidAlpha = wasAlphaFixed; - alphaCache.insert(key, data); - } - return true; -} - - -// ------------------------------------------------------------------------------------------------ - -/*! - \class QWindowsXPStyle - \brief The QWindowsXPStyle class provides a Microsoft Windows XP-like look and feel. - - \ingroup appearance - \inmodule QtWidgets - \internal - - \warning This style is only available on the Windows XP platform - because it makes use of Windows XP's style engine. - - Most of the functions are documented in the base classes - QWindowsStyle, QCommonStyle, and QStyle, but the - QWindowsXPStyle overloads of drawComplexControl(), drawControl(), - drawControlMask(), drawPrimitive(), proxy()->subControlRect(), and - sizeFromContents(), are documented here. - - \image qwindowsxpstyle.png - \sa QMacStyle, QWindowsStyle, QFusionStyle -*/ - -/*! - \internal - - Constructs a QWindowsXPStyle object. -*/ -QWindowsXPStyle::QWindowsXPStyle(QWindowsXPStylePrivate &dd) : QWindowsStyle(dd) -{ -} - -/*! - Destroys the style. -*/ -QWindowsXPStyle::~QWindowsXPStyle() = default; - -/*! \reimp */ -void QWindowsXPStyle::polish(QWidget *widget) -{ - QWindowsStyle::polish(widget); - if (!QWindowsXPStylePrivate::useXP()) - return; - - if (false -#if QT_CONFIG(abstractbutton) - || qobject_cast<QAbstractButton*>(widget) -#endif - || qobject_cast<QToolButton*>(widget) - || qobject_cast<QTabBar*>(widget) -#if QT_CONFIG(combobox) - || qobject_cast<QComboBox*>(widget) -#endif // QT_CONFIG(combobox) - || qobject_cast<QScrollBar*>(widget) - || qobject_cast<QSlider*>(widget) - || qobject_cast<QHeaderView*>(widget) -#if QT_CONFIG(spinbox) - || qobject_cast<QAbstractSpinBox*>(widget) - || qobject_cast<QSpinBox*>(widget) -#endif // QT_CONFIG(spinbox) - ) { - widget->setAttribute(Qt::WA_Hover); - } - -#if QT_CONFIG(rubberband) - if (qobject_cast<QRubberBand*>(widget)) - widget->setWindowOpacity(0.6); -#endif - - Q_D(QWindowsXPStyle); - if (!d->hasInitColors) { - // Get text color for group box labels - COLORREF cref; - XPThemeData theme(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme, 0, 0); - GetThemeColor(theme.handle(), BP_GROUPBOX, GBS_NORMAL, TMT_TEXTCOLOR, &cref); - d->groupBoxTextColor = qRgb(GetRValue(cref), GetGValue(cref), GetBValue(cref)); - GetThemeColor(theme.handle(), BP_GROUPBOX, GBS_DISABLED, TMT_TEXTCOLOR, &cref); - d->groupBoxTextColorDisabled = qRgb(GetRValue(cref), GetGValue(cref), GetBValue(cref)); - // Where does this color come from? - //GetThemeColor(theme.handle(), TKP_TICS, TSS_NORMAL, TMT_COLOR, &cref); - d->sliderTickColor = qRgb(165, 162, 148); - d->hasInitColors = true; - } -} - -/*! \reimp */ -void QWindowsXPStyle::polish(QPalette &pal) -{ - QWindowsStyle::polish(pal); - pal.setBrush(QPalette::AlternateBase, pal.base().color().darker(110)); -} - -/*! \reimp */ -void QWindowsXPStyle::unpolish(QWidget *widget) -{ -#if QT_CONFIG(rubberband) - if (qobject_cast<QRubberBand*>(widget)) - widget->setWindowOpacity(1.0); -#endif - Q_D(QWindowsXPStyle); - // Unpolish of widgets is the first thing that - // happens when a theme changes, or the theme - // engine is turned off. So we detect it here. - bool oldState = QWindowsXPStylePrivate::useXP(); - bool newState = QWindowsXPStylePrivate::useXP(true); - if ((oldState != newState) && newState) { - d->cleanup(true); - d->init(true); - } else { - // Cleanup handle map, if just changing style, - // or turning it on. In both cases the values - // already in the map might be old (other style). - d->cleanupHandleMap(); - } - if (false -#if QT_CONFIG(abstractbutton) - || qobject_cast<QAbstractButton*>(widget) -#endif - || qobject_cast<QToolButton*>(widget) - || qobject_cast<QTabBar*>(widget) -#if QT_CONFIG(combobox) - || qobject_cast<QComboBox*>(widget) -#endif // QT_CONFIG(combobox) - || qobject_cast<QScrollBar*>(widget) - || qobject_cast<QSlider*>(widget) - || qobject_cast<QHeaderView*>(widget) -#if QT_CONFIG(spinbox) - || qobject_cast<QAbstractSpinBox*>(widget) - || qobject_cast<QSpinBox*>(widget) -#endif // QT_CONFIG(spinbox) - ) { - widget->setAttribute(Qt::WA_Hover, false); - } - QWindowsStyle::unpolish(widget); -} - -/*! \reimp */ -QRect QWindowsXPStyle::subElementRect(SubElement sr, const QStyleOption *option, const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::subElementRect(sr, option, widget); - - QRect rect(option->rect); - switch(sr) { - case SE_DockWidgetCloseButton: - case SE_DockWidgetFloatButton: - rect = QWindowsStyle::subElementRect(sr, option, widget); - return rect.translated(0, 1); - break; - case SE_TabWidgetTabContents: - if (qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option)) { - rect = QWindowsStyle::subElementRect(sr, option, widget); - if (sr == SE_TabWidgetTabContents) { - if (const QTabWidget *tabWidget = qobject_cast<const QTabWidget *>(widget)) { - if (tabWidget->documentMode()) - break; - } - - rect.adjust(0, 0, -2, -2); - } - } - break; - case SE_TabWidgetTabBar: { - rect = QWindowsStyle::subElementRect(sr, option, widget); - const auto *twfOption = qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option); - if (twfOption && twfOption->direction == Qt::RightToLeft - && (twfOption->shape == QTabBar::RoundedNorth - || twfOption->shape == QTabBar::RoundedSouth)) - { - QStyleOptionTab otherOption; - otherOption.shape = (twfOption->shape == QTabBar::RoundedNorth - ? QTabBar::RoundedEast : QTabBar::RoundedSouth); - int overlap = proxy()->pixelMetric(PM_TabBarBaseOverlap, &otherOption, widget); - int borderThickness = proxy()->pixelMetric(PM_DefaultFrameWidth, option, widget); - rect.adjust(-overlap + borderThickness, 0, -overlap + borderThickness, 0); - } - break; - } - case SE_PushButtonContents: - if (const auto *btn = qstyleoption_cast<const QStyleOptionButton *>(option)) { - MARGINS borderSize; - if (widget) { - XPThemeData buttontheme(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme); - HTHEME theme = buttontheme.handle(); - if (theme) { - int stateId; - if (!(option->state & State_Enabled)) - stateId = PBS_DISABLED; - else if (option->state & State_Sunken) - stateId = PBS_PRESSED; - else if (option->state & State_MouseOver) - stateId = PBS_HOT; - else if (btn->features & QStyleOptionButton::DefaultButton) - stateId = PBS_DEFAULTED; - else - stateId = PBS_NORMAL; - - int border = proxy()->pixelMetric(PM_DefaultFrameWidth, btn, widget); - rect = option->rect.adjusted(border, border, -border, -border); - - if (SUCCEEDED(GetThemeMargins(theme, nullptr, BP_PUSHBUTTON, stateId, TMT_CONTENTMARGINS, nullptr, &borderSize))) { - rect.adjust(borderSize.cxLeftWidth, borderSize.cyTopHeight, - -borderSize.cxRightWidth, -borderSize.cyBottomHeight); - rect = visualRect(option->direction, option->rect, rect); - } - } - } - } - break; - case SE_ProgressBarContents: - rect = QCommonStyle::subElementRect(SE_ProgressBarGroove, option, widget); - if (option->state & QStyle::State_Horizontal) - rect.adjust(4, 3, -4, -3); - else - rect.adjust(3, 2, -3, -2); - break; - default: - rect = QWindowsStyle::subElementRect(sr, option, widget); - } - return rect; -} - -/*! - \reimp -*/ -void QWindowsXPStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *option, QPainter *p, - const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) { - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - - auto *d = const_cast<QWindowsXPStylePrivate*>(d_func()); - int themeNumber = -1; - int partId = 0; - int stateId = 0; - QRect rect = option->rect; - State flags = option->state; - bool hMirrored = false; - bool vMirrored = false; - bool noBorder = false; - bool noContent = false; - int rotate = 0; - - switch (pe) { - case PE_FrameTabBarBase: - if (const auto *tbb = qstyleoption_cast<const QStyleOptionTabBarBase *>(option)) { - p->save(); - switch (tbb->shape) { - case QTabBar::RoundedNorth: - p->setPen(QPen(tbb->palette.dark(), 0)); - p->drawLine(tbb->rect.topLeft(), tbb->rect.topRight()); - break; - case QTabBar::RoundedWest: - p->setPen(QPen(tbb->palette.dark(), 0)); - p->drawLine(tbb->rect.left(), tbb->rect.top(), tbb->rect.left(), tbb->rect.bottom()); - break; - case QTabBar::RoundedSouth: - p->setPen(QPen(tbb->palette.dark(), 0)); - p->drawLine(tbb->rect.left(), tbb->rect.top(), - tbb->rect.right(), tbb->rect.top()); - break; - case QTabBar::RoundedEast: - p->setPen(QPen(tbb->palette.dark(), 0)); - p->drawLine(tbb->rect.topLeft(), tbb->rect.bottomLeft()); - break; - case QTabBar::TriangularNorth: - case QTabBar::TriangularEast: - case QTabBar::TriangularWest: - case QTabBar::TriangularSouth: - p->restore(); - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - p->restore(); - } - return; - case PE_PanelButtonBevel: - themeNumber = QWindowsXPStylePrivate::ButtonTheme; - partId = BP_PUSHBUTTON; - if (!(flags & State_Enabled)) - stateId = PBS_DISABLED; - else if ((flags & State_Sunken) || (flags & State_On)) - stateId = PBS_PRESSED; - else if (flags & State_MouseOver) - stateId = PBS_HOT; - //else if (flags & State_ButtonDefault) - // stateId = PBS_DEFAULTED; - else - stateId = PBS_NORMAL; - break; - - case PE_PanelButtonTool: - if (widget && widget->inherits("QDockWidgetTitleButton")) { - if (const QWidget *dw = widget->parentWidget()) { - if (dw->isWindow()) - return; - } - } - themeNumber = QWindowsXPStylePrivate::ToolBarTheme; - partId = TP_BUTTON; - if (!(flags & State_Enabled)) - stateId = TS_DISABLED; - else if (flags & State_Sunken) - stateId = TS_PRESSED; - else if (flags & State_MouseOver) - stateId = flags & State_On ? TS_HOTCHECKED : TS_HOT; - else if (flags & State_On) - stateId = TS_CHECKED; - else if (!(flags & State_AutoRaise)) - stateId = TS_HOT; - else - stateId = TS_NORMAL; - break; - - case PE_IndicatorButtonDropDown: - themeNumber = QWindowsXPStylePrivate::ToolBarTheme; - partId = TP_SPLITBUTTONDROPDOWN; - if (!(flags & State_Enabled)) - stateId = TS_DISABLED; - else if (flags & State_Sunken) - stateId = TS_PRESSED; - else if (flags & State_MouseOver) - stateId = flags & State_On ? TS_HOTCHECKED : TS_HOT; - else if (flags & State_On) - stateId = TS_CHECKED; - else if (!(flags & State_AutoRaise)) - stateId = TS_HOT; - else - stateId = TS_NORMAL; - if (option->direction == Qt::RightToLeft) - hMirrored = true; - break; - - case PE_IndicatorCheckBox: - themeNumber = QWindowsXPStylePrivate::ButtonTheme; - partId = BP_CHECKBOX; - if (!(flags & State_Enabled)) - stateId = CBS_UNCHECKEDDISABLED; - else if (flags & State_Sunken) - stateId = CBS_UNCHECKEDPRESSED; - else if (flags & State_MouseOver) - stateId = CBS_UNCHECKEDHOT; - else - stateId = CBS_UNCHECKEDNORMAL; - - if (flags & State_On) - stateId += CBS_CHECKEDNORMAL-1; - else if (flags & State_NoChange) - stateId += CBS_MIXEDNORMAL-1; - - break; - - case PE_IndicatorRadioButton: - themeNumber = QWindowsXPStylePrivate::ButtonTheme; - partId = BP_RADIOBUTTON; - if (!(flags & State_Enabled)) - stateId = RBS_UNCHECKEDDISABLED; - else if (flags & State_Sunken) - stateId = RBS_UNCHECKEDPRESSED; - else if (flags & State_MouseOver) - stateId = RBS_UNCHECKEDHOT; - else - stateId = RBS_UNCHECKEDNORMAL; - - if (flags & State_On) - stateId += RBS_CHECKEDNORMAL-1; - break; - - case PE_IndicatorDockWidgetResizeHandle: - return; - - case PE_Frame: { - if (flags & State_Raised) - return; - themeNumber = QWindowsXPStylePrivate::ListViewTheme; - partId = LVP_LISTGROUP; - XPThemeData theme(widget, nullptr, themeNumber, partId); - - if (!(flags & State_Enabled)) - stateId = ETS_DISABLED; - else - stateId = ETS_NORMAL; - int fillType; - if (GetThemeEnumValue(theme.handle(), partId, stateId, TMT_BGTYPE, &fillType) == S_OK) { - if (fillType == BT_BORDERFILL) { - COLORREF bcRef; - GetThemeColor(theme.handle(), partId, stateId, TMT_BORDERCOLOR, &bcRef); - QColor bordercolor(qRgb(GetRValue(bcRef), GetGValue(bcRef), GetBValue(bcRef))); - QPen oldPen = p->pen(); - // int borderSize = 1; - // GetThemeInt(theme.handle(), partId, stateId, TMT_BORDERCOLOR, &borderSize); - - // Inner white border - p->setPen(QPen(option->palette.base().color(), 0)); - const qreal dpi = QStyleHelper::dpi(option); - const auto topLevelAdjustment = QStyleHelper::dpiScaled(0.5, dpi); - const auto bottomRightAdjustment = QStyleHelper::dpiScaled(-1, dpi); - p->drawRect(QRectF(option->rect).adjusted(topLevelAdjustment, topLevelAdjustment, - bottomRightAdjustment, bottomRightAdjustment)); - // Outer dark border - p->setPen(QPen(bordercolor, 0)); - p->drawRect(QRectF(option->rect).adjusted(0, 0, -topLevelAdjustment, -topLevelAdjustment)); - p->setPen(oldPen); - return; - } - if (fillType == BT_NONE) - return; - } - break; - } - - case PE_FrameTabWidget: - if (const auto *tab = qstyleoption_cast<const QStyleOptionTabWidgetFrame *>(option)) { - themeNumber = QWindowsXPStylePrivate::TabTheme; - partId = TABP_PANE; - - if (widget) { - bool useGradient = true; - const int maxlength = 256; - wchar_t themeFileName[maxlength]; - wchar_t themeColor[maxlength]; - // Due to a a scaling issue with the XP Silver theme, tab gradients are not used with it - if (GetCurrentThemeName(themeFileName, maxlength, themeColor, maxlength, nullptr, 0) == S_OK) { - wchar_t *offset = nullptr; - if ((offset = wcsrchr(themeFileName, QChar(QLatin1Char('\\')).unicode())) != nullptr) { - offset++; - if (!lstrcmp(offset, L"Luna.msstyles") && !lstrcmp(offset, L"Metallic")) - useGradient = false; - } - } - // This should work, but currently there's an error in the ::drawBackgroundDirectly() - // code, when using the HDC directly.. - if (useGradient) { - QStyleOptionTabWidgetFrame frameOpt = *tab; - frameOpt.rect = widget->rect(); - QRect contentsRect = subElementRect(SE_TabWidgetTabContents, &frameOpt, widget); - QRegion reg = option->rect; - reg -= contentsRect; - p->setClipRegion(reg); - XPThemeData theme(widget, p, themeNumber, partId, stateId, rect); - theme.mirrorHorizontally = hMirrored; - theme.mirrorVertically = vMirrored; - d->drawBackground(theme); - p->setClipRect(contentsRect); - partId = TABP_BODY; - } - } - switch (tab->shape) { - case QTabBar::RoundedNorth: - case QTabBar::TriangularNorth: - break; - case QTabBar::RoundedSouth: - case QTabBar::TriangularSouth: - vMirrored = true; - break; - case QTabBar::RoundedEast: - case QTabBar::TriangularEast: - rotate = 90; - break; - case QTabBar::RoundedWest: - case QTabBar::TriangularWest: - rotate = 90; - hMirrored = true; - break; - default: - break; - } - } - break; - - case PE_FrameMenu: - p->save(); - p->setPen(option->palette.dark().color()); - p->drawRect(rect.adjusted(0, 0, -1, -1)); - p->restore(); - return; - - case PE_PanelMenuBar: - break; - - case PE_FrameDockWidget: - if (const auto *frm = qstyleoption_cast<const QStyleOptionFrame *>(option)) { - themeNumber = QWindowsXPStylePrivate::WindowTheme; - if (flags & State_Active) - stateId = FS_ACTIVE; - else - stateId = FS_INACTIVE; - - int fwidth = proxy()->pixelMetric(PM_DockWidgetFrameWidth, frm, widget); - - XPThemeData theme(widget, p, themeNumber, 0, stateId); - if (!theme.isValid()) - break; - theme.rect = QRect(frm->rect.x(), frm->rect.y(), frm->rect.x()+fwidth, frm->rect.height()-fwidth); - theme.partId = WP_SMALLFRAMELEFT; - d->drawBackground(theme); - theme.rect = QRect(frm->rect.width()-fwidth, frm->rect.y(), fwidth, frm->rect.height()-fwidth); - theme.partId = WP_SMALLFRAMERIGHT; - d->drawBackground(theme); - theme.rect = QRect(frm->rect.x(), frm->rect.bottom()-fwidth+1, frm->rect.width(), fwidth); - theme.partId = WP_SMALLFRAMEBOTTOM; - d->drawBackground(theme); - return; - } - break; - - case PE_IndicatorHeaderArrow: { -#if 0 // XP theme engine doesn't know about this :( - name = QWindowsXPStylePrivate::HeaderTheme; - partId = HP_HEADERSORTARROW; - if (flags & State_Down) - stateId = HSAS_SORTEDDOWN; - else - stateId = HSAS_SORTEDUP; -#else - if (const auto *header = qstyleoption_cast<const QStyleOptionHeader *>(option)) { - p->save(); - p->setPen(option->palette.dark().color()); - p->translate(0, option->rect.height()/2 - 4); - if (header->sortIndicator & QStyleOptionHeader::SortUp) { // invert logic to follow Windows style guide - p->drawLine(option->rect.x(), option->rect.y(), option->rect.x()+8, option->rect.y()); - p->drawLine(option->rect.x()+1, option->rect.y()+1, option->rect.x()+7, option->rect.y()+1); - p->drawLine(option->rect.x()+2, option->rect.y()+2, option->rect.x()+6, option->rect.y()+2); - p->drawLine(option->rect.x()+3, option->rect.y()+3, option->rect.x()+5, option->rect.y()+3); - p->drawPoint(option->rect.x()+4, option->rect.y()+4); - } else if (header->sortIndicator & QStyleOptionHeader::SortDown) { - p->drawLine(option->rect.x(), option->rect.y()+4, option->rect.x()+8, option->rect.y()+4); - p->drawLine(option->rect.x()+1, option->rect.y()+3, option->rect.x()+7, option->rect.y()+3); - p->drawLine(option->rect.x()+2, option->rect.y()+2, option->rect.x()+6, option->rect.y()+2); - p->drawLine(option->rect.x()+3, option->rect.y()+1, option->rect.x()+5, option->rect.y()+1); - p->drawPoint(option->rect.x()+4, option->rect.y()); - } - p->restore(); - return; - } -#endif - break; - } - - case PE_FrameStatusBarItem: - themeNumber = QWindowsXPStylePrivate::StatusTheme; - partId = SP_PANE; - break; - - case PE_FrameGroupBox: - themeNumber = QWindowsXPStylePrivate::ButtonTheme; - partId = BP_GROUPBOX; - if (!(flags & State_Enabled)) - stateId = GBS_DISABLED; - else - stateId = GBS_NORMAL; - if (const auto *frame = qstyleoption_cast<const QStyleOptionFrame *>(option)) { - if (frame->features & QStyleOptionFrame::Flat) { - // Windows XP does not have a theme part for a flat GroupBox, paint it with the windows style - QRect fr = frame->rect; - QPoint p1(fr.x(), fr.y() + 1); - QPoint p2(fr.x() + fr.width(), p1.y() + 1); - rect = QRect(p1, p2); - themeNumber = -1; - } - } - break; - - case PE_FrameWindow: - if (const auto *frm = qstyleoption_cast<const QStyleOptionFrame *>(option)) { - themeNumber = QWindowsXPStylePrivate::WindowTheme; - if (flags & State_Active) - stateId = FS_ACTIVE; - else - stateId = FS_INACTIVE; - - int fwidth = int((frm->lineWidth + frm->midLineWidth) / QWindowsStylePrivate::nativeMetricScaleFactor(widget)); - - XPThemeData theme(widget, p, themeNumber, 0, stateId); - if (!theme.isValid()) - break; - - // May fail due to too-large buffers for large widgets, fall back to Windows style. - theme.rect = QRect(option->rect.x(), option->rect.y()+fwidth, option->rect.x()+fwidth, option->rect.height()-fwidth); - theme.partId = WP_FRAMELEFT; - if (!d->drawBackground(theme)) { - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - theme.rect = QRect(option->rect.width()-fwidth, option->rect.y()+fwidth, fwidth, option->rect.height()-fwidth); - theme.partId = WP_FRAMERIGHT; - if (!d->drawBackground(theme)) { - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - theme.rect = QRect(option->rect.x(), option->rect.height()-fwidth, option->rect.width(), fwidth); - theme.partId = WP_FRAMEBOTTOM; - if (!d->drawBackground(theme)) { - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - theme.rect = QRect(option->rect.x(), option->rect.y(), option->rect.width(), option->rect.y()+fwidth); - theme.partId = WP_CAPTION; - if (!d->drawBackground(theme)) - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - break; - - case PE_IndicatorToolBarSeparator: - if (option->rect.height() < 3) { - // XP style requires a few pixels for the separator - // to be visible. - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - themeNumber = QWindowsXPStylePrivate::ToolBarTheme; - partId = TP_SEPARATOR; - - if (option->state & State_Horizontal) - partId = TP_SEPARATOR; - else - partId = TP_SEPARATORVERT; - - break; - - case PE_IndicatorToolBarHandle: - themeNumber = QWindowsXPStylePrivate::RebarTheme; - partId = RP_GRIPPER; - if (option->state & State_Horizontal) { - partId = RP_GRIPPER; - rect.adjust(0, 0, -2, 0); - } - else { - partId = RP_GRIPPERVERT; - rect.adjust(0, 0, 0, -2); - } - break; - - case PE_IndicatorItemViewItemCheck: { - QStyleOptionButton button; - button.QStyleOption::operator=(*option); - button.state &= ~State_MouseOver; - proxy()->drawPrimitive(PE_IndicatorCheckBox, &button, p, widget); - return; - } - - default: - break; - } - - XPThemeData theme(widget, p, themeNumber, partId, stateId, rect); - if (!theme.isValid()) { - QWindowsStyle::drawPrimitive(pe, option, p, widget); - return; - } - theme.mirrorHorizontally = hMirrored; - theme.mirrorVertically = vMirrored; - theme.noBorder = noBorder; - theme.noContent = noContent; - theme.rotate = rotate; - d->drawBackground(theme); -} - -/*! - \reimp -*/ -void QWindowsXPStyle::drawControl(ControlElement element, const QStyleOption *option, QPainter *p, - const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) { - QWindowsStyle::drawControl(element, option, p, widget); - return; - } - - auto *d = const_cast<QWindowsXPStylePrivate*>(d_func()); - QRect rect(option->rect); - - int rotate = 0; - bool hMirrored = false; - bool vMirrored = false; - - int themeNumber = -1; - int partId = 0; - int stateId = 0; - switch (element) { - case CE_SizeGrip: { - themeNumber = QWindowsXPStylePrivate::StatusTheme; - partId = SP_GRIPPER; - XPThemeData theme(nullptr, p, themeNumber, partId); - QSize size = (theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); - size.rheight()--; - if (const auto *sg = qstyleoption_cast<const QStyleOptionSizeGrip *>(option)) { - switch (sg->corner) { - case Qt::BottomRightCorner: - rect = QRect(QPoint(rect.right() - size.width(), rect.bottom() - size.height()), size); - break; - case Qt::BottomLeftCorner: - rect = QRect(QPoint(rect.left() + 1, rect.bottom() - size.height()), size); - hMirrored = true; - break; - case Qt::TopRightCorner: - rect = QRect(QPoint(rect.right() - size.width(), rect.top() + 1), size); - vMirrored = true; - break; - case Qt::TopLeftCorner: - rect = QRect(rect.topLeft() + QPoint(1, 1), size); - hMirrored = vMirrored = true; - } - } - break; - } - - case CE_Splitter: - p->eraseRect(option->rect); - return; - - case CE_TabBarTab: - if (const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) - stateId = tab->state & State_Enabled ? TIS_NORMAL : TIS_DISABLED; - break; - - case CE_TabBarTabShape: - if (const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) { - themeNumber = QWindowsXPStylePrivate::TabTheme; - const bool isDisabled = !(tab->state & State_Enabled); - const bool hasFocus = tab->state & State_HasFocus; - const bool isHot = tab->state & State_MouseOver; - const bool selected = tab->state & State_Selected; - bool lastTab = tab->position == QStyleOptionTab::End; - bool firstTab = tab->position == QStyleOptionTab::Beginning; - const bool onlyOne = tab->position == QStyleOptionTab::OnlyOneTab; - const bool leftAligned = proxy()->styleHint(SH_TabBar_Alignment, tab, widget) == Qt::AlignLeft; - const bool centerAligned = proxy()->styleHint(SH_TabBar_Alignment, tab, widget) == Qt::AlignCenter; - const int borderThickness = proxy()->pixelMetric(PM_DefaultFrameWidth, option, widget); - const int tabOverlap = proxy()->pixelMetric(PM_TabBarTabOverlap, option, widget); - - if (isDisabled) - stateId = TIS_DISABLED; - else if (selected) - stateId = TIS_SELECTED; - else if (hasFocus) - stateId = TIS_FOCUSED; - else if (isHot) - stateId = TIS_HOT; - else - stateId = TIS_NORMAL; - - // Selecting proper part depending on position - if (firstTab || onlyOne) { - if (leftAligned) - partId = TABP_TABITEMLEFTEDGE; - else if (centerAligned) - partId = TABP_TABITEM; - else // rightAligned - partId = TABP_TABITEMRIGHTEDGE; - } else { - partId = TABP_TABITEM; - } - - if (tab->direction == Qt::RightToLeft - && (tab->shape == QTabBar::RoundedNorth || tab->shape == QTabBar::RoundedSouth)) { - bool temp = firstTab; - firstTab = lastTab; - lastTab = temp; - } - const bool begin = firstTab || onlyOne; - const bool end = lastTab || onlyOne; - switch (tab->shape) { - case QTabBar::RoundedNorth: - if (selected) - rect.adjust(begin ? 0 : -tabOverlap, 0, end ? 0 : tabOverlap, borderThickness); - else - rect.adjust(begin? tabOverlap : 0, tabOverlap, end ? -tabOverlap : 0, 0); - break; - case QTabBar::RoundedSouth: - //vMirrored = true; - rotate = 180; // Not 100% correct, but works - if (selected) - rect.adjust(begin ? 0 : -tabOverlap , -borderThickness, end ? 0 : tabOverlap, 0); - else - rect.adjust(begin ? tabOverlap : 0, 0, end ? -tabOverlap : 0 , -tabOverlap); - break; - case QTabBar::RoundedEast: - rotate = 90; - if (selected) - rect.adjust(-borderThickness, begin ? 0 : -tabOverlap, 0, end ? 0 : tabOverlap); - else - rect.adjust(0, begin ? tabOverlap : 0, -tabOverlap, end ? -tabOverlap : 0); - break; - case QTabBar::RoundedWest: - hMirrored = true; - rotate = 90; - if (selected) - rect.adjust(0, begin ? 0 : -tabOverlap, borderThickness, end ? 0 : tabOverlap); - else - rect.adjust(tabOverlap, begin ? tabOverlap : 0, 0, end ? -tabOverlap : 0); - break; - default: - themeNumber = -1; // Do our own painting for triangular - break; - } - - if (!selected) { - switch (tab->shape) { - case QTabBar::RoundedNorth: - rect.adjust(0,0, 0,-1); - break; - case QTabBar::RoundedSouth: - rect.adjust(0,1, 0,0); - break; - case QTabBar::RoundedEast: - rect.adjust( 1,0, 0,0); - break; - case QTabBar::RoundedWest: - rect.adjust(0,0, -1,0); - break; - default: - break; - } - } - } - break; - - case CE_ProgressBarGroove: { - Qt::Orientation orient = progressBarOrientation(option); - partId = (orient == Qt::Horizontal) ? PP_BAR : PP_BARVERT; - themeNumber = QWindowsXPStylePrivate::ProgressTheme; - stateId = 1; - break; - } - case CE_MenuEmptyArea: - case CE_MenuItem: - if (const auto *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { - const int tab = menuitem->reservedShortcutWidth; - const bool dis = !(menuitem->state & State_Enabled); - const bool act = menuitem->state & State_Selected; - const bool checkable = menuitem->menuHasCheckableItems; - const bool checked = checkable ? menuitem->checked : false; - - // windows always has a check column, regardless whether we have an icon or not - const int checkcol = qMax(menuitem->maxIconWidth, 12); - - int x, y, w, h; - rect.getRect(&x, &y, &w, &h); - - QBrush fill = menuitem->palette.brush(act ? QPalette::Highlight : QPalette::Button); - p->fillRect(rect, fill); - - if (element == CE_MenuEmptyArea) - break; - - // draw separator ------------------------------------------------- - if (menuitem->menuItemType == QStyleOptionMenuItem::Separator) { - int yoff = y-1 + h / 2; - p->setPen(menuitem->palette.dark().color()); - p->drawLine(x, yoff, x+w, yoff); - ++yoff; - p->setPen(menuitem->palette.light().color()); - p->drawLine(x, yoff, x+w, yoff); - return; - } - - int xpos = x; - - // draw icon ------------------------------------------------------ - if (!menuitem->icon.isNull()) { - QIcon::Mode mode = dis ? QIcon::Disabled : QIcon::Normal; - if (act && !dis) - mode = QIcon::Active; - QPixmap pixmap = checked ? - menuitem->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), mode, QIcon::On) : - menuitem->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), mode); - QRect iconRect(QPoint(0, 0), pixmap.deviceIndependentSize().toSize()); - iconRect.moveCenter(QRect(xpos, y, checkcol, h).center()); - QRect vIconRect = visualRect(option->direction, option->rect, iconRect); - p->setPen(menuitem->palette.text().color()); - p->setBrush(Qt::NoBrush); - if (checked) - p->drawRect(vIconRect.adjusted(-1, -1, 0, 0)); - p->drawPixmap(vIconRect.topLeft(), pixmap); - - // draw checkmark ------------------------------------------------- - } else if (checked) { - QStyleOptionMenuItem newMi = *menuitem; - newMi.state = State_None; - if (!dis) - newMi.state |= State_Enabled; - if (act) - newMi.state |= State_On; - - QRect checkMarkRect = QRect(menuitem->rect.x() + windowsItemFrame, - menuitem->rect.y() + windowsItemFrame, - checkcol - 2 * windowsItemFrame, - menuitem->rect.height() - 2*windowsItemFrame); - newMi.rect = visualRect(option->direction, option->rect, checkMarkRect); - proxy()->drawPrimitive(PE_IndicatorMenuCheckMark, &newMi, p, widget); - } - - QColor textColor = dis ? menuitem->palette.text().color() : - act ? menuitem->palette.highlightedText().color() : menuitem->palette.buttonText().color(); - p->setPen(textColor); - - // draw text ------------------------------------------------------ - int xm = windowsItemFrame + checkcol + windowsItemHMargin; - xpos = menuitem->rect.x() + xm; - QRect textRect(xpos, y + windowsItemVMargin, w - xm - windowsRightBorder - tab + 1, h - 2 * windowsItemVMargin); - QRect vTextRect = visualRect(option->direction, option->rect, textRect); - QString s = menuitem->text; - if (!s.isEmpty()) { - p->save(); - int t = s.indexOf(QLatin1Char('\t')); - int text_flags = Qt::AlignVCenter|Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignLeft; - if (!proxy()->styleHint(SH_UnderlineShortcut, menuitem, widget)) - text_flags |= Qt::TextHideMnemonic; - // draw tab text ---------------- - if (t >= 0) { - QRect vShortcutRect = visualRect(option->direction, option->rect, QRect(textRect.topRight(), menuitem->rect.bottomRight())); - if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) { - p->setPen(menuitem->palette.light().color()); - p->drawText(vShortcutRect.adjusted(1,1,1,1), text_flags, s.mid(t + 1)); - p->setPen(textColor); - } - p->drawText(vShortcutRect, text_flags, s.mid(t + 1)); - s = s.left(t); - } - QFont font = menuitem->font; - if (menuitem->menuItemType == QStyleOptionMenuItem::DefaultItem) - font.setBold(true); - p->setFont(font); - if (dis && !act && proxy()->styleHint(SH_EtchDisabledText, option, widget)) { - p->setPen(menuitem->palette.light().color()); - p->drawText(vTextRect.adjusted(1,1,1,1), text_flags, s.left(t)); - p->setPen(textColor); - } - p->drawText(vTextRect, text_flags, s); - p->restore(); - } - - // draw sub menu arrow -------------------------------------------- - if (menuitem->menuItemType == QStyleOptionMenuItem::SubMenu) { - int dim = (h - 2) / 2; - PrimitiveElement arrow; - arrow = (option->direction == Qt::RightToLeft) ? PE_IndicatorArrowLeft : PE_IndicatorArrowRight; - xpos = x + w - windowsArrowHMargin - windowsItemFrame - dim; - QRect vSubMenuRect = visualRect(option->direction, option->rect, QRect(xpos, y + h / 2 - dim / 2, dim, dim)); - QStyleOptionMenuItem newMI = *menuitem; - newMI.rect = vSubMenuRect; - newMI.state = dis ? State_None : State_Enabled; - if (act) - newMI.palette.setColor(QPalette::ButtonText, newMI.palette.highlightedText().color()); - proxy()->drawPrimitive(arrow, &newMI, p, widget); - } - } - return; - - case CE_MenuBarItem: - if (const auto *mbi = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { - if (mbi->menuItemType == QStyleOptionMenuItem::DefaultItem) - break; - - bool act = mbi->state & State_Selected; - bool dis = !(mbi->state & State_Enabled); - - QBrush fill = mbi->palette.brush(act ? QPalette::Highlight : QPalette::Button); - QPalette::ColorRole textRole = dis ? QPalette::Text: - act ? QPalette::HighlightedText : QPalette::ButtonText; - QPixmap pix = mbi->icon.pixmap(proxy()->pixelMetric(PM_SmallIconSize, option, widget), QIcon::Normal); - - uint alignment = Qt::AlignCenter | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; - if (!proxy()->styleHint(SH_UnderlineShortcut, mbi, widget)) - alignment |= Qt::TextHideMnemonic; - - p->fillRect(rect, fill); - if (!pix.isNull()) - drawItemPixmap(p, mbi->rect, alignment, pix); - else - drawItemText(p, mbi->rect, alignment, mbi->palette, mbi->state & State_Enabled, mbi->text, textRole); - } - return; -#if QT_CONFIG(dockwidget) - case CE_DockWidgetTitle: - if (const auto *dwOpt = qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { - const int buttonMargin = 4; - const int mw = proxy()->pixelMetric(QStyle::PM_DockWidgetTitleMargin, dwOpt, widget); - const int fw = proxy()->pixelMetric(PM_DockWidgetFrameWidth, dwOpt, widget); - const bool isFloating = widget && widget->isWindow(); - const bool isActive = dwOpt->state & State_Active; - - const bool verticalTitleBar = dwOpt->verticalTitleBar; - - if (verticalTitleBar) { - rect = rect.transposed(); - - p->translate(rect.left() - 1, rect.top() + rect.width()); - p->rotate(-90); - p->translate(-rect.left() + 1, -rect.top()); - } - QRect r = rect.adjusted(0, 2, -1, -3); - QRect titleRect = r; - - if (dwOpt->closable) { - QSize sz = proxy()->standardIcon(QStyle::SP_TitleBarCloseButton, dwOpt, widget).actualSize(QSize(10, 10)); - titleRect.adjust(0, 0, -sz.width() - mw - buttonMargin, 0); - } - - if (dwOpt->floatable) { - QSize sz = proxy()->standardIcon(QStyle::SP_TitleBarMaxButton, dwOpt, widget).actualSize(QSize(10, 10)); - titleRect.adjust(0, 0, -sz.width() - mw - buttonMargin, 0); - } - - if (isFloating) { - titleRect.adjust(0, -fw, 0, 0); - if (widget != nullptr && widget->windowIcon().cacheKey() != QApplication::windowIcon().cacheKey()) - titleRect.adjust(titleRect.height() + mw, 0, 0, 0); - } else { - titleRect.adjust(mw, 0, 0, 0); - if (!dwOpt->floatable && !dwOpt->closable) - titleRect.adjust(0, 0, -mw, 0); - } - - if (!verticalTitleBar) - titleRect = visualRect(dwOpt->direction, r, titleRect); - - if (!isFloating) { - QPen oldPen = p->pen(); - QString titleText = p->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width()); - p->setPen(dwOpt->palette.color(QPalette::Dark)); - p->drawRect(r); - - if (!titleText.isEmpty()) { - drawItemText(p, titleRect, - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextShowMnemonic, dwOpt->palette, - dwOpt->state & State_Enabled, titleText, - QPalette::WindowText); - } - - p->setPen(oldPen); - } else { - themeNumber = QWindowsXPStylePrivate::WindowTheme; - if (isActive) - stateId = CS_ACTIVE; - else - stateId = CS_INACTIVE; - - int titleHeight = rect.height() - 2; - rect = rect.adjusted(-fw, -fw, fw, 0); - - XPThemeData theme(widget, p, themeNumber, 0, stateId); - if (!theme.isValid()) - break; - - // Draw small type title bar - theme.rect = rect; - theme.partId = WP_SMALLCAPTION; - d->drawBackground(theme); - - // Figure out maximal button space on title bar - - QIcon ico = widget->windowIcon(); - bool hasIcon = (ico.cacheKey() != QApplication::windowIcon().cacheKey()); - if (hasIcon) { - QPixmap pxIco = ico.pixmap(titleHeight); - if (!verticalTitleBar && dwOpt->direction == Qt::RightToLeft) - p->drawPixmap(rect.width() - titleHeight - pxIco.width(), rect.bottom() - titleHeight - 2, pxIco); - else - p->drawPixmap(fw, rect.bottom() - titleHeight - 2, pxIco); - } - if (!dwOpt->title.isEmpty()) { - QPen oldPen = p->pen(); - QFont oldFont = p->font(); - QFont titleFont = oldFont; - titleFont.setBold(true); - p->setFont(titleFont); - QString titleText - = p->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width()); - - int result = TST_NONE; - GetThemeEnumValue(theme.handle(), WP_SMALLCAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWTYPE, &result); - if (result != TST_NONE) { - COLORREF textShadowRef; - GetThemeColor(theme.handle(), WP_SMALLCAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWCOLOR, &textShadowRef); - QColor textShadow = qRgb(GetRValue(textShadowRef), GetGValue(textShadowRef), GetBValue(textShadowRef)); - p->setPen(textShadow); - drawItemText(p, titleRect.adjusted(1, 1, 1, 1), - Qt::AlignLeft | Qt::AlignBottom, dwOpt->palette, - dwOpt->state & State_Enabled, titleText); - } - - COLORREF captionText = GetSysColor(isActive ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT); - QColor textColor = qRgb(GetRValue(captionText), GetGValue(captionText), GetBValue(captionText)); - p->setPen(textColor); - drawItemText(p, titleRect, - Qt::AlignLeft | Qt::AlignBottom, dwOpt->palette, - dwOpt->state & State_Enabled, titleText); - p->setFont(oldFont); - p->setPen(oldPen); - } - - } - - return; - } - break; -#endif // QT_CONFIG(dockwidget) -#if QT_CONFIG(rubberband) - case CE_RubberBand: - if (qstyleoption_cast<const QStyleOptionRubberBand *>(option)) { - QColor highlight = option->palette.color(QPalette::Active, QPalette::Highlight); - p->save(); - p->setPen(highlight.darker(120)); - QColor dimHighlight(qMin(highlight.red()/2 + 110, 255), - qMin(highlight.green()/2 + 110, 255), - qMin(highlight.blue()/2 + 110, 255), - (widget && widget->isWindow())? 255 : 127); - p->setBrush(dimHighlight); - p->drawRect(option->rect.adjusted(0, 0, -1, -1)); - p->restore(); - return; - } - break; -#endif // QT_CONFIG(rubberband) - case CE_HeaderEmptyArea: - if (option->state & State_Horizontal) { - themeNumber = QWindowsXPStylePrivate::HeaderTheme; - stateId = HIS_NORMAL; - } else { - QWindowsStyle::drawControl(CE_HeaderEmptyArea, option, p, widget); - return; - } - break; - default: - break; - } - - XPThemeData theme(widget, p, themeNumber, partId, stateId, rect); - if (!theme.isValid()) { - QWindowsStyle::drawControl(element, option, p, widget); - return; - } - - theme.rotate = rotate; - theme.mirrorHorizontally = hMirrored; - theme.mirrorVertically = vMirrored; - d->drawBackground(theme); -} - -QRect QWindowsXPStylePrivate::scrollBarGripperBounds(QStyle::State flags, const QWidget *widget, XPThemeData *theme) -{ - const bool horizontal = flags & QStyle::State_Horizontal; - const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); - const QMargins contentsMargin = - (theme->margins(theme->rect, TMT_SIZINGMARGINS) * factor).toMargins(); - theme->partId = horizontal ? SBP_GRIPPERHORZ : SBP_GRIPPERVERT; - const QSize size = (theme->size() * factor).toSize(); - - const int hSpace = theme->rect.width() - size.width(); - const int vSpace = theme->rect.height() - size.height(); - const bool sufficientSpace = (horizontal && hSpace > (contentsMargin.left() + contentsMargin.right())) - || vSpace > contentsMargin.top() + contentsMargin.bottom(); - return sufficientSpace ? QRect(theme->rect.topLeft() + QPoint(hSpace, vSpace) / 2, size) : QRect(); -} - -#if QT_CONFIG(mdiarea) -// Helper for drawing MDI buttons into the corner widget of QMenuBar in case a -// QMdiSubWindow is maximized. -static void populateMdiButtonTheme(const QStyle *proxy, const QWidget *widget, - const QStyleOptionComplex *option, - QStyle::SubControl subControl, int part, - XPThemeData *theme) -{ - theme->partId = part; - theme->rect = proxy->subControlRect(QStyle::CC_MdiControls, option, subControl, widget); - if (!option->state.testFlag(QStyle::State_Enabled)) - theme->stateId = CBS_INACTIVE; - else if (option->state.testFlag(QStyle::State_Sunken) && option->activeSubControls.testFlag(subControl)) - theme->stateId = CBS_PUSHED; - else if (option->state.testFlag(QStyle::State_MouseOver) && option->activeSubControls.testFlag(subControl)) - theme->stateId = CBS_HOT; - else - theme->stateId = CBS_NORMAL; -} - -// Calculate an small (max 2), empirical correction factor for scaling up -// WP_MDICLOSEBUTTON, WP_MDIRESTOREBUTTON, WP_MDIMINBUTTON, which are too -// small on High DPI screens (QTBUG-75927). -static qreal mdiButtonCorrectionFactor(XPThemeData &theme, const QPaintDevice *pd = nullptr) -{ - const auto dpr = pd ? pd->devicePixelRatio() : qApp->devicePixelRatio(); - const QSizeF nativeSize = QSizeF(theme.size()) / dpr; - const QSizeF requestedSize(theme.rect.size()); - const auto rawFactor = qMin(requestedSize.width() / nativeSize.width(), - requestedSize.height() / nativeSize.height()); - const auto factor = rawFactor >= qreal(2) ? qreal(2) : qreal(1); - return factor; -} -#endif // QT_CONFIG(mdiarea) - -static void populateTitleBarButtonTheme(const QStyle *proxy, const QWidget *widget, - const QStyleOptionComplex *option, - QStyle::SubControl subControl, - bool isTitleBarActive, int part, - XPThemeData *theme) -{ - theme->rect = proxy->subControlRect(QStyle::CC_TitleBar, option, subControl, widget); - theme->partId = part; - if (widget && !widget->isEnabled()) - theme->stateId = RBS_DISABLED; - else if (option->activeSubControls == subControl && option->state.testFlag(QStyle::State_Sunken)) - theme->stateId = RBS_PUSHED; - else if (option->activeSubControls == subControl && option->state.testFlag(QStyle::State_MouseOver)) - theme->stateId = RBS_HOT; - else if (!isTitleBarActive) - theme->stateId = RBS_INACTIVE; - else - theme->stateId = RBS_NORMAL; -} - -/*! - \reimp -*/ -void QWindowsXPStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, - QPainter *p, const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) { - QWindowsStyle::drawComplexControl(cc, option, p, widget); - return; - } - - auto *d = const_cast<QWindowsXPStylePrivate*>(d_func()); - - State flags = option->state; - SubControls sub = option->subControls; - - int partId = 0; - int stateId = 0; - if (widget && widget->testAttribute(Qt::WA_UnderMouse) && widget->isActiveWindow()) - flags |= State_MouseOver; - - switch (cc) { -#if QT_CONFIG(slider) - case CC_Slider: - if (const auto *slider = qstyleoption_cast<const QStyleOptionSlider *>(option)) { - XPThemeData theme(widget, p, QWindowsXPStylePrivate::TrackBarTheme); - QRect slrect = slider->rect; - QRegion tickreg = slrect; - if (sub & SC_SliderGroove) { - theme.rect = proxy()->subControlRect(CC_Slider, option, SC_SliderGroove, widget); - if (slider->orientation == Qt::Horizontal) { - partId = TKP_TRACK; - stateId = TRS_NORMAL; - theme.rect = QRect(slrect.left(), theme.rect.center().y() - 2, slrect.width(), 4); - } else { - partId = TKP_TRACKVERT; - stateId = TRVS_NORMAL; - theme.rect = QRect(theme.rect.center().x() - 2, slrect.top(), 4, slrect.height()); - } - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - tickreg -= theme.rect; - } - if (sub & SC_SliderTickmarks) { - int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget); - int ticks = slider->tickPosition; - int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget); - int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); - int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); - int interval = slider->tickInterval; - if (interval <= 0) { - interval = slider->singleStep; - if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, - available) - - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, - 0, available) < 3) - interval = slider->pageStep; - } - if (!interval) - interval = 1; - int fudge = len / 2; - int pos; - int bothOffset = (ticks & QSlider::TicksAbove && ticks & QSlider::TicksBelow) ? 1 : 0; - p->setPen(d->sliderTickColor); - QVarLengthArray<QLine, 32> lines; - int v = slider->minimum; - while (v <= slider->maximum + 1) { - if (v == slider->maximum + 1 && interval == 1) - break; - const int v_ = qMin(v, slider->maximum); - int tickLength = (v_ == slider->minimum || v_ >= slider->maximum) ? 4 : 3; - pos = QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, - v_, available) + fudge; - if (slider->orientation == Qt::Horizontal) { - if (ticks & QSlider::TicksAbove) { - lines.append(QLine(pos, tickOffset - 1 - bothOffset, - pos, tickOffset - 1 - bothOffset - tickLength)); - } - - if (ticks & QSlider::TicksBelow) { - lines.append(QLine(pos, tickOffset + thickness + bothOffset, - pos, tickOffset + thickness + bothOffset + tickLength)); - } - } else { - if (ticks & QSlider::TicksAbove) { - lines.append(QLine(tickOffset - 1 - bothOffset, pos, - tickOffset - 1 - bothOffset - tickLength, pos)); - } - - if (ticks & QSlider::TicksBelow) { - lines.append(QLine(tickOffset + thickness + bothOffset, pos, - tickOffset + thickness + bothOffset + tickLength, pos)); - } - } - // in the case where maximum is max int - int nextInterval = v + interval; - if (nextInterval < v) - break; - v = nextInterval; - } - if (!lines.isEmpty()) { - p->save(); - p->translate(slrect.topLeft()); - p->drawLines(lines.constData(), lines.size()); - p->restore(); - } - } - if (sub & SC_SliderHandle) { - theme.rect = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); - if (slider->orientation == Qt::Horizontal) { - if (slider->tickPosition == QSlider::TicksAbove) - partId = TKP_THUMBTOP; - else if (slider->tickPosition == QSlider::TicksBelow) - partId = TKP_THUMBBOTTOM; - else - partId = TKP_THUMB; - - if (!(slider->state & State_Enabled)) - stateId = TUS_DISABLED; - else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_Sunken)) - stateId = TUS_PRESSED; - else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_MouseOver)) - stateId = TUS_HOT; - else if (flags & State_HasFocus) - stateId = TUS_FOCUSED; - else - stateId = TUS_NORMAL; - } else { - if (slider->tickPosition == QSlider::TicksLeft) - partId = TKP_THUMBLEFT; - else if (slider->tickPosition == QSlider::TicksRight) - partId = TKP_THUMBRIGHT; - else - partId = TKP_THUMBVERT; - - if (!(slider->state & State_Enabled)) - stateId = TUVS_DISABLED; - else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_Sunken)) - stateId = TUVS_PRESSED; - else if (slider->activeSubControls & SC_SliderHandle && (slider->state & State_MouseOver)) - stateId = TUVS_HOT; - else if (flags & State_HasFocus) - stateId = TUVS_FOCUSED; - else - stateId = TUVS_NORMAL; - } - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - } - if (slider->state & State_HasFocus) { - QStyleOptionFocusRect fropt; - fropt.QStyleOption::operator=(*slider); - fropt.rect = subElementRect(SE_SliderFocusRect, slider, widget); - proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, p, widget); - } - } - break; -#endif -#if QT_CONFIG(toolbutton) - case CC_ToolButton: - if (const auto *toolbutton = qstyleoption_cast<const QStyleOptionToolButton *>(option)) { - QRect button, menuarea; - button = proxy()->subControlRect(cc, toolbutton, SC_ToolButton, widget); - menuarea = proxy()->subControlRect(cc, toolbutton, SC_ToolButtonMenu, widget); - - State bflags = toolbutton->state & ~State_Sunken; - State mflags = bflags; - bool autoRaise = flags & State_AutoRaise; - if (autoRaise) { - if (!(bflags & State_MouseOver) || !(bflags & State_Enabled)) - bflags &= ~State_Raised; - } - - if (toolbutton->state & State_Sunken) { - if (toolbutton->activeSubControls & SC_ToolButton) { - bflags |= State_Sunken; - mflags |= State_MouseOver | State_Sunken; - } else if (toolbutton->activeSubControls & SC_ToolButtonMenu) { - mflags |= State_Sunken; - bflags |= State_MouseOver; - } - } - - QStyleOption tool = *toolbutton; - if (toolbutton->subControls & SC_ToolButton) { - if (flags & (State_Sunken | State_On | State_Raised) || !autoRaise) { - if (toolbutton->features & QStyleOptionToolButton::MenuButtonPopup && autoRaise) { - XPThemeData theme(widget, p, QWindowsXPStylePrivate::ToolBarTheme); - theme.partId = TP_SPLITBUTTON; - theme.rect = button; - if (!(bflags & State_Enabled)) - stateId = TS_DISABLED; - else if (bflags & State_Sunken) - stateId = TS_PRESSED; - else if (bflags & State_MouseOver || !(flags & State_AutoRaise)) - stateId = flags & State_On ? TS_HOTCHECKED : TS_HOT; - else if (bflags & State_On) - stateId = TS_CHECKED; - else - stateId = TS_NORMAL; - if (option->direction == Qt::RightToLeft) - theme.mirrorHorizontally = true; - theme.stateId = stateId; - d->drawBackground(theme); - } else { - tool.rect = option->rect; - tool.state = bflags; - if (autoRaise) // for tool bars - proxy()->drawPrimitive(PE_PanelButtonTool, &tool, p, widget); - else - proxy()->drawPrimitive(PE_PanelButtonBevel, &tool, p, widget); - } - } - } - - if (toolbutton->state & State_HasFocus) { - QStyleOptionFocusRect fr; - fr.QStyleOption::operator=(*toolbutton); - fr.rect.adjust(3, 3, -3, -3); - if (toolbutton->features & QStyleOptionToolButton::MenuButtonPopup) - fr.rect.adjust(0, 0, -proxy()->pixelMetric(QStyle::PM_MenuButtonIndicator, - toolbutton, widget), 0); - proxy()->drawPrimitive(PE_FrameFocusRect, &fr, p, widget); - } - QStyleOptionToolButton label = *toolbutton; - label.state = bflags; - int fw = 2; - if (!autoRaise) - label.state &= ~State_Sunken; - label.rect = button.adjusted(fw, fw, -fw, -fw); - proxy()->drawControl(CE_ToolButtonLabel, &label, p, widget); - - if (toolbutton->subControls & SC_ToolButtonMenu) { - tool.rect = menuarea; - tool.state = mflags; - if (autoRaise) { - proxy()->drawPrimitive(PE_IndicatorButtonDropDown, &tool, p, widget); - } else { - tool.state = mflags; - menuarea.adjust(-2, 0, 0, 0); - // Draw menu button - if ((bflags & State_Sunken) != (mflags & State_Sunken)){ - p->save(); - p->setClipRect(menuarea); - tool.rect = option->rect; - proxy()->drawPrimitive(PE_PanelButtonBevel, &tool, p, nullptr); - p->restore(); - } - // Draw arrow - p->save(); - p->setPen(option->palette.dark().color()); - p->drawLine(menuarea.left(), menuarea.top() + 3, - menuarea.left(), menuarea.bottom() - 3); - p->setPen(option->palette.light().color()); - p->drawLine(menuarea.left() - 1, menuarea.top() + 3, - menuarea.left() - 1, menuarea.bottom() - 3); - - tool.rect = menuarea.adjusted(2, 3, -2, -1); - proxy()->drawPrimitive(PE_IndicatorArrowDown, &tool, p, widget); - p->restore(); - } - } else if (toolbutton->features & QStyleOptionToolButton::HasMenu) { - int mbi = proxy()->pixelMetric(PM_MenuButtonIndicator, toolbutton, widget); - QRect ir = toolbutton->rect; - QStyleOptionToolButton newBtn = *toolbutton; - newBtn.rect = QRect(ir.right() + 4 - mbi, ir.height() - mbi + 4, mbi - 5, mbi - 5); - proxy()->drawPrimitive(PE_IndicatorArrowDown, &newBtn, p, widget); - } - } - break; -#endif // QT_CONFIG(toolbutton) - - case CC_TitleBar: - if (const auto *tb = qstyleoption_cast<const QStyleOptionTitleBar *>(option)) { - const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); - bool isActive = tb->titleBarState & QStyle::State_Active; - XPThemeData theme(widget, p, QWindowsXPStylePrivate::WindowTheme); - if (sub & SC_TitleBarLabel) { - partId = (tb->titleBarState & Qt::WindowMinimized) ? WP_MINCAPTION : WP_CAPTION; - theme.rect = option->rect; - if (widget && !widget->isEnabled()) - stateId = CS_DISABLED; - else if (isActive) - stateId = CS_ACTIVE; - else - stateId = CS_INACTIVE; - - theme.partId = partId; - theme.stateId = stateId; - d->drawBackground(theme); - - QRect ir = proxy()->subControlRect(CC_TitleBar, tb, SC_TitleBarLabel, widget); - - int result = TST_NONE; - GetThemeEnumValue(theme.handle(), WP_CAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWTYPE, &result); - if (result != TST_NONE) { - COLORREF textShadowRef; - GetThemeColor(theme.handle(), WP_CAPTION, isActive ? CS_ACTIVE : CS_INACTIVE, TMT_TEXTSHADOWCOLOR, &textShadowRef); - QColor textShadow = qRgb(GetRValue(textShadowRef), GetGValue(textShadowRef), GetBValue(textShadowRef)); - p->setPen(textShadow); - p->drawText(int(ir.x() + 3 * factor), int(ir.y() + 2 * factor), - int(ir.width() - 1 * factor), ir.height(), - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, tb->text); - } - COLORREF captionText = GetSysColor(isActive ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT); - QColor textColor = qRgb(GetRValue(captionText), GetGValue(captionText), GetBValue(captionText)); - p->setPen(textColor); - p->drawText(int(ir.x() + 2 * factor), int(ir.y() + 1 * factor), - int(ir.width() - 2 * factor), ir.height(), - Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, tb->text); - } - if (sub & SC_TitleBarSysMenu && tb->titleBarFlags & Qt::WindowSystemMenuHint) { - theme.rect = proxy()->subControlRect(CC_TitleBar, option, SC_TitleBarSysMenu, widget); - partId = WP_SYSBUTTON; - if ((widget && !widget->isEnabled()) || !isActive) - stateId = SBS_DISABLED; - else if (option->activeSubControls == SC_TitleBarSysMenu && (option->state & State_Sunken)) - stateId = SBS_PUSHED; - else if (option->activeSubControls == SC_TitleBarSysMenu && (option->state & State_MouseOver)) - stateId = SBS_HOT; - else - stateId = SBS_NORMAL; - if (!tb->icon.isNull()) { - tb->icon.paint(p, theme.rect); - } else { - theme.partId = partId; - theme.stateId = stateId; - if (theme.size().isEmpty()) { - int iconSize = proxy()->pixelMetric(PM_SmallIconSize, tb, widget); - QPixmap pm = proxy()->standardIcon(SP_TitleBarMenuButton, tb, widget).pixmap(iconSize, iconSize); - p->save(); - drawItemPixmap(p, theme.rect, Qt::AlignCenter, pm); - p->restore(); - } else { - d->drawBackground(theme); - } - } - } - - if (sub & SC_TitleBarMinButton && tb->titleBarFlags & Qt::WindowMinimizeButtonHint - && !(tb->titleBarState & Qt::WindowMinimized)) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarMinButton, isActive, WP_MINBUTTON, &theme); - d->drawBackground(theme); - } - if (sub & SC_TitleBarMaxButton && tb->titleBarFlags & Qt::WindowMaximizeButtonHint - && !(tb->titleBarState & Qt::WindowMaximized)) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarMaxButton, isActive, WP_MAXBUTTON, &theme); - d->drawBackground(theme); - } - if (sub & SC_TitleBarContextHelpButton - && tb->titleBarFlags & Qt::WindowContextHelpButtonHint) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarContextHelpButton, isActive, WP_HELPBUTTON, &theme); - d->drawBackground(theme); - } - bool drawNormalButton = (sub & SC_TitleBarNormalButton) - && (((tb->titleBarFlags & Qt::WindowMinimizeButtonHint) - && (tb->titleBarState & Qt::WindowMinimized)) - || ((tb->titleBarFlags & Qt::WindowMaximizeButtonHint) - && (tb->titleBarState & Qt::WindowMaximized))); - if (drawNormalButton) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarNormalButton, isActive, WP_RESTOREBUTTON, &theme); - d->drawBackground(theme); - } - if (sub & SC_TitleBarShadeButton && tb->titleBarFlags & Qt::WindowShadeButtonHint - && !(tb->titleBarState & Qt::WindowMinimized)) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarShadeButton, isActive, WP_MINBUTTON, &theme); - d->drawBackground(theme); - } - if (sub & SC_TitleBarUnshadeButton && tb->titleBarFlags & Qt::WindowShadeButtonHint - && tb->titleBarState & Qt::WindowMinimized) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarUnshadeButton, isActive, WP_RESTOREBUTTON, &theme); - d->drawBackground(theme); - } - if (sub & SC_TitleBarCloseButton && tb->titleBarFlags & Qt::WindowSystemMenuHint) { - populateTitleBarButtonTheme(proxy(), widget, option, SC_TitleBarCloseButton, isActive, WP_CLOSEBUTTON, &theme); - d->drawBackground(theme); - } - } - break; - -#if QT_CONFIG(mdiarea) - case CC_MdiControls: { - XPThemeData theme(widget, p, QWindowsXPStylePrivate::WindowTheme, WP_MDICLOSEBUTTON, CBS_NORMAL); - if (Q_UNLIKELY(!theme.isValid())) - return; - - if (option->subControls.testFlag(SC_MdiCloseButton)) { - populateMdiButtonTheme(proxy(), widget, option, SC_MdiCloseButton, WP_MDICLOSEBUTTON, &theme); - d->drawBackground(theme, mdiButtonCorrectionFactor(theme, widget)); - } - if (option->subControls.testFlag(SC_MdiNormalButton)) { - populateMdiButtonTheme(proxy(), widget, option, SC_MdiNormalButton, WP_MDIRESTOREBUTTON, &theme); - d->drawBackground(theme, mdiButtonCorrectionFactor(theme, widget)); - } - if (option->subControls.testFlag(QStyle::SC_MdiMinButton)) { - populateMdiButtonTheme(proxy(), widget, option, SC_MdiMinButton, WP_MDIMINBUTTON, &theme); - d->drawBackground(theme, mdiButtonCorrectionFactor(theme, widget)); - } - break; - } -#endif // QT_CONFIG(mdiarea) -#if QT_CONFIG(dial) - case CC_Dial: - if (const auto *dial = qstyleoption_cast<const QStyleOptionSlider *>(option)) - QStyleHelper::drawDial(dial, p); - break; -#endif // QT_CONFIG(dial) - default: - QWindowsStyle::drawComplexControl(cc, option, p, widget); - break; - } -} - -int QWindowsXPStylePrivate::pixelMetricFromSystemDp(QStyle::PixelMetric pm, const QStyleOption *option, const QWidget *widget) -{ - switch (pm) { - case QStyle::PM_IndicatorWidth: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme, BP_CHECKBOX, CBS_UNCHECKEDNORMAL).width(); - case QStyle::PM_IndicatorHeight: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme, BP_CHECKBOX, CBS_UNCHECKEDNORMAL).height(); - case QStyle::PM_ExclusiveIndicatorWidth: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme, BP_RADIOBUTTON, RBS_UNCHECKEDNORMAL).width(); - case QStyle::PM_ExclusiveIndicatorHeight: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme, BP_RADIOBUTTON, RBS_UNCHECKEDNORMAL).height(); - case QStyle::PM_ProgressBarChunkWidth: - return progressBarOrientation(option) == Qt::Horizontal - ? XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::ProgressTheme, PP_CHUNK).width() - : XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::ProgressTheme, PP_CHUNKVERT).height(); - case QStyle::PM_SliderThickness: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::TrackBarTheme, TKP_THUMB).height(); - case QStyle::PM_TitleBarHeight: - return widget && (widget->windowType() == Qt::Tool) - ? GetSystemMetrics(SM_CYSMCAPTION) + GetSystemMetrics(SM_CXSIZEFRAME) - : GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CXSIZEFRAME); - case QStyle::PM_MdiSubWindowFrameWidth: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::WindowTheme, WP_FRAMELEFT, FS_ACTIVE).width(); - case QStyle::PM_DockWidgetFrameWidth: - return XPThemeData::themeSize(widget, nullptr, QWindowsXPStylePrivate::WindowTheme, WP_SMALLFRAMERIGHT, FS_ACTIVE).width(); - default: - break; - } - return QWindowsXPStylePrivate::InvalidMetric; -} - -/*! \reimp */ -int QWindowsXPStyle::pixelMetric(PixelMetric pm, const QStyleOption *option, const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::pixelMetric(pm, option, widget); - - int res = QWindowsXPStylePrivate::pixelMetricFromSystemDp(pm, option, widget); - if (res != QWindowsStylePrivate::InvalidMetric) - return qRound(qreal(res) * QWindowsStylePrivate::nativeMetricScaleFactor(widget)); - - res = 0; - switch (pm) { - case PM_MenuBarPanelWidth: - case PM_ButtonDefaultIndicator: - res = 0; - break; - - case PM_DefaultFrameWidth: - res = qobject_cast<const QListView*>(widget) ? 2 : 1; - break; - case PM_MenuPanelWidth: - case PM_SpinBoxFrameWidth: - res = 1; - break; - - case PM_TabBarTabOverlap: - case PM_MenuHMargin: - case PM_MenuVMargin: - res = 2; - break; - - case PM_TabBarBaseOverlap: - if (const auto *tab = qstyleoption_cast<const QStyleOptionTab *>(option)) { - switch (tab->shape) { - case QTabBar::RoundedNorth: - case QTabBar::TriangularNorth: - case QTabBar::RoundedWest: - case QTabBar::TriangularWest: - res = 1; - break; - case QTabBar::RoundedSouth: - case QTabBar::TriangularSouth: - res = 2; - break; - case QTabBar::RoundedEast: - case QTabBar::TriangularEast: - res = 3; - break; - } - } - break; - - case PM_SplitterWidth: - res = QStyleHelper::dpiScaled(5., option); - break; - - case PM_MdiSubWindowMinimizedWidth: - res = 160; - break; - -#if QT_CONFIG(toolbar) - case PM_ToolBarHandleExtent: - res = int(QStyleHelper::dpiScaled(8., option)); - break; - -#endif // QT_CONFIG(toolbar) - case PM_DockWidgetSeparatorExtent: - case PM_DockWidgetTitleMargin: - res = int(QStyleHelper::dpiScaled(4., option)); - break; - - case PM_ButtonShiftHorizontal: - case PM_ButtonShiftVertical: - res = qstyleoption_cast<const QStyleOptionToolButton *>(option) ? 1 : 0; - break; - - default: - res = QWindowsStyle::pixelMetric(pm, option, widget); - } - - return res; -} - -/* - This function is used by subControlRect to check if a button - should be drawn for the given subControl given a set of window flags. -*/ -static bool buttonVisible(const QStyle::SubControl sc, const QStyleOptionTitleBar *tb) -{ - bool isMinimized = tb->titleBarState & Qt::WindowMinimized; - bool isMaximized = tb->titleBarState & Qt::WindowMaximized; - const uint flags = tb->titleBarFlags; - bool retVal = false; - switch (sc) { - case QStyle::SC_TitleBarContextHelpButton: - if (flags & Qt::WindowContextHelpButtonHint) - retVal = true; - break; - case QStyle::SC_TitleBarMinButton: - if (!isMinimized && (flags & Qt::WindowMinimizeButtonHint)) - retVal = true; - break; - case QStyle::SC_TitleBarNormalButton: - if (isMinimized && (flags & Qt::WindowMinimizeButtonHint)) - retVal = true; - else if (isMaximized && (flags & Qt::WindowMaximizeButtonHint)) - retVal = true; - break; - case QStyle::SC_TitleBarMaxButton: - if (!isMaximized && (flags & Qt::WindowMaximizeButtonHint)) - retVal = true; - break; - case QStyle::SC_TitleBarShadeButton: - if (!isMinimized && flags & Qt::WindowShadeButtonHint) - retVal = true; - break; - case QStyle::SC_TitleBarUnshadeButton: - if (isMinimized && flags & Qt::WindowShadeButtonHint) - retVal = true; - break; - case QStyle::SC_TitleBarCloseButton: - if (flags & Qt::WindowSystemMenuHint) - retVal = true; - break; - case QStyle::SC_TitleBarSysMenu: - if (flags & Qt::WindowSystemMenuHint) - retVal = true; - break; - default : - retVal = true; - } - return retVal; -} - -/*! - \reimp -*/ -QRect QWindowsXPStyle::subControlRect(ComplexControl cc, const QStyleOptionComplex *option, - SubControl subControl, const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::subControlRect(cc, option, subControl, widget); - - QRect rect; - - switch (cc) { - case CC_TitleBar: - if (const auto *tb = qstyleoption_cast<const QStyleOptionTitleBar *>(option)) { - if (!buttonVisible(subControl, tb)) - return rect; - const bool isToolTitle = false; - const int height = tb->rect.height(); - const int width = tb->rect.width(); - const int buttonMargin = int(QStyleHelper::dpiScaled(4, option)); - const qreal factor = QWindowsStylePrivate::nativeMetricScaleFactor(widget); - int buttonHeight = qRound(qreal(GetSystemMetrics(SM_CYSIZE)) * factor) - - buttonMargin; - int buttonWidth = qRound(qreal(GetSystemMetrics(SM_CXSIZE)) * factor) - - buttonMargin; - const int delta = buttonWidth + 2; - int controlTop = option->rect.bottom() - buttonHeight - 2; - const int frameWidth = proxy()->pixelMetric(PM_MdiSubWindowFrameWidth, option, widget); - const bool sysmenuHint = (tb->titleBarFlags & Qt::WindowSystemMenuHint) != 0; - const bool minimizeHint = (tb->titleBarFlags & Qt::WindowMinimizeButtonHint) != 0; - const bool maximizeHint = (tb->titleBarFlags & Qt::WindowMaximizeButtonHint) != 0; - const bool contextHint = (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) != 0; - const bool shadeHint = (tb->titleBarFlags & Qt::WindowShadeButtonHint) != 0; - bool isMinimized = tb->titleBarState & Qt::WindowMinimized; - bool isMaximized = tb->titleBarState & Qt::WindowMaximized; - int offset = 0; - - switch (subControl) { - case SC_TitleBarLabel: - rect = QRect(frameWidth, 0, width - (buttonWidth + frameWidth + 10), height); - if (isToolTitle) { - if (sysmenuHint) { - rect.adjust(0, 0, -buttonWidth - 3, 0); - } - if (minimizeHint || maximizeHint) - rect.adjust(0, 0, -buttonWidth - 2, 0); - } else { - if (sysmenuHint) { - const int leftOffset = height - 8; - rect.adjust(leftOffset, 0, 0, 0); - } - if (minimizeHint) - rect.adjust(0, 0, -buttonWidth - 2, 0); - if (maximizeHint) - rect.adjust(0, 0, -buttonWidth - 2, 0); - if (contextHint) - rect.adjust(0, 0, -buttonWidth - 2, 0); - if (shadeHint) - rect.adjust(0, 0, -buttonWidth - 2, 0); - } - break; - - case SC_TitleBarContextHelpButton: - if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) - offset += delta; - Q_FALLTHROUGH(); - case SC_TitleBarMinButton: - if (!isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) - offset += delta; - else if (subControl == SC_TitleBarMinButton) - break; - Q_FALLTHROUGH(); - case SC_TitleBarNormalButton: - if (isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) - offset += delta; - else if (isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) - offset += delta; - else if (subControl == SC_TitleBarNormalButton) - break; - Q_FALLTHROUGH(); - case SC_TitleBarMaxButton: - if (!isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) - offset += delta; - else if (subControl == SC_TitleBarMaxButton) - break; - Q_FALLTHROUGH(); - case SC_TitleBarShadeButton: - if (!isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) - offset += delta; - else if (subControl == SC_TitleBarShadeButton) - break; - Q_FALLTHROUGH(); - case SC_TitleBarUnshadeButton: - if (isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) - offset += delta; - else if (subControl == SC_TitleBarUnshadeButton) - break; - Q_FALLTHROUGH(); - case SC_TitleBarCloseButton: - if (tb->titleBarFlags & Qt::WindowSystemMenuHint) - offset += delta; - else if (subControl == SC_TitleBarCloseButton) - break; - - rect.setRect(width - offset - controlTop + 1, controlTop, - buttonWidth, buttonHeight); - break; - - case SC_TitleBarSysMenu: { - const int controlTop = 6; - const int controlHeight = height - controlTop - 3; - const int iconExtent = proxy()->pixelMetric(PM_SmallIconSize, option); - QSize iconSize = tb->icon.actualSize(QSize(iconExtent, iconExtent)); - if (tb->icon.isNull()) - iconSize = QSize(controlHeight, controlHeight); - int hPad = (controlHeight - iconSize.height())/2; - int vPad = (controlHeight - iconSize.width())/2; - rect = QRect(frameWidth + hPad, controlTop + vPad, iconSize.width(), iconSize.height()); - break; - } - default: - break; - } - } - break; - - case CC_ComboBox: - if (const auto *cmb = qstyleoption_cast<const QStyleOptionComboBox *>(option)) { - const int x = cmb->rect.x(), y = cmb->rect.y(), wi = cmb->rect.width(), he = cmb->rect.height(); - const int xpos = x + wi - qRound(QStyleHelper::dpiScaled(1 + 16, option)); - - switch (subControl) { - case SC_ComboBoxFrame: - rect = cmb->rect; - break; - - case SC_ComboBoxArrow: { - const qreal dpi = QStyleHelper::dpi(option); - rect = QRect(xpos, y + qRound(QStyleHelper::dpiScaled(1, dpi)), - qRound(QStyleHelper::dpiScaled(16, dpi)), - he - qRound(QStyleHelper::dpiScaled(2, dpi))); - break; - } - - case SC_ComboBoxEditField: { - const qreal dpi = QStyleHelper::dpi(option); - const int frame = qRound(QStyleHelper::dpiScaled(2, dpi)); - rect = QRect(x + frame, y + frame, - wi - qRound(QStyleHelper::dpiScaled(3 + 16, dpi)), - he - qRound(QStyleHelper::dpiScaled(4, dpi))); - break; - } - - case SC_ComboBoxListBoxPopup: - rect = cmb->rect; - break; - - default: - break; - } - } - break; -#if QT_CONFIG(mdiarea) - case CC_MdiControls: { - int numSubControls = 0; - if (option->subControls & SC_MdiCloseButton) - ++numSubControls; - if (option->subControls & SC_MdiMinButton) - ++numSubControls; - if (option->subControls & SC_MdiNormalButton) - ++numSubControls; - if (numSubControls == 0) - break; - - int buttonWidth = option->rect.width() / numSubControls; - int offset = 0; - switch (subControl) { - case SC_MdiCloseButton: - // Only one sub control, no offset needed. - if (numSubControls == 1) - break; - offset += buttonWidth; - Q_FALLTHROUGH(); - case SC_MdiNormalButton: - // No offset needed if - // 1) There's only one sub control - // 2) We have a close button and a normal button (offset already added in SC_MdiClose) - if (numSubControls == 1 || (numSubControls == 2 && !(option->subControls & SC_MdiMinButton))) - break; - if (option->subControls & SC_MdiNormalButton) - offset += buttonWidth; - break; - default: - break; - } - rect = QRect(offset, 0, buttonWidth, option->rect.height()); - break; - } -#endif // QT_CONFIG(mdiarea) - - default: - rect = visualRect(option->direction, option->rect, - QWindowsStyle::subControlRect(cc, option, subControl, widget)); - break; - } - return visualRect(option->direction, option->rect, rect); -} - -/*! - \reimp -*/ -QSize QWindowsXPStyle::sizeFromContents(ContentsType ct, const QStyleOption *option, - const QSize &contentsSize, const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::sizeFromContents(ct, option, contentsSize, widget); - - QSize sz(contentsSize); - switch (ct) { - case CT_LineEdit: - case CT_ComboBox: { - XPThemeData buttontheme(widget, nullptr, QWindowsXPStylePrivate::ButtonTheme, BP_PUSHBUTTON, PBS_NORMAL); - if (buttontheme.isValid()) { - const qreal factor = QWindowsXPStylePrivate::nativeMetricScaleFactor(widget); - const QMarginsF borderSize = buttontheme.margins() * factor; - if (!borderSize.isNull()) { - const qreal margin = qreal(2) * factor; - sz.rwidth() += qRound(borderSize.left() + borderSize.right() - margin); - sz.rheight() += int(borderSize.bottom() + borderSize.top() - margin - + qreal(1) / factor - 1); - } - const int textMargins = 2*(proxy()->pixelMetric(PM_FocusFrameHMargin, option) + 1); - sz += QSize(qMax(pixelMetric(QStyle::PM_ScrollBarExtent, option, widget) - + textMargins, 23), 0); //arrow button - } - break; - } - case CT_TabWidget: - sz += QSize(6, 6); - break; - case CT_Menu: - sz += QSize(1, 0); - break; -#if QT_CONFIG(menubar) - case CT_MenuBarItem: - if (!sz.isEmpty()) - sz += QSize(windowsItemHMargin * 5 + 1, 6); - break; -#endif - case CT_MenuItem: - if (const auto *menuitem = qstyleoption_cast<const QStyleOptionMenuItem *>(option)) { - if (menuitem->menuItemType != QStyleOptionMenuItem::Separator) { - sz = QWindowsStyle::sizeFromContents(ct, option, sz, widget); - sz.setHeight(sz.height() - 2); - return sz; - } - } - sz = QWindowsStyle::sizeFromContents(ct, option, sz, widget); - break; - - case CT_MdiControls: { - sz.setHeight(int(QStyleHelper::dpiScaled(19, option))); - int width = 54; - if (const auto *styleOpt = qstyleoption_cast<const QStyleOptionComplex *>(option)) { - width = 0; - if (styleOpt->subControls & SC_MdiMinButton) - width += 17 + 1; - if (styleOpt->subControls & SC_MdiNormalButton) - width += 17 + 1; - if (styleOpt->subControls & SC_MdiCloseButton) - width += 17 + 1; - } - sz.setWidth(int(QStyleHelper::dpiScaled(width, option))); - break; - } - - default: - sz = QWindowsStyle::sizeFromContents(ct, option, sz, widget); - break; - } - - return sz; -} - - -/*! \reimp */ -int QWindowsXPStyle::styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, - QStyleHintReturn *returnData) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::styleHint(hint, option, widget, returnData); - - auto *d = const_cast<QWindowsXPStylePrivate*>(d_func()); - int res = 0; - switch (hint) { - case SH_EtchDisabledText: - res = (qobject_cast<const QLabel*>(widget) != 0); - break; - - case SH_SpinControls_DisableOnBounds: - res = 0; - break; - - case SH_TitleBar_AutoRaise: - case SH_TitleBar_NoBorder: - res = 1; - break; - - case SH_GroupBox_TextLabelColor: - if (!widget || widget->isEnabled()) - res = d->groupBoxTextColor; - else - res = d->groupBoxTextColorDisabled; - break; - - case SH_Table_GridLineColor: - res = 0xC0C0C0; - break; - - case SH_WindowFrame_Mask: { - res = 1; - auto *mask = qstyleoption_cast<QStyleHintReturnMask *>(returnData); - const auto *titlebar = qstyleoption_cast<const QStyleOptionTitleBar *>(option); - if (mask && titlebar) { - // Note certain themes will not return the whole window frame but only the titlebar part when - // queried This function needs to return the entire window mask, hence we will only fetch the mask for the - // titlebar itself and add the remaining part of the window rect at the bottom. - int tbHeight = proxy()->pixelMetric(PM_TitleBarHeight, option, widget); - QRect titleBarRect = option->rect; - titleBarRect.setHeight(tbHeight); - XPThemeData themeData; - if (titlebar->titleBarState & Qt::WindowMinimized) { - themeData = XPThemeData(widget, nullptr, - QWindowsXPStylePrivate::WindowTheme, - WP_MINCAPTION, CS_ACTIVE, titleBarRect); - } else - themeData = XPThemeData(widget, nullptr, - QWindowsXPStylePrivate::WindowTheme, - WP_CAPTION, CS_ACTIVE, titleBarRect); - mask->region = d->region(themeData) + - QRect(0, tbHeight, option->rect.width(), option->rect.height() - tbHeight); - } - break; - } -#if QT_CONFIG(rubberband) - case SH_RubberBand_Mask: - if (qstyleoption_cast<const QStyleOptionRubberBand *>(option)) - res = 0; - break; -#endif // QT_CONFIG(rubberband) - - case SH_ItemView_DrawDelegateFrame: - res = 1; - break; - - default: - res = QWindowsStyle::styleHint(hint, option, widget, returnData); - } - - return res; -} - -/*! \reimp */ -QPalette QWindowsXPStyle::standardPalette() const -{ - return QWindowsXPStylePrivate::useXP() ? QPalette() : QWindowsStyle::standardPalette(); -} - -/*! - \reimp -*/ -QPixmap QWindowsXPStyle::standardPixmap(StandardPixmap standardPixmap, const QStyleOption *option, - const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::standardPixmap(standardPixmap, option, widget); - - switch(standardPixmap) { - case SP_TitleBarMaxButton: - case SP_TitleBarCloseButton: - if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { - if (widget && widget->isWindow()) { - XPThemeData theme(widget, nullptr, QWindowsXPStylePrivate::WindowTheme, WP_SMALLCLOSEBUTTON, CBS_NORMAL); - if (theme.isValid()) { - const QSize size = (theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); - return QIcon(QWindowsStyle::standardPixmap(standardPixmap, option, widget)).pixmap(size); - } - } - } - break; - default: - break; - } - return QWindowsStyle::standardPixmap(standardPixmap, option, widget); -} - -/*! - \reimp -*/ -QIcon QWindowsXPStyle::standardIcon(StandardPixmap standardIcon, - const QStyleOption *option, - const QWidget *widget) const -{ - if (!QWindowsXPStylePrivate::useXP()) - return QWindowsStyle::standardIcon(standardIcon, option, widget); - - auto *d = const_cast<QWindowsXPStylePrivate*>(d_func()); - switch(standardIcon) { - case SP_TitleBarMaxButton: - if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { - if (d->dockFloat.isNull()) { - XPThemeData themeSize(nullptr, nullptr, QWindowsXPStylePrivate::WindowTheme, - WP_SMALLCLOSEBUTTON, CBS_NORMAL); - XPThemeData theme(nullptr, nullptr, QWindowsXPStylePrivate::WindowTheme, - WP_MAXBUTTON, MAXBS_NORMAL); - if (theme.isValid()) { - const QSize size = (themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); - QPixmap pm(size); - pm.fill(Qt::transparent); - QPainter p(&pm); - theme.painter = &p; - theme.rect = QRect(QPoint(0, 0), size); - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal - pm.fill(Qt::transparent); - theme.stateId = MAXBS_PUSHED; - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed - pm.fill(Qt::transparent); - theme.stateId = MAXBS_HOT; - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover - pm.fill(Qt::transparent); - theme.stateId = MAXBS_INACTIVE; - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled - } - } - if (widget && widget->isWindow()) - return d->dockFloat; - } - break; - case SP_TitleBarCloseButton: - if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { - if (d->dockClose.isNull()) { - XPThemeData theme(nullptr, nullptr, QWindowsXPStylePrivate::WindowTheme, - WP_SMALLCLOSEBUTTON, CBS_NORMAL); - if (theme.isValid()) { - const QSize size = (theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); - QPixmap pm(size); - pm.fill(Qt::transparent); - QPainter p(&pm); - theme.painter = &p; - theme.partId = WP_CLOSEBUTTON; // #### - theme.rect = QRect(QPoint(0, 0), size); - d->drawBackground(theme); - d->dockClose.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal - pm.fill(Qt::transparent); - theme.stateId = CBS_PUSHED; - d->drawBackground(theme); - d->dockClose.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed - pm.fill(Qt::transparent); - theme.stateId = CBS_HOT; - d->drawBackground(theme); - d->dockClose.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover - pm.fill(Qt::transparent); - theme.stateId = CBS_INACTIVE; - d->drawBackground(theme); - d->dockClose.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled - } - } - if (widget && widget->isWindow()) - return d->dockClose; - } - break; - case SP_TitleBarNormalButton: - if (qstyleoption_cast<const QStyleOptionDockWidget *>(option)) { - if (d->dockFloat.isNull()) { - XPThemeData themeSize(nullptr, nullptr, QWindowsXPStylePrivate::WindowTheme, - WP_SMALLCLOSEBUTTON, CBS_NORMAL); - XPThemeData theme(nullptr, nullptr, QWindowsXPStylePrivate::WindowTheme, - WP_RESTOREBUTTON, RBS_NORMAL); - if (theme.isValid()) { - const QSize size = (themeSize.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget)).toSize(); - QPixmap pm(size); - pm.fill(Qt::transparent); - QPainter p(&pm); - theme.painter = &p; - theme.rect = QRect(QPoint(0, 0), size); - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::Off); // Normal - pm.fill(Qt::transparent); - theme.stateId = RBS_PUSHED; - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Normal, QIcon::On); // Pressed - pm.fill(Qt::transparent); - theme.stateId = RBS_HOT; - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Active, QIcon::Off); // Hover - pm.fill(Qt::transparent); - theme.stateId = RBS_INACTIVE; - d->drawBackground(theme); - d->dockFloat.addPixmap(pm, QIcon::Disabled, QIcon::Off); // Disabled - } - } - if (widget && widget->isWindow()) - return d->dockFloat; - } - break; - default: - break; - } - - return QWindowsStyle::standardIcon(standardIcon, option, widget); -} - -#ifndef QT_NO_DEBUG_STREAM -QDebug operator<<(QDebug d, const XPThemeData &t) -{ - QDebugStateSaver saver(d); - d.nospace(); - d << "XPThemeData(" << t.widget << ", theme=#" << t.theme << ", " << t.htheme - << ", partId=" << t.partId << ", stateId=" << t.stateId << ", rect=" << t.rect - << ", mirrorHorizontally=" << t.mirrorHorizontally << ", mirrorVertically=" - << t.mirrorVertically << ", noBorder=" << t.noBorder << ", noContent=" << t.noContent - << ", rotate=" << t.rotate << ')'; - return d; -} - -QDebug operator<<(QDebug d, const ThemeMapKey &k) -{ - QDebugStateSaver saver(d); - d.nospace(); - d << "ThemeMapKey(theme=#" << k.theme - << ", partId=" << k.partId << ", stateId=" << k.stateId - << ", noBorder=" << k.noBorder << ", noContent=" << k.noContent << ')'; - return d; -} - -QDebug operator<<(QDebug d, const ThemeMapData &td) -{ - QDebugStateSaver saver(d); - d.nospace(); - d << "ThemeMapData(alphaType=" << td.alphaType - << ", dataValid=" << td.dataValid << ", partIsTransparent=" << td.partIsTransparent - << ", hasAlphaChannel=" << td.hasAlphaChannel << ", wasAlphaSwapped=" << td.wasAlphaSwapped - << ", hadInvalidAlpha=" << td.hadInvalidAlpha << ')'; - return d; -} -#endif // QT_NO_DEBUG_STREAM - -// Debugging code ---------------------------------------------------------------------[ START ]--- -// The code for this point on is not compiled by default, but only used as assisting -// debugging code when you uncomment the DEBUG_XP_STYLE define at the top of the file. - -#ifdef DEBUG_XP_STYLE -// The schema file expects these to be defined by the user. -#define TMT_ENUMDEF 8 -#define TMT_ENUMVAL TEXT('A') -#define TMT_ENUM TEXT('B') -#define SCHEMA_STRINGS // For 2nd pass on schema file -QT_BEGIN_INCLUDE_NAMESPACE -#include <tmschema.h> -QT_END_INCLUDE_NAMESPACE - -// A property's value, type and name combo -struct PropPair { - int propValue; - int propType; - LPCWSTR propName; -}; - -// Operator for sorting of PropPairs -bool operator<(PropPair a, PropPair b) { - return wcscmp(a.propName, b.propName) < 0; -} - -// Our list of all possible properties -static QList<PropPair> all_props; - - -/*! \internal - Dumps a portion of the full native DIB section double buffer. - The DIB section double buffer is only used when doing special - transformations to the theme part, or when the real double - buffer in the paintengine does not have an HDC we may use - directly. - Since we cannot rely on the pixel data we get from Microsoft - when drawing into the DIB section, we use this function to - see the actual data we got, and can determine the appropriate - action. -*/ -void QWindowsXPStylePrivate::dumpNativeDIB(int w, int h) -{ - if (w && h) { - static int pCount = 0; - DWORD *bufPix = (DWORD*)bufferPixels; - - char *bufferDump = new char[bufferH * bufferW * 16]; - char *bufferPos = bufferDump; - - memset(bufferDump, 0, sizeof(bufferDump)); - bufferPos += sprintf(bufferPos, "const int pixelBufferW%d = %d;\n", pCount, w); - bufferPos += sprintf(bufferPos, "const int pixelBufferH%d = %d;\n", pCount, h); - bufferPos += sprintf(bufferPos, "const unsigned DWORD pixelBuffer%d[] = {", pCount); - for (int iy = 0; iy < h; ++iy) { - bufferPos += sprintf(bufferPos, "\n "); - bufPix = (DWORD*)(bufferPixels + (iy * bufferW * 4)); - for (int ix = 0; ix < w; ++ix) { - bufferPos += sprintf(bufferPos, "0x%08x, ", *bufPix); - ++bufPix; - } - } - bufferPos += sprintf(bufferPos, "\n};\n\n"); - printf(bufferDump); - - delete[] bufferDump; - ++pCount; - } -} - -/*! \internal - Shows the value of a given property for a part. -*/ -static void showProperty(XPThemeData &themeData, const PropPair &prop) -{ - PROPERTYORIGIN origin = PO_NOTFOUND; - GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &origin); - const char *originStr; - switch(origin) { - case PO_STATE: - originStr = "State "; - break; - case PO_PART: - originStr = "Part "; - break; - case PO_CLASS: - originStr = "Class "; - break; - case PO_GLOBAL: - originStr = "Globl "; - break; - case PO_NOTFOUND: - default: - originStr = "Unkwn "; - break; - } - - switch(prop.propType) { - case TMT_STRING: - { - wchar_t buffer[512]; - GetThemeString(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, buffer, 512); - printf(" (%sString) %-20S: %S\n", originStr, prop.propName, buffer); - } - break; - case TMT_ENUM: - { - int result = -1; - GetThemeEnumValue(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sEnum) %-20S: %d\n", originStr, prop.propName, result); - } - break; - case TMT_INT: - { - int result = -1; - GetThemeInt(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sint) %-20S: %d\n", originStr, prop.propName, result); - } - break; - case TMT_BOOL: - { - BOOL result = false; - GetThemeBool(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sbool) %-20S: %d\n", originStr, prop.propName, result); - } - break; - case TMT_COLOR: - { - COLORREF result = 0; - GetThemeColor(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%scolor) %-20S: 0x%08X\n", originStr, prop.propName, result); - } - break; - case TMT_MARGINS: - { - MARGINS result; - memset(&result, 0, sizeof(result)); - GetThemeMargins(themeData.handle(), 0, themeData.partId, themeData.stateId, prop.propValue, 0, &result); - printf(" (%smargins) %-20S: (%d, %d, %d, %d)\n", originStr, - prop.propName, result.cxLeftWidth, result.cyTopHeight, result.cxRightWidth, result.cyBottomHeight); - } - break; - case TMT_FILENAME: - { - wchar_t buffer[512]; - GetThemeFilename(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, buffer, 512); - printf(" (%sfilename)%-20S: %S\n", originStr, prop.propName, buffer); - } - break; - case TMT_SIZE: - { - SIZE result1; - SIZE result2; - SIZE result3; - memset(&result1, 0, sizeof(result1)); - memset(&result2, 0, sizeof(result2)); - memset(&result3, 0, sizeof(result3)); - GetThemePartSize(themeData.handle(), 0, themeData.partId, themeData.stateId, 0, TS_MIN, &result1); - GetThemePartSize(themeData.handle(), 0, themeData.partId, themeData.stateId, 0, TS_TRUE, &result2); - GetThemePartSize(themeData.handle(), 0, themeData.partId, themeData.stateId, 0, TS_DRAW, &result3); - printf(" (%ssize) %-20S: Min (%d, %d), True(%d, %d), Draw(%d, %d)\n", originStr, prop.propName, - result1.cx, result1.cy, result2.cx, result2.cy, result3.cx, result3.cy); - } - break; - case TMT_POSITION: - { - POINT result; - memset(&result, 0, sizeof(result)); - GetThemePosition(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sPosition)%-20S: (%d, %d)\n", originStr, prop.propName, result.x, result.y); - } - break; - case TMT_RECT: - { - RECT result; - memset(&result, 0, sizeof(result)); - GetThemeRect(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sRect) %-20S: (%d, %d, %d, %d)\n", originStr, prop.propName, result.left, result.top, result.right, result.bottom); - } - break; - case TMT_FONT: - { - LOGFONT result; - memset(&result, 0, sizeof(result)); - GetThemeFont(themeData.handle(), 0, themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sFont) %-20S: %S height(%d) width(%d) weight(%d)\n", originStr, prop.propName, - result.lfFaceName, result.lfHeight, result.lfWidth, result.lfWeight); - } - break; - case TMT_INTLIST: - { - INTLIST result; - memset(&result, 0, sizeof(result)); - GetThemeIntList(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &result); - printf(" (%sInt list)%-20S: { ", originStr, prop.propName); - for (int i = 0; i < result.iValueCount; ++i) - printf("%d ", result.iValues[i]); - printf("}\n"); - } - break; - default: - printf(" %s%S : Unknown property type (%d)!\n", originStr, prop.propName, prop.propType); - } -} - -/*! \internal - Dump all valid properties for a part. - If it's the first time this function is called, then the name, - enum value and documentation of all properties are shown, as - well as all global properties. -*/ -void QWindowsXPStylePrivate::showProperties(XPThemeData &themeData) -{ - if (!all_props.count()) { - const TMSCHEMAINFO *infoTable = GetSchemaInfo(); - for (int i = 0; i < infoTable->iPropCount; ++i) { - int propType = infoTable->pPropTable[i].bPrimVal; - int propValue = infoTable->pPropTable[i].sEnumVal; - LPCWSTR propName = infoTable->pPropTable[i].pszName; - - switch(propType) { - case TMT_ENUMDEF: - case TMT_ENUMVAL: - continue; - default: - if (propType != propValue) { - PropPair prop; - prop.propValue = propValue; - prop.propName = propName; - prop.propType = propType; - all_props.append(prop); - } - } - } - std::sort(all_props.begin(), all_props.end()); - - {// List all properties - printf("part properties count = %d:\n", all_props.count()); - printf(" Enum Property Name Description\n"); - printf("-----------------------------------------------------------\n"); - wchar_t themeName[256]; - pGetCurrentThemeName(themeName, 256, 0, 0, 0, 0); - for (int j = 0; j < all_props.count(); ++j) { - PropPair prop = all_props.at(j); - wchar_t buf[500]; - GetThemeDocumentationProperty(themeName, prop.propName, buf, 500); - printf("%3d: (%4d) %-20S %S\n", j, prop.propValue, prop.propName, buf); - } - } - - {// Show Global values - printf("Global Properties:\n"); - for (int j = 0; j < all_props.count(); ++j) { - PropPair prop = all_props.at(j); - PROPERTYORIGIN origin = PO_NOTFOUND; - GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &origin); - if (origin == PO_GLOBAL) { - showProperty(themeData, prop); - } - } - } - } - - for (int j = 0; j < all_props.count(); ++j) { - PropPair prop = all_props.at(j); - PROPERTYORIGIN origin = PO_NOTFOUND; - GetThemePropertyOrigin(themeData.handle(), themeData.partId, themeData.stateId, prop.propValue, &origin); - if (origin != PO_NOTFOUND) { - showProperty(themeData, prop); - } - } -} -#endif -// Debugging code -----------------------------------------------------------------------[ END ]--- - - -QT_END_NAMESPACE diff --git a/src/plugins/styles/windowsvista/qwindowsxpstyle_p.h b/src/plugins/styles/windowsvista/qwindowsxpstyle_p.h deleted file mode 100644 index 6fa7105ea6..0000000000 --- a/src/plugins/styles/windowsvista/qwindowsxpstyle_p.h +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QWINDOWSXPSTYLE_P_H -#define QWINDOWSXPSTYLE_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -#include <QtWidgets/private/qtwidgetsglobal_p.h> -#include <QtWidgets/private/qwindowsstyle_p.h> - -QT_BEGIN_NAMESPACE - -class QWindowsXPStylePrivate; -class QWindowsXPStyle : public QWindowsStyle -{ - Q_OBJECT -public: - ~QWindowsXPStyle() override; - - void polish(QWidget*) override; - void polish(QPalette&) override; - void unpolish(QWidget*) override; - - void drawPrimitive(PrimitiveElement pe, const QStyleOption *option, QPainter *p, - const QWidget *widget = nullptr) const override; - void drawControl(ControlElement element, const QStyleOption *option, QPainter *p, - const QWidget *wwidget = nullptr) const override; - QRect subElementRect(SubElement r, const QStyleOption *option, - const QWidget *widget = nullptr) const override; - QRect subControlRect(ComplexControl cc, const QStyleOptionComplex *option, SubControl sc, - const QWidget *widget = nullptr) const override; - void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *option, QPainter *p, - const QWidget *widget = nullptr) const override; - QSize sizeFromContents(ContentsType ct, const QStyleOption *option, const QSize &contentsSize, - const QWidget *widget = nullptr) const override; - int pixelMetric(PixelMetric pm, const QStyleOption *option = nullptr, - const QWidget *widget = nullptr) const override; - int styleHint(StyleHint hint, const QStyleOption *option = nullptr, - const QWidget *widget = nullptr, - QStyleHintReturn *returnData = nullptr) const override; - - QPalette standardPalette() const override; - QPixmap standardPixmap(StandardPixmap standardIcon, const QStyleOption *option, - const QWidget *widget = nullptr) const override; - QIcon standardIcon(StandardPixmap standardIcon, const QStyleOption *option = nullptr, - const QWidget *widget = nullptr) const override; - -protected: - QWindowsXPStyle(QWindowsXPStylePrivate &dd); - -private: - Q_DISABLE_COPY_MOVE(QWindowsXPStyle) - Q_DECLARE_PRIVATE(QWindowsXPStyle) - friend class QStyleFactory; -}; - -QT_END_NAMESPACE - -#endif // QWINDOWSXPSTYLE_P_H diff --git a/src/plugins/styles/windowsvista/qwindowsxpstyle_p_p.h b/src/plugins/styles/windowsvista/qwindowsxpstyle_p_p.h deleted file mode 100644 index fbbd493f54..0000000000 --- a/src/plugins/styles/windowsvista/qwindowsxpstyle_p_p.h +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QWINDOWSXPSTYLE_P_P_H -#define QWINDOWSXPSTYLE_P_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of qapplication_*.cpp, qwidget*.cpp and qfiledialog.cpp. This header -// file may change from version to version without notice, or even be removed. -// -// We mean it. -// - -#include <QtWidgets/private/qtwidgetsglobal_p.h> -#include "qwindowsxpstyle_p.h" -#include <QtWidgets/private/qwindowsstyle_p_p.h> -#include <qmap.h> -#include <qt_windows.h> - -#include <uxtheme.h> -#include <vssym32.h> - -#include <limits.h> - -QT_BEGIN_NAMESPACE - -class QDebug; - -// TMT_TEXTSHADOWCOLOR is wrongly defined in mingw -#if TMT_TEXTSHADOWCOLOR != 3818 -#undef TMT_TEXTSHADOWCOLOR -#define TMT_TEXTSHADOWCOLOR 3818 -#endif -#ifndef TST_NONE -# define TST_NONE 0 -#endif - -// These defines are missing from the tmschema, but still exist as -// states for their parts -#ifndef MINBS_INACTIVE -#define MINBS_INACTIVE 5 -#endif -#ifndef MAXBS_INACTIVE -#define MAXBS_INACTIVE 5 -#endif -#ifndef RBS_INACTIVE -#define RBS_INACTIVE 5 -#endif -#ifndef HBS_INACTIVE -#define HBS_INACTIVE 5 -#endif -#ifndef CBS_INACTIVE -#define CBS_INACTIVE 5 -#endif - -// Uncomment define below to build debug assisting code, and output -// #define DEBUG_XP_STYLE - -// Declarations ----------------------------------------------------------------------------------- -class XPThemeData -{ -public: - explicit XPThemeData(const QWidget *w = nullptr, QPainter *p = nullptr, int themeIn = -1, - int part = 0, int state = 0, const QRect &r = QRect()) - : widget(w), painter(p), theme(themeIn), partId(part), stateId(state), - mirrorHorizontally(false), mirrorVertically(false), noBorder(false), - noContent(false), rect(r) - {} - - HRGN mask(QWidget *widget); - HTHEME handle(); - - static RECT toRECT(const QRect &qr); - bool isValid(); - - QSizeF size(); - QMarginsF margins(const QRect &rect, int propId = TMT_CONTENTMARGINS); - QMarginsF margins(int propId = TMT_CONTENTMARGINS); - - static QSizeF themeSize(const QWidget *w = nullptr, QPainter *p = nullptr, int themeIn = -1, int part = 0, int state = 0); - static QMarginsF themeMargins(const QRect &rect, const QWidget *w = nullptr, QPainter *p = nullptr, int themeIn = -1, - int part = 0, int state = 0, int propId = TMT_CONTENTMARGINS); - static QMarginsF themeMargins(const QWidget *w = nullptr, QPainter *p = nullptr, int themeIn = -1, - int part = 0, int state = 0, int propId = TMT_CONTENTMARGINS); - - const QWidget *widget; - QPainter *painter; - - int theme; - HTHEME htheme = nullptr; - int partId; - int stateId; - - uint mirrorHorizontally : 1; - uint mirrorVertically : 1; - uint noBorder : 1; - uint noContent : 1; - uint rotate = 0; - QRect rect; -}; - -struct ThemeMapKey { - int theme = 0; - int partId = -1; - int stateId = -1; - bool noBorder = false; - bool noContent = false; - - ThemeMapKey() = default; - ThemeMapKey(const XPThemeData &data) - : theme(data.theme), partId(data.partId), stateId(data.stateId), - noBorder(data.noBorder), noContent(data.noContent) {} - -}; - -inline size_t qHash(const ThemeMapKey &key) -{ return key.theme ^ key.partId ^ key.stateId; } - -inline bool operator==(const ThemeMapKey &k1, const ThemeMapKey &k2) -{ - return k1.theme == k2.theme - && k1.partId == k2.partId - && k1.stateId == k2.stateId; -} - -enum AlphaChannelType { - UnknownAlpha = -1, // Alpha of part & state not yet known - NoAlpha, // Totally opaque, no need to touch alpha (RGB) - MaskAlpha, // Alpha channel must be fixed (ARGB) - RealAlpha // Proper alpha values from Windows (ARGB_Premultiplied) -}; - -struct ThemeMapData { - AlphaChannelType alphaType = UnknownAlpha; // Which type of alpha on part & state - - bool dataValid : 1; // Only used to detect if hash value is ok - bool partIsTransparent : 1; - bool hasAlphaChannel : 1; // True = part & state has real Alpha - bool wasAlphaSwapped : 1; // True = alpha channel needs to be swapped - bool hadInvalidAlpha : 1; // True = alpha channel contained invalid alpha values - - ThemeMapData() : dataValid(false), partIsTransparent(false), - hasAlphaChannel(false), wasAlphaSwapped(false), hadInvalidAlpha(false) {} -}; - -#ifndef QT_NO_DEBUG_STREAM -QDebug operator<<(QDebug d, const XPThemeData &t); -QDebug operator<<(QDebug d, const ThemeMapKey &k); -QDebug operator<<(QDebug d, const ThemeMapData &td); -#endif - -class QWindowsXPStylePrivate : public QWindowsStylePrivate -{ - Q_DECLARE_PUBLIC(QWindowsXPStyle) -public: - enum Theme { - ButtonTheme, - ComboboxTheme, - EditTheme, - HeaderTheme, - ListViewTheme, - MenuTheme, - ProgressTheme, - RebarTheme, - ScrollBarTheme, - SpinTheme, - TabTheme, - TaskDialogTheme, - ToolBarTheme, - ToolTipTheme, - TrackBarTheme, - XpTreeViewTheme, // '+'/'-' shape treeview indicators (XP) - WindowTheme, - StatusTheme, - VistaTreeViewTheme, // arrow shape treeview indicators (Vista) obtained from "explorer" theme. - NThemes - }; - - QWindowsXPStylePrivate() - { init(); } - - ~QWindowsXPStylePrivate() - { cleanup(); } - - static int pixelMetricFromSystemDp(QStyle::PixelMetric pm, const QStyleOption *option = nullptr, const QWidget *widget = nullptr); - static int fixedPixelMetric(QStyle::PixelMetric pm, const QStyleOption *option = nullptr, const QWidget *widget = nullptr); - - static HWND winId(const QWidget *widget); - - void init(bool force = false); - void cleanup(bool force = false); - void cleanupHandleMap(); - - HBITMAP buffer(int w = 0, int h = 0); - HDC bufferHDC() - { return bufferDC;} - - static bool useXP(bool update = false); - static QRect scrollBarGripperBounds(QStyle::State flags, const QWidget *widget, XPThemeData *theme); - - bool isTransparent(XPThemeData &themeData); - QRegion region(XPThemeData &themeData); - - bool drawBackground(XPThemeData &themeData, qreal correctionFactor = 1); - bool drawBackgroundThruNativeBuffer(XPThemeData &themeData, qreal aditionalDevicePixelRatio, qreal correctionFactor); - bool drawBackgroundDirectly(HDC dc, XPThemeData &themeData, qreal aditionalDevicePixelRatio); - - bool hasAlphaChannel(const QRect &rect); - bool fixAlphaChannel(const QRect &rect); - bool swapAlphaChannel(const QRect &rect, bool allPixels = false); - - QRgb groupBoxTextColor = 0; - QRgb groupBoxTextColorDisabled = 0; - QRgb sliderTickColor = 0; - bool hasInitColors = false; - - static HTHEME createTheme(int theme, HWND hwnd); - static QString themeName(int theme); - static inline bool hasTheme(int theme) { return theme >= 0 && theme < NThemes && m_themes[theme]; } - static bool isItemViewDelegateLineEdit(const QWidget *widget); - static bool isLineEditBaseColorSet(const QStyleOption *option, const QWidget *widget); - - QIcon dockFloat, dockClose; - -private: -#ifdef DEBUG_XP_STYLE - void dumpNativeDIB(int w, int h); - void showProperties(XPThemeData &themeData); -#endif - - static bool initVistaTreeViewTheming(); - static void cleanupVistaTreeViewTheming(); - - static QBasicAtomicInt ref; - static bool use_xp; - - QHash<ThemeMapKey, ThemeMapData> alphaCache; - HDC bufferDC = nullptr; - HBITMAP bufferBitmap = nullptr; - HBITMAP nullBitmap = nullptr; - uchar *bufferPixels = nullptr; - int bufferW = 0; - int bufferH = 0; - - static HWND m_vistaTreeViewHelper; - static HTHEME m_themes[NThemes]; -}; - -inline QSizeF XPThemeData::size() -{ - QSizeF result(0, 0); - if (isValid()) { - SIZE size; - if (SUCCEEDED(GetThemePartSize(handle(), nullptr, partId, stateId, nullptr, TS_TRUE, &size))) - result = QSize(size.cx, size.cy); - } - return result; -} - -inline QMarginsF XPThemeData::margins(const QRect &qRect, int propId) -{ - QMarginsF result(0, 0, 0 ,0); - if (isValid()) { - MARGINS margins; - RECT rect = XPThemeData::toRECT(qRect); - if (SUCCEEDED(GetThemeMargins(handle(), nullptr, partId, stateId, propId, &rect, &margins))) - result = QMargins(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight); - } - return result; -} - -inline QMarginsF XPThemeData::margins(int propId) -{ - QMarginsF result(0, 0, 0 ,0); - if (isValid()) { - MARGINS margins; - if (SUCCEEDED(GetThemeMargins(handle(), nullptr, partId, stateId, propId, nullptr, &margins))) - result = QMargins(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight); - } - return result; -} - -inline QSizeF XPThemeData::themeSize(const QWidget *w, QPainter *p, int themeIn, int part, int state) -{ - XPThemeData theme(w, p, themeIn, part, state); - return theme.size(); -} - -inline QMarginsF XPThemeData::themeMargins(const QRect &rect, const QWidget *w, QPainter *p, int themeIn, - int part, int state, int propId) -{ - XPThemeData theme(w, p, themeIn, part, state); - return theme.margins(rect, propId); -} - -inline QMarginsF XPThemeData::themeMargins(const QWidget *w, QPainter *p, int themeIn, - int part, int state, int propId) -{ - XPThemeData theme(w, p, themeIn, part, state); - return theme.margins(propId); -} - -QT_END_NAMESPACE - -#endif //QWINDOWSXPSTYLE_P_P_H diff --git a/src/plugins/styles/windowsvista/windowsvistastyle.json b/src/plugins/styles/windowsvista/windowsvistastyle.json deleted file mode 100644 index 771aa0c600..0000000000 --- a/src/plugins/styles/windowsvista/windowsvistastyle.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Keys": [ "windowsvista" ] -} diff --git a/src/plugins/tls/CMakeLists.txt b/src/plugins/tls/CMakeLists.txt index a17cda9594..91b9267123 100644 --- a/src/plugins/tls/CMakeLists.txt +++ b/src/plugins/tls/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + if(QT_FEATURE_securetransport) add_subdirectory(securetransport) endif() diff --git a/src/plugins/tls/certonly/CMakeLists.txt b/src/plugins/tls/certonly/CMakeLists.txt index 14b769cbba..495f408144 100644 --- a/src/plugins/tls/certonly/CMakeLists.txt +++ b/src/plugins/tls/certonly/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QTlsBackendCertOnlyPlugin OUTPUT_NAME qcertonlybackend CLASS_NAME QTlsBackendCertOnly diff --git a/src/plugins/tls/openssl/CMakeLists.txt b/src/plugins/tls/openssl/CMakeLists.txt index a7fb54f08c..0e0a7a1552 100644 --- a/src/plugins/tls/openssl/CMakeLists.txt +++ b/src/plugins/tls/openssl/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QTlsBackendOpenSSLPlugin OUTPUT_NAME qopensslbackend CLASS_NAME QTlsBackendOpenSSL @@ -21,6 +24,10 @@ qt_internal_add_plugin(QTlsBackendOpenSSLPlugin OPENSSL_API_COMPAT=0x10100000L ) +if (WIN32) # Windows header issues + set_target_properties(QTlsBackendOpenSSLPlugin PROPERTIES UNITY_BUILD OFF) +endif() + qt_internal_extend_target(QTlsBackendOpenSSLPlugin CONDITION QT_FEATURE_dtls SOURCES qdtls_openssl.cpp qdtls_openssl_p.h @@ -48,12 +55,9 @@ qt_internal_extend_target(QTlsBackendOpenSSLPlugin CONDITION WIN32 crypt32 ) -qt_internal_extend_target(QTlsBackendOpenSSLPlugin CONDITION QT_FEATURE_openssl_linked - LIBRARIES - WrapOpenSSL::WrapOpenSSL -) - -qt_internal_extend_target(QTlsBackendOpenSSLPlugin CONDITION NOT QT_FEATURE_openssl_linked - LIBRARIES - WrapOpenSSLHeaders::WrapOpenSSLHeaders -) +if(QT_FEATURE_openssl_linked) + target_link_libraries(QTlsBackendOpenSSLPlugin PRIVATE WrapOpenSSL::WrapOpenSSL) +else() + qt_internal_add_target_include_dirs(QTlsBackendOpenSSLPlugin + WrapOpenSSLHeaders::WrapOpenSSLHeaders) +endif() diff --git a/src/plugins/tls/openssl/qdtls_openssl.cpp b/src/plugins/tls/openssl/qdtls_openssl.cpp index fe8cbf23e0..fc07a29ec8 100644 --- a/src/plugins/tls/openssl/qdtls_openssl.cpp +++ b/src/plugins/tls/openssl/qdtls_openssl.cpp @@ -1,11 +1,7 @@ // Copyright (C) 2018 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#ifndef NOMINMAX -#define NOMINMAX -#endif // NOMINMAX - -#include <QtNetwork/private/qnativesocketengine_p.h> +#include <QtNetwork/private/qnativesocketengine_p_p.h> #include "qsslsocket_openssl_symbols_p.h" #include "qdtls_openssl_p.h" @@ -182,7 +178,7 @@ extern "C" int q_generate_cookie_callback(SSL *ssl, unsigned char *dst, QMessageAuthenticationCode hmac(dtls->hashAlgorithm, dtls->secret); hmac.addData(peerData); - const QByteArray cookie = hmac.result(); + const QByteArrayView cookie = hmac.resultView(); Q_ASSERT(cookie.size() >= 0); // DTLS1_COOKIE_LENGTH is erroneously 256 bytes long, must be 255 - RFC 6347, 4.2.1. *cookieLength = qMin(DTLS1_COOKIE_LENGTH - 1, cookie.size()); @@ -205,7 +201,7 @@ extern "C" int q_verify_cookie_callback(SSL *ssl, const unsigned char *cookie, return 0; return newCookieLength == cookieLength - && !std::memcmp(cookie, newCookie, cookieLength); + && !q_CRYPTO_memcmp(cookie, newCookie, size_t(cookieLength)); } extern "C" int q_X509DtlsCallback(int ok, X509_STORE_CTX *ctx) @@ -1252,12 +1248,12 @@ unsigned QDtlsPrivateOpenSSL::pskClientCallback(const char *hint, char *identity return 0; // Copy data back into OpenSSL - const int identityLength = qMin(pskAuthenticator.identity().length(), + const int identityLength = qMin(pskAuthenticator.identity().size(), pskAuthenticator.maximumIdentityLength()); std::memcpy(identity, pskAuthenticator.identity().constData(), identityLength); identity[identityLength] = 0; - const int pskLength = qMin(pskAuthenticator.preSharedKey().length(), + const int pskLength = qMin(pskAuthenticator.preSharedKey().size(), pskAuthenticator.maximumPreSharedKeyLength()); std::memcpy(psk, pskAuthenticator.preSharedKey().constData(), pskLength); @@ -1283,7 +1279,7 @@ unsigned QDtlsPrivateOpenSSL::pskServerCallback(const char *identity, unsigned c return 0; // Copy data back into OpenSSL - const int pskLength = qMin(pskAuthenticator.preSharedKey().length(), + const int pskLength = qMin(pskAuthenticator.preSharedKey().size(), pskAuthenticator.maximumPreSharedKeyLength()); std::memcpy(psk, pskAuthenticator.preSharedKey().constData(), pskLength); @@ -1328,7 +1324,7 @@ bool QDtlsPrivateOpenSSL::verifyPeer() // Translate errors from the error list into QSslErrors using CertClass = QTlsPrivate::X509CertificateOpenSSL; errors.reserve(errors.size() + opensslErrors.size()); - for (const auto &error : qAsConst(opensslErrors)) { + for (const auto &error : std::as_const(opensslErrors)) { const auto value = peerCertificateChain.value(error.depth); errors << CertClass::openSSLErrorToQSslError(error.code, value); } diff --git a/src/plugins/tls/openssl/qsslcontext_openssl.cpp b/src/plugins/tls/openssl/qsslcontext_openssl.cpp index 9e59477634..75c192bd01 100644 --- a/src/plugins/tls/openssl/qsslcontext_openssl.cpp +++ b/src/plugins/tls/openssl/qsslcontext_openssl.cpp @@ -63,9 +63,9 @@ static inline QString msgErrorSettingEllipticCurves(const QString &why) return QSslSocket::tr("Error when setting the elliptic curves (%1)").arg(why); } -long QSslContext::setupOpenSslOptions(QSsl::SslProtocol protocol, QSsl::SslOptions sslOptions) +qssloptions QSslContext::setupOpenSslOptions(QSsl::SslProtocol protocol, QSsl::SslOptions sslOptions) { - long options; + qssloptions options; switch (protocol) { QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED @@ -218,7 +218,7 @@ SSL* QSslContext::createSsl() QList<QByteArray> protocols = sslConfiguration.d.constData()->nextAllowedProtocols; if (!protocols.isEmpty()) { m_supportedNPNVersions.clear(); - for (int a = 0; a < protocols.count(); ++a) { + for (int a = 0; a < protocols.size(); ++a) { if (protocols.at(a).size() > 255) { qCWarning(lcTlsBackend) << "TLS NPN extension" << protocols.at(a) << "is too long and will be ignored."; @@ -230,7 +230,7 @@ SSL* QSslContext::createSsl() } if (m_supportedNPNVersions.size()) { m_npnContext.data = reinterpret_cast<unsigned char *>(m_supportedNPNVersions.data()); - m_npnContext.len = m_supportedNPNVersions.length(); + m_npnContext.len = m_supportedNPNVersions.size(); m_npnContext.status = QSslConfiguration::NextProtocolNegotiationNone; // Callback's type has a parameter 'const unsigned char ** out' // since it was introduced in 1.0.2. Internally, OpenSSL's own code @@ -476,7 +476,7 @@ QT_WARNING_POP } // Enable bug workarounds. - const long options = setupOpenSslOptions(configuration.protocol(), configuration.d->sslOptions); + const qssloptions options = setupOpenSslOptions(configuration.protocol(), configuration.d->sslOptions); q_SSL_CTX_set_options(sslContext->ctx, options); // Tell OpenSSL to release memory early @@ -632,7 +632,7 @@ QT_WARNING_POP // If we have any intermediate certificates then we need to add them to our chain bool first = true; - for (const QSslCertificate &cert : qAsConst(configuration.d->localCertificateChain)) { + for (const QSslCertificate &cert : std::as_const(configuration.d->localCertificateChain)) { if (first) { first = false; continue; @@ -697,12 +697,14 @@ QT_WARNING_POP return; } - if (!dhparams.isEmpty()) { + if (dhparams.isEmpty()) { + q_SSL_CTX_set_dh_auto(sslContext->ctx, 1); + } else { #ifndef OPENSSL_NO_DEPRECATED_3_0 const QByteArray ¶ms = dhparams.d->derData; const char *ptr = params.constData(); DH *dh = q_d2i_DHparams(nullptr, reinterpret_cast<const unsigned char **>(&ptr), - params.length()); + params.size()); if (dh == nullptr) qFatal("q_d2i_DHparams failed to convert QSslDiffieHellmanParameters to DER form"); q_SSL_CTX_set_tmp_dh(sslContext->ctx, dh); diff --git a/src/plugins/tls/openssl/qsslcontext_openssl_p.h b/src/plugins/tls/openssl/qsslcontext_openssl_p.h index 55af2af292..3bd39baf0c 100644 --- a/src/plugins/tls/openssl/qsslcontext_openssl_p.h +++ b/src/plugins/tls/openssl/qsslcontext_openssl_p.h @@ -37,7 +37,8 @@ public: bool allowRootCertOnDemandLoading); static std::shared_ptr<QSslContext> sharedFromPrivateConfiguration(QSslSocket::SslMode mode, QSslConfigurationPrivate *privConfiguration, bool allowRootCertOnDemandLoading); - static long setupOpenSslOptions(QSsl::SslProtocol protocol, QSsl::SslOptions sslOptions); + + static qssloptions setupOpenSslOptions(QSsl::SslProtocol protocol, QSsl::SslOptions sslOptions); QSslError::SslError error() const; QString errorString() const; diff --git a/src/plugins/tls/openssl/qssldiffiehellmanparameters_openssl.cpp b/src/plugins/tls/openssl/qssldiffiehellmanparameters_openssl.cpp index 81cbc6a12d..16e31e605f 100644 --- a/src/plugins/tls/openssl/qssldiffiehellmanparameters_openssl.cpp +++ b/src/plugins/tls/openssl/qssldiffiehellmanparameters_openssl.cpp @@ -137,8 +137,9 @@ int QTlsBackendOpenSSL::dhParametersFromPem(const QByteArray &pem, QByteArray *d if (isSafeDH(dh)) { char *buf = nullptr; const int len = q_i2d_DHparams(dh, reinterpret_cast<unsigned char **>(&buf)); + const auto freeBuf = qScopeGuard([&] { q_OPENSSL_free(buf); }); if (len > 0) - *data = QByteArray(buf, len); + data->assign({buf, len}); else return DHParams::InvalidInputDataError; } else { diff --git a/src/plugins/tls/openssl/qsslsocket_openssl_symbols.cpp b/src/plugins/tls/openssl/qsslsocket_openssl_symbols.cpp index c3d05f134c..4aa9ca6fb1 100644 --- a/src/plugins/tls/openssl/qsslsocket_openssl_symbols.cpp +++ b/src/plugins/tls/openssl/qsslsocket_openssl_symbols.cpp @@ -28,7 +28,6 @@ #elif QT_CONFIG(library) # include <QtCore/qlibrary.h> #endif -#include <QtCore/qmutex.h> #include <QtCore/qdatetime.h> #if defined(Q_OS_UNIX) #include <QtCore/qdir.h> @@ -123,7 +122,7 @@ DEFINEFUNC2(void, OPENSSL_sk_push, OPENSSL_STACK *a, a, void *b, b, return, DUMM DEFINEFUNC(void, OPENSSL_sk_free, OPENSSL_STACK *a, a, return, DUMMYARG) DEFINEFUNC2(void *, OPENSSL_sk_value, OPENSSL_STACK *a, a, int b, b, return nullptr, return) DEFINEFUNC(int, SSL_session_reused, SSL *a, a, return 0, return) -DEFINEFUNC2(unsigned long, SSL_CTX_set_options, SSL_CTX *ctx, ctx, unsigned long op, op, return 0, return) +DEFINEFUNC2(qssloptions, SSL_CTX_set_options, SSL_CTX *ctx, ctx, qssloptions op, op, return 0, return) using info_callback = void (*) (const SSL *ssl, int type, int val); DEFINEFUNC2(void, SSL_set_info_callback, SSL *ssl, ssl, info_callback cb, cb, return, return) DEFINEFUNC(const char *, SSL_alert_type_string, int value, value, return nullptr, return) @@ -156,10 +155,10 @@ DEFINEFUNC3(int, X509_STORE_set_ex_data, X509_STORE *a, a, int idx, idx, void *d DEFINEFUNC2(void *, X509_STORE_get_ex_data, X509_STORE *r, r, int idx, idx, return nullptr, return) DEFINEFUNC(STACK_OF(X509) *, X509_STORE_CTX_get0_chain, X509_STORE_CTX *a, a, return nullptr, return) DEFINEFUNC3(void, CRYPTO_free, void *str, str, const char *file, file, int line, line, return, DUMMYARG) +DEFINEFUNC3(int, CRYPTO_memcmp, const void * in_a, in_a, const void * in_b, in_b, size_t len, len, return 1, return); DEFINEFUNC(long, OpenSSL_version_num, void, DUMMYARG, return 0, return) DEFINEFUNC(const char *, OpenSSL_version, int a, a, return nullptr, return) DEFINEFUNC(unsigned long, SSL_SESSION_get_ticket_lifetime_hint, const SSL_SESSION *session, session, return 0, return) -DEFINEFUNC4(void, DH_get0_pqg, const DH *dh, dh, const BIGNUM **p, p, const BIGNUM **q, q, const BIGNUM **g, g, return, DUMMYARG) #if QT_CONFIG(dtls) DEFINEFUNC2(int, DTLSv1_listen, SSL *s, s, BIO_ADDR *c, c, return -1, return) @@ -263,7 +262,6 @@ DEFINEFUNC4(int, OBJ_obj2txt, char *a, a, int b, b, ASN1_OBJECT *c, c, int d, d, DEFINEFUNC(int, OBJ_obj2nid, const ASN1_OBJECT *a, a, return NID_undef, return) DEFINEFUNC4(EVP_PKEY *, PEM_read_bio_PrivateKey, BIO *a, a, EVP_PKEY **b, b, pem_password_cb *c, c, void *d, d, return nullptr, return) -DEFINEFUNC4(DH *, PEM_read_bio_DHparams, BIO *a, a, DH **b, b, pem_password_cb *c, c, void *d, d, return nullptr, return) DEFINEFUNC7(int, PEM_write_bio_PrivateKey, BIO *a, a, EVP_PKEY *b, b, const EVP_CIPHER *c, c, unsigned char *d, d, int e, e, pem_password_cb *f, f, void *g, g, return 0, return) DEFINEFUNC7(int, PEM_write_bio_PrivateKey_traditional, BIO *a, a, EVP_PKEY *b, b, const EVP_CIPHER *c, c, unsigned char *d, d, int e, e, pem_password_cb *f, f, void *g, g, return 0, return) DEFINEFUNC4(EVP_PKEY *, PEM_read_bio_PUBKEY, BIO *a, a, EVP_PKEY **b, b, pem_password_cb *c, c, void *d, d, return nullptr, return) @@ -429,13 +427,21 @@ DEFINEFUNC2(void *, BIO_get_ex_data, BIO *b, b, int idx, idx, return nullptr, re DEFINEFUNC3(int, BIO_set_ex_data, BIO *b, b, int idx, idx, void *data, data, return -1, return) DEFINEFUNC3(void *, CRYPTO_malloc, size_t num, num, const char *file, file, int line, line, return nullptr, return) + +#ifndef OPENSSL_NO_DEPRECATED_3_0 DEFINEFUNC(DH *, DH_new, DUMMYARG, DUMMYARG, return nullptr, return) DEFINEFUNC(void, DH_free, DH *dh, dh, return, DUMMYARG) +DEFINEFUNC2(int, DH_check, DH *dh, dh, int *codes, codes, return 0, return) +DEFINEFUNC4(void, DH_get0_pqg, const DH *dh, dh, const BIGNUM **p, p, const BIGNUM **q, q, const BIGNUM **g, g, return, DUMMYARG) + DEFINEFUNC3(DH *, d2i_DHparams, DH**a, a, const unsigned char **pp, pp, long length, length, return nullptr, return) DEFINEFUNC2(int, i2d_DHparams, DH *a, a, unsigned char **p, p, return -1, return) -DEFINEFUNC2(int, DH_check, DH *dh, dh, int *codes, codes, return 0, return) + +DEFINEFUNC4(DH *, PEM_read_bio_DHparams, BIO *a, a, DH **b, b, pem_password_cb *c, c, void *d, d, return nullptr, return) +#endif DEFINEFUNC3(BIGNUM *, BN_bin2bn, const unsigned char *s, s, int len, len, BIGNUM *ret, ret, return nullptr, return) + #ifndef OPENSSL_NO_EC DEFINEFUNC2(size_t, EC_get_builtin_curves, EC_builtin_curve * r, r, size_t nitems, nitems, return 0, return) DEFINEFUNC(int, EC_curve_nist2nid, const char *name, name, return 0, return) @@ -551,7 +557,7 @@ struct LibGreaterThan { const auto lhsparts = lhs.split(u'.'); const auto rhsparts = rhs.split(u'.'); - Q_ASSERT(lhsparts.count() > 1 && rhsparts.count() > 1); + Q_ASSERT(lhsparts.size() > 1 && rhsparts.size() > 1); // note: checking rhs < lhs, the same as lhs > rhs return std::lexicographical_compare(rhsparts.begin() + 1, rhsparts.end(), @@ -622,7 +628,7 @@ static QStringList findAllLibs(QLatin1StringView filter) QStringList entryList = dir.entryList(filters, QDir::Files); std::sort(entryList.begin(), entryList.end(), LibGreaterThan()); - for (const QString &entry : qAsConst(entryList)) + for (const QString &entry : std::as_const(entryList)) found << path + u'/' + entry; } @@ -640,6 +646,12 @@ static QStringList findAllLibCrypto() } # endif +#if (OPENSSL_VERSION_NUMBER >> 28) < 3 +#define QT_OPENSSL_VERSION "1_1" +#elif OPENSSL_VERSION_MAJOR == 3 // Starting with 3.0 this define is available +#define QT_OPENSSL_VERSION "3" +#endif // > 3 intentionally left undefined + #ifdef Q_OS_WIN struct LoadedOpenSsl { @@ -671,12 +683,6 @@ static LoadedOpenSsl loadOpenSsl() // MSVC and GCC. For 3.0 the version suffix changed again, to just '3'. // For non-x86 builds, an architecture suffix is also appended. -#if (OPENSSL_VERSION_NUMBER >> 28) < 3 -#define QT_OPENSSL_VERSION "1_1" -#elif OPENSSL_VERSION_MAJOR == 3 // Starting with 3.0 this define is available -#define QT_OPENSSL_VERSION "3" -#endif // > 3 intentionally left undefined - #if defined(Q_PROCESSOR_X86_64) #define QT_SSL_SUFFIX "-x64" #elif defined(Q_PROCESSOR_ARM_64) @@ -693,7 +699,7 @@ static LoadedOpenSsl loadOpenSsl() #undef QT_SSL_SUFFIX return result; } -#else +#else // !Q_OS_WIN: struct LoadedOpenSsl { std::unique_ptr<QLibrary> ssl, crypto; @@ -743,10 +749,22 @@ static LoadedOpenSsl loadOpenSsl() #ifdef Q_OS_OPENBSD libcrypto->setLoadHints(QLibrary::ExportExternalSymbolsHint); #endif -#if defined(SHLIB_VERSION_NUMBER) && !defined(Q_OS_QNX) // on QNX, the libs are always libssl.so and libcrypto.so + +#if !defined(Q_OS_QNX) // on QNX, the libs are always libssl.so and libcrypto.so + +#if defined(OPENSSL_SHLIB_VERSION) + // OpenSSL v.3 does not have SLIB_VERSION_NUMBER but has OPENSSL_SHLIB_VERSION. + // The comment about OPENSSL_SHLIB_VERSION in opensslv.h is a bit troublesome: + // "This is defined in free form." + auto shlibVersion = QString("%1"_L1).arg(OPENSSL_SHLIB_VERSION); + libssl->setFileNameAndVersion("ssl"_L1, shlibVersion); + libcrypto->setFileNameAndVersion("crypto"_L1, shlibVersion); +#elif defined(SHLIB_VERSION_NUMBER) // first attempt: the canonical name is libssl.so.<SHLIB_VERSION_NUMBER> libssl->setFileNameAndVersion("ssl"_L1, SHLIB_VERSION_NUMBER ""_L1); libcrypto->setFileNameAndVersion("crypto"_L1, SHLIB_VERSION_NUMBER ""_L1); +#endif // OPENSSL_SHLIB_VERSION + if (libcrypto->load() && libssl->load()) { // libssl.so.<SHLIB_VERSION_NUMBER> and libcrypto.so.<SHLIB_VERSION_NUMBER> found return result; @@ -754,7 +772,7 @@ static LoadedOpenSsl loadOpenSsl() libssl->unload(); libcrypto->unload(); } -#endif +#endif // !defined(Q_OS_QNX) #ifndef Q_OS_DARWIN // second attempt: find the development files libssl.so and libcrypto.so @@ -772,7 +790,7 @@ static LoadedOpenSsl loadOpenSsl() return suffix; }; - static QString suffix = QString::fromLatin1(openSSLSuffix("_1_1")); + static QString suffix = QString::fromLatin1(openSSLSuffix("_" QT_OPENSSL_VERSION)); libssl->setFileNameAndVersion("ssl"_L1 + suffix, -1); libcrypto->setFileNameAndVersion("crypto"_L1 + suffix, -1); @@ -832,412 +850,405 @@ static LoadedOpenSsl loadOpenSsl() } #endif -Q_CONSTINIT static QBasicMutex symbolResolveMutex; -Q_CONSTINIT static QBasicAtomicInt symbolsResolved = Q_BASIC_ATOMIC_INITIALIZER(false); -Q_CONSTINIT static bool triedToResolveSymbols = false; - bool q_resolveOpenSslSymbols() { - if (symbolsResolved.loadAcquire()) - return true; - QMutexLocker locker(&symbolResolveMutex); - if (symbolsResolved.loadRelaxed()) - return true; - if (triedToResolveSymbols) - return false; - triedToResolveSymbols = true; - - LoadedOpenSsl libs = loadOpenSsl(); - if (!libs.ssl || !libs.crypto) { - qCWarning(lcTlsBackend, "Failed to load libssl/libcrypto."); - return false; - } + static bool symbolsResolved = []() { + LoadedOpenSsl libs = loadOpenSsl(); + if (!libs.ssl || !libs.crypto) { + qCWarning(lcTlsBackend, "Failed to load libssl/libcrypto."); + return false; + } - RESOLVEFUNC(OPENSSL_init_ssl) - RESOLVEFUNC(OPENSSL_init_crypto) - RESOLVEFUNC(ASN1_STRING_get0_data) - RESOLVEFUNC(EVP_CIPHER_CTX_reset) - RESOLVEFUNC(AUTHORITY_INFO_ACCESS_free) - RESOLVEFUNC(EVP_PKEY_up_ref) - RESOLVEFUNC(EVP_PKEY_CTX_new) - RESOLVEFUNC(EVP_PKEY_param_check) - RESOLVEFUNC(EVP_PKEY_CTX_free) - RESOLVEFUNC(OPENSSL_sk_new_null) - RESOLVEFUNC(OPENSSL_sk_push) - RESOLVEFUNC(OPENSSL_sk_free) - RESOLVEFUNC(OPENSSL_sk_num) - RESOLVEFUNC(OPENSSL_sk_pop_free) - RESOLVEFUNC(OPENSSL_sk_value) - RESOLVEFUNC(DH_get0_pqg) - RESOLVEFUNC(SSL_CTX_set_options) - RESOLVEFUNC(SSL_set_info_callback) - RESOLVEFUNC(SSL_alert_type_string) - RESOLVEFUNC(SSL_alert_desc_string_long) - RESOLVEFUNC(SSL_CTX_get_security_level) - RESOLVEFUNC(SSL_CTX_set_security_level) + RESOLVEFUNC(OPENSSL_init_ssl) + RESOLVEFUNC(OPENSSL_init_crypto) + RESOLVEFUNC(ASN1_STRING_get0_data) + RESOLVEFUNC(EVP_CIPHER_CTX_reset) + RESOLVEFUNC(AUTHORITY_INFO_ACCESS_free) + RESOLVEFUNC(EVP_PKEY_up_ref) + RESOLVEFUNC(EVP_PKEY_CTX_new) + RESOLVEFUNC(EVP_PKEY_param_check) + RESOLVEFUNC(EVP_PKEY_CTX_free) + RESOLVEFUNC(OPENSSL_sk_new_null) + RESOLVEFUNC(OPENSSL_sk_push) + RESOLVEFUNC(OPENSSL_sk_free) + RESOLVEFUNC(OPENSSL_sk_num) + RESOLVEFUNC(OPENSSL_sk_pop_free) + RESOLVEFUNC(OPENSSL_sk_value) + RESOLVEFUNC(SSL_CTX_set_options) + RESOLVEFUNC(SSL_set_info_callback) + RESOLVEFUNC(SSL_alert_type_string) + RESOLVEFUNC(SSL_alert_desc_string_long) + RESOLVEFUNC(SSL_CTX_get_security_level) + RESOLVEFUNC(SSL_CTX_set_security_level) #ifdef TLS1_3_VERSION - RESOLVEFUNC(SSL_CTX_set_ciphersuites) - RESOLVEFUNC(SSL_set_psk_use_session_callback) - RESOLVEFUNC(SSL_CTX_sess_set_new_cb) - RESOLVEFUNC(SSL_SESSION_is_resumable) + RESOLVEFUNC(SSL_CTX_set_ciphersuites) + RESOLVEFUNC(SSL_set_psk_use_session_callback) + RESOLVEFUNC(SSL_CTX_sess_set_new_cb) + RESOLVEFUNC(SSL_SESSION_is_resumable) #endif // TLS 1.3 or OpenSSL > 1.1.1 - RESOLVEFUNC(SSL_get_client_random) - RESOLVEFUNC(SSL_SESSION_get_master_key) - RESOLVEFUNC(SSL_session_reused) - RESOLVEFUNC(SSL_get_session) - RESOLVEFUNC(SSL_set_options) - RESOLVEFUNC(CRYPTO_get_ex_new_index) - RESOLVEFUNC(TLS_method) - RESOLVEFUNC(TLS_client_method) - RESOLVEFUNC(TLS_server_method) - RESOLVEFUNC(X509_up_ref) - RESOLVEFUNC(X509_STORE_CTX_get0_chain) - RESOLVEFUNC(X509_getm_notBefore) - RESOLVEFUNC(X509_getm_notAfter) - RESOLVEFUNC(ASN1_item_free) - RESOLVEFUNC(X509V3_conf_free) - RESOLVEFUNC(X509_get_version) - RESOLVEFUNC(X509_get_pubkey) - RESOLVEFUNC(X509_STORE_set_verify_cb) - RESOLVEFUNC(X509_STORE_set_ex_data) - RESOLVEFUNC(X509_STORE_get_ex_data) - RESOLVEFUNC(CRYPTO_free) - RESOLVEFUNC(OpenSSL_version_num) - RESOLVEFUNC(OpenSSL_version) - - if (!_q_OpenSSL_version || !_q_OpenSSL_version_num) { - // Apparently, we were built with OpenSSL 1.1 enabled but are now using - // a wrong library. - qCWarning(lcTlsBackend, "Incompatible version of OpenSSL"); - return false; - } + RESOLVEFUNC(SSL_get_client_random) + RESOLVEFUNC(SSL_SESSION_get_master_key) + RESOLVEFUNC(SSL_session_reused) + RESOLVEFUNC(SSL_get_session) + RESOLVEFUNC(SSL_set_options) + RESOLVEFUNC(CRYPTO_get_ex_new_index) + RESOLVEFUNC(TLS_method) + RESOLVEFUNC(TLS_client_method) + RESOLVEFUNC(TLS_server_method) + RESOLVEFUNC(X509_up_ref) + RESOLVEFUNC(X509_STORE_CTX_get0_chain) + RESOLVEFUNC(X509_getm_notBefore) + RESOLVEFUNC(X509_getm_notAfter) + RESOLVEFUNC(ASN1_item_free) + RESOLVEFUNC(X509V3_conf_free) + RESOLVEFUNC(X509_get_version) + RESOLVEFUNC(X509_get_pubkey) + RESOLVEFUNC(X509_STORE_set_verify_cb) + RESOLVEFUNC(X509_STORE_set_ex_data) + RESOLVEFUNC(X509_STORE_get_ex_data) + RESOLVEFUNC(CRYPTO_free) + RESOLVEFUNC(CRYPTO_memcmp) + RESOLVEFUNC(OpenSSL_version_num) + RESOLVEFUNC(OpenSSL_version) + + if (!_q_OpenSSL_version || !_q_OpenSSL_version_num) { + // Apparently, we were built with OpenSSL 1.1 enabled but are now using + // a wrong library. + qCWarning(lcTlsBackend, "Incompatible version of OpenSSL"); + return false; + } #if OPENSSL_VERSION_NUMBER >= 0x30000000 - if (q_OpenSSL_version_num() < 0x30000000) { - qCWarning(lcTlsBackend, "Incompatible version of OpenSSL (built with OpenSSL >= 3.x, runtime version is < 3.x)"); - return false; - } + if (q_OpenSSL_version_num() < 0x30000000) { + qCWarning(lcTlsBackend, "Incompatible version of OpenSSL (built with OpenSSL >= 3.x, runtime version is < 3.x)"); + return false; + } #else - if (q_OpenSSL_version_num() >= 0x30000000) { - qCWarning(lcTlsBackend, "Incompatible version of OpenSSL (built with OpenSSL 1.x, runtime version is >= 3.x)"); - return false; - } + if (q_OpenSSL_version_num() >= 0x30000000) { + qCWarning(lcTlsBackend, "Incompatible version of OpenSSL (built with OpenSSL 1.x, runtime version is >= 3.x)"); + return false; + } #endif // OPENSSL_VERSION_NUMBER - RESOLVEFUNC(SSL_SESSION_get_ticket_lifetime_hint) + RESOLVEFUNC(SSL_SESSION_get_ticket_lifetime_hint) #if QT_CONFIG(dtls) - RESOLVEFUNC(DTLSv1_listen) - RESOLVEFUNC(BIO_ADDR_new) - RESOLVEFUNC(BIO_ADDR_free) - RESOLVEFUNC(BIO_meth_new) - RESOLVEFUNC(BIO_meth_free) - RESOLVEFUNC(BIO_meth_set_write) - RESOLVEFUNC(BIO_meth_set_read) - RESOLVEFUNC(BIO_meth_set_puts) - RESOLVEFUNC(BIO_meth_set_ctrl) - RESOLVEFUNC(BIO_meth_set_create) - RESOLVEFUNC(BIO_meth_set_destroy) + RESOLVEFUNC(DTLSv1_listen) + RESOLVEFUNC(BIO_ADDR_new) + RESOLVEFUNC(BIO_ADDR_free) + RESOLVEFUNC(BIO_meth_new) + RESOLVEFUNC(BIO_meth_free) + RESOLVEFUNC(BIO_meth_set_write) + RESOLVEFUNC(BIO_meth_set_read) + RESOLVEFUNC(BIO_meth_set_puts) + RESOLVEFUNC(BIO_meth_set_ctrl) + RESOLVEFUNC(BIO_meth_set_create) + RESOLVEFUNC(BIO_meth_set_destroy) #endif // dtls #if QT_CONFIG(ocsp) - RESOLVEFUNC(OCSP_SINGLERESP_get0_id) - RESOLVEFUNC(d2i_OCSP_RESPONSE) - RESOLVEFUNC(OCSP_RESPONSE_free) - RESOLVEFUNC(OCSP_response_status) - RESOLVEFUNC(OCSP_response_get1_basic) - RESOLVEFUNC(OCSP_BASICRESP_free) - RESOLVEFUNC(OCSP_basic_verify) - RESOLVEFUNC(OCSP_resp_count) - RESOLVEFUNC(OCSP_resp_get0) - RESOLVEFUNC(OCSP_single_get0_status) - RESOLVEFUNC(OCSP_check_validity) - RESOLVEFUNC(OCSP_cert_to_id) - RESOLVEFUNC(OCSP_id_get0_info) - RESOLVEFUNC(OCSP_resp_get0_certs) - RESOLVEFUNC(OCSP_basic_sign) - RESOLVEFUNC(OCSP_response_create) - RESOLVEFUNC(i2d_OCSP_RESPONSE) - RESOLVEFUNC(OCSP_basic_add1_status) - RESOLVEFUNC(OCSP_BASICRESP_new) - RESOLVEFUNC(OCSP_CERTID_free) - RESOLVEFUNC(OCSP_cert_to_id) - RESOLVEFUNC(OCSP_id_cmp) + RESOLVEFUNC(OCSP_SINGLERESP_get0_id) + RESOLVEFUNC(d2i_OCSP_RESPONSE) + RESOLVEFUNC(OCSP_RESPONSE_free) + RESOLVEFUNC(OCSP_response_status) + RESOLVEFUNC(OCSP_response_get1_basic) + RESOLVEFUNC(OCSP_BASICRESP_free) + RESOLVEFUNC(OCSP_basic_verify) + RESOLVEFUNC(OCSP_resp_count) + RESOLVEFUNC(OCSP_resp_get0) + RESOLVEFUNC(OCSP_single_get0_status) + RESOLVEFUNC(OCSP_check_validity) + RESOLVEFUNC(OCSP_cert_to_id) + RESOLVEFUNC(OCSP_id_get0_info) + RESOLVEFUNC(OCSP_resp_get0_certs) + RESOLVEFUNC(OCSP_basic_sign) + RESOLVEFUNC(OCSP_response_create) + RESOLVEFUNC(i2d_OCSP_RESPONSE) + RESOLVEFUNC(OCSP_basic_add1_status) + RESOLVEFUNC(OCSP_BASICRESP_new) + RESOLVEFUNC(OCSP_CERTID_free) + RESOLVEFUNC(OCSP_cert_to_id) + RESOLVEFUNC(OCSP_id_cmp) #endif // ocsp - RESOLVEFUNC(BIO_set_data) - RESOLVEFUNC(BIO_get_data) - RESOLVEFUNC(BIO_set_init) - RESOLVEFUNC(BIO_get_shutdown) - RESOLVEFUNC(BIO_set_shutdown) - RESOLVEFUNC(ASN1_INTEGER_get) - RESOLVEFUNC(ASN1_INTEGER_cmp) - RESOLVEFUNC(ASN1_STRING_length) - RESOLVEFUNC(ASN1_STRING_to_UTF8) - RESOLVEFUNC(ASN1_TIME_to_tm) - RESOLVEFUNC(BIO_ctrl) - RESOLVEFUNC(BIO_free) - RESOLVEFUNC(BIO_new) - RESOLVEFUNC(BIO_new_mem_buf) - RESOLVEFUNC(BIO_read) - RESOLVEFUNC(BIO_s_mem) - RESOLVEFUNC(BIO_write) - RESOLVEFUNC(BIO_set_flags) - RESOLVEFUNC(BIO_clear_flags) - RESOLVEFUNC(BIO_set_ex_data) - RESOLVEFUNC(BIO_get_ex_data) - RESOLVEFUNC(BN_num_bits) - RESOLVEFUNC(BN_is_word) - RESOLVEFUNC(BN_mod_word) - RESOLVEFUNC(ERR_error_string) - RESOLVEFUNC(ERR_error_string_n) - RESOLVEFUNC(ERR_get_error) - RESOLVEFUNC(EVP_CIPHER_CTX_new) - RESOLVEFUNC(EVP_CIPHER_CTX_free) - RESOLVEFUNC(EVP_CIPHER_CTX_ctrl) - RESOLVEFUNC(EVP_CIPHER_CTX_set_key_length) - RESOLVEFUNC(EVP_CipherInit) - RESOLVEFUNC(EVP_CipherInit_ex) - RESOLVEFUNC(EVP_CipherUpdate) - RESOLVEFUNC(EVP_CipherFinal) - RESOLVEFUNC(EVP_get_digestbyname) + RESOLVEFUNC(BIO_set_data) + RESOLVEFUNC(BIO_get_data) + RESOLVEFUNC(BIO_set_init) + RESOLVEFUNC(BIO_get_shutdown) + RESOLVEFUNC(BIO_set_shutdown) + RESOLVEFUNC(ASN1_INTEGER_get) + RESOLVEFUNC(ASN1_INTEGER_cmp) + RESOLVEFUNC(ASN1_STRING_length) + RESOLVEFUNC(ASN1_STRING_to_UTF8) + RESOLVEFUNC(ASN1_TIME_to_tm) + RESOLVEFUNC(BIO_ctrl) + RESOLVEFUNC(BIO_free) + RESOLVEFUNC(BIO_new) + RESOLVEFUNC(BIO_new_mem_buf) + RESOLVEFUNC(BIO_read) + RESOLVEFUNC(BIO_s_mem) + RESOLVEFUNC(BIO_write) + RESOLVEFUNC(BIO_set_flags) + RESOLVEFUNC(BIO_clear_flags) + RESOLVEFUNC(BIO_set_ex_data) + RESOLVEFUNC(BIO_get_ex_data) + RESOLVEFUNC(BN_num_bits) + RESOLVEFUNC(BN_is_word) + RESOLVEFUNC(BN_mod_word) + RESOLVEFUNC(ERR_error_string) + RESOLVEFUNC(ERR_error_string_n) + RESOLVEFUNC(ERR_get_error) + RESOLVEFUNC(EVP_CIPHER_CTX_new) + RESOLVEFUNC(EVP_CIPHER_CTX_free) + RESOLVEFUNC(EVP_CIPHER_CTX_ctrl) + RESOLVEFUNC(EVP_CIPHER_CTX_set_key_length) + RESOLVEFUNC(EVP_CipherInit) + RESOLVEFUNC(EVP_CipherInit_ex) + RESOLVEFUNC(EVP_CipherUpdate) + RESOLVEFUNC(EVP_CipherFinal) + RESOLVEFUNC(EVP_get_digestbyname) #ifndef OPENSSL_NO_DES - RESOLVEFUNC(EVP_des_cbc) - RESOLVEFUNC(EVP_des_ede3_cbc) + RESOLVEFUNC(EVP_des_cbc) + RESOLVEFUNC(EVP_des_ede3_cbc) #endif #ifndef OPENSSL_NO_RC2 - RESOLVEFUNC(EVP_rc2_cbc) + RESOLVEFUNC(EVP_rc2_cbc) #endif #ifndef OPENSSL_NO_AES - RESOLVEFUNC(EVP_aes_128_cbc) - RESOLVEFUNC(EVP_aes_192_cbc) - RESOLVEFUNC(EVP_aes_256_cbc) + RESOLVEFUNC(EVP_aes_128_cbc) + RESOLVEFUNC(EVP_aes_192_cbc) + RESOLVEFUNC(EVP_aes_256_cbc) #endif - RESOLVEFUNC(EVP_sha1) - RESOLVEFUNC(EVP_PKEY_free) - RESOLVEFUNC(EVP_PKEY_new) - RESOLVEFUNC(EVP_PKEY_type) - RESOLVEFUNC(OBJ_nid2sn) - RESOLVEFUNC(OBJ_nid2ln) - RESOLVEFUNC(OBJ_sn2nid) - RESOLVEFUNC(OBJ_ln2nid) - RESOLVEFUNC(i2t_ASN1_OBJECT) - RESOLVEFUNC(OBJ_obj2txt) - RESOLVEFUNC(OBJ_obj2nid) - RESOLVEFUNC(PEM_read_bio_PrivateKey) - RESOLVEFUNC(PEM_read_bio_DHparams) - RESOLVEFUNC(PEM_write_bio_PrivateKey) - RESOLVEFUNC(PEM_write_bio_PrivateKey_traditional) - RESOLVEFUNC(PEM_read_bio_PUBKEY) - RESOLVEFUNC(PEM_write_bio_PUBKEY) - RESOLVEFUNC(RAND_seed) - RESOLVEFUNC(RAND_status) - RESOLVEFUNC(RAND_bytes) - RESOLVEFUNC(SSL_CIPHER_description) - RESOLVEFUNC(SSL_CIPHER_get_bits) - RESOLVEFUNC(SSL_get_rbio) - RESOLVEFUNC(SSL_CTX_check_private_key) - RESOLVEFUNC(SSL_CTX_ctrl) - RESOLVEFUNC(SSL_CTX_free) - RESOLVEFUNC(SSL_CTX_new) - RESOLVEFUNC(SSL_CTX_set_cipher_list) - RESOLVEFUNC(SSL_CTX_callback_ctrl) - RESOLVEFUNC(SSL_CTX_set_default_verify_paths) - RESOLVEFUNC(SSL_CTX_set_verify) - RESOLVEFUNC(SSL_CTX_set_verify_depth) - RESOLVEFUNC(SSL_CTX_use_certificate) - RESOLVEFUNC(SSL_CTX_use_certificate_file) - RESOLVEFUNC(SSL_CTX_use_PrivateKey) - RESOLVEFUNC(SSL_CTX_use_PrivateKey_file) - RESOLVEFUNC(SSL_CTX_get_cert_store); - RESOLVEFUNC(SSL_CONF_CTX_new); - RESOLVEFUNC(SSL_CONF_CTX_free); - RESOLVEFUNC(SSL_CONF_CTX_set_ssl_ctx); - RESOLVEFUNC(SSL_CONF_CTX_set_flags); - RESOLVEFUNC(SSL_CONF_CTX_finish); - RESOLVEFUNC(SSL_CONF_cmd); - RESOLVEFUNC(SSL_accept) - RESOLVEFUNC(SSL_clear) - RESOLVEFUNC(SSL_connect) - RESOLVEFUNC(SSL_free) - RESOLVEFUNC(SSL_get_ciphers) - RESOLVEFUNC(SSL_get_current_cipher) - RESOLVEFUNC(SSL_version) - RESOLVEFUNC(SSL_get_error) - RESOLVEFUNC(SSL_get_peer_cert_chain) + RESOLVEFUNC(EVP_sha1) + RESOLVEFUNC(EVP_PKEY_free) + RESOLVEFUNC(EVP_PKEY_new) + RESOLVEFUNC(EVP_PKEY_type) + RESOLVEFUNC(OBJ_nid2sn) + RESOLVEFUNC(OBJ_nid2ln) + RESOLVEFUNC(OBJ_sn2nid) + RESOLVEFUNC(OBJ_ln2nid) + RESOLVEFUNC(i2t_ASN1_OBJECT) + RESOLVEFUNC(OBJ_obj2txt) + RESOLVEFUNC(OBJ_obj2nid) + RESOLVEFUNC(PEM_read_bio_PrivateKey) + RESOLVEFUNC(PEM_write_bio_PrivateKey) + RESOLVEFUNC(PEM_write_bio_PrivateKey_traditional) + RESOLVEFUNC(PEM_read_bio_PUBKEY) + RESOLVEFUNC(PEM_write_bio_PUBKEY) + RESOLVEFUNC(RAND_seed) + RESOLVEFUNC(RAND_status) + RESOLVEFUNC(RAND_bytes) + RESOLVEFUNC(SSL_CIPHER_description) + RESOLVEFUNC(SSL_CIPHER_get_bits) + RESOLVEFUNC(SSL_get_rbio) + RESOLVEFUNC(SSL_CTX_check_private_key) + RESOLVEFUNC(SSL_CTX_ctrl) + RESOLVEFUNC(SSL_CTX_free) + RESOLVEFUNC(SSL_CTX_new) + RESOLVEFUNC(SSL_CTX_set_cipher_list) + RESOLVEFUNC(SSL_CTX_callback_ctrl) + RESOLVEFUNC(SSL_CTX_set_default_verify_paths) + RESOLVEFUNC(SSL_CTX_set_verify) + RESOLVEFUNC(SSL_CTX_set_verify_depth) + RESOLVEFUNC(SSL_CTX_use_certificate) + RESOLVEFUNC(SSL_CTX_use_certificate_file) + RESOLVEFUNC(SSL_CTX_use_PrivateKey) + RESOLVEFUNC(SSL_CTX_use_PrivateKey_file) + RESOLVEFUNC(SSL_CTX_get_cert_store); + RESOLVEFUNC(SSL_CONF_CTX_new); + RESOLVEFUNC(SSL_CONF_CTX_free); + RESOLVEFUNC(SSL_CONF_CTX_set_ssl_ctx); + RESOLVEFUNC(SSL_CONF_CTX_set_flags); + RESOLVEFUNC(SSL_CONF_CTX_finish); + RESOLVEFUNC(SSL_CONF_cmd); + RESOLVEFUNC(SSL_accept) + RESOLVEFUNC(SSL_clear) + RESOLVEFUNC(SSL_connect) + RESOLVEFUNC(SSL_free) + RESOLVEFUNC(SSL_get_ciphers) + RESOLVEFUNC(SSL_get_current_cipher) + RESOLVEFUNC(SSL_version) + RESOLVEFUNC(SSL_get_error) + RESOLVEFUNC(SSL_get_peer_cert_chain) #if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 3 - RESOLVEFUNC(SSL_get1_peer_certificate) - RESOLVEFUNC(EVP_PKEY_get_bits) - RESOLVEFUNC(EVP_PKEY_get_base_id) + RESOLVEFUNC(SSL_get1_peer_certificate) + RESOLVEFUNC(EVP_PKEY_get_bits) + RESOLVEFUNC(EVP_PKEY_get_base_id) #else - RESOLVEFUNC(SSL_get_peer_certificate) - RESOLVEFUNC(EVP_PKEY_base_id) + RESOLVEFUNC(SSL_get_peer_certificate) + RESOLVEFUNC(EVP_PKEY_base_id) #endif // OPENSSL_VERSION_MAJOR >= 3 #ifndef OPENSSL_NO_DEPRECATED_3_0 - RESOLVEFUNC(EVP_PKEY_assign) - RESOLVEFUNC(EVP_PKEY_cmp) + RESOLVEFUNC(DH_new) + RESOLVEFUNC(DH_free) + RESOLVEFUNC(DH_check) + RESOLVEFUNC(DH_get0_pqg) - RESOLVEFUNC(EVP_PKEY_set1_RSA) - RESOLVEFUNC(EVP_PKEY_set1_DSA) - RESOLVEFUNC(EVP_PKEY_set1_DH) + RESOLVEFUNC(d2i_DHparams) + RESOLVEFUNC(i2d_DHparams) - RESOLVEFUNC(EVP_PKEY_get1_DSA) - RESOLVEFUNC(EVP_PKEY_get1_RSA) - RESOLVEFUNC(EVP_PKEY_get1_DH) + RESOLVEFUNC(PEM_read_bio_DHparams) - RESOLVEFUNC(PEM_read_bio_DSA_PUBKEY) - RESOLVEFUNC(PEM_read_bio_RSA_PUBKEY) - RESOLVEFUNC(PEM_read_bio_DSAPrivateKey) - RESOLVEFUNC(PEM_read_bio_RSAPrivateKey) + RESOLVEFUNC(EVP_PKEY_assign) + RESOLVEFUNC(EVP_PKEY_cmp) - RESOLVEFUNC(PEM_write_bio_DSA_PUBKEY) - RESOLVEFUNC(PEM_write_bio_RSA_PUBKEY) - RESOLVEFUNC(PEM_write_bio_DSAPrivateKey) - RESOLVEFUNC(PEM_write_bio_RSAPrivateKey) - RESOLVEFUNC(SSL_CTX_use_RSAPrivateKey) + RESOLVEFUNC(EVP_PKEY_set1_RSA) + RESOLVEFUNC(EVP_PKEY_set1_DSA) + RESOLVEFUNC(EVP_PKEY_set1_DH) - RESOLVEFUNC(DSA_new) - RESOLVEFUNC(DSA_free) + RESOLVEFUNC(EVP_PKEY_get1_DSA) + RESOLVEFUNC(EVP_PKEY_get1_RSA) + RESOLVEFUNC(EVP_PKEY_get1_DH) - RESOLVEFUNC(RSA_new) - RESOLVEFUNC(RSA_free) + RESOLVEFUNC(PEM_read_bio_DSA_PUBKEY) + RESOLVEFUNC(PEM_read_bio_RSA_PUBKEY) + RESOLVEFUNC(PEM_read_bio_DSAPrivateKey) + RESOLVEFUNC(PEM_read_bio_RSAPrivateKey) - RESOLVEFUNC(DH_bits) - RESOLVEFUNC(DSA_bits) - RESOLVEFUNC(RSA_bits) + RESOLVEFUNC(PEM_write_bio_DSA_PUBKEY) + RESOLVEFUNC(PEM_write_bio_RSA_PUBKEY) + RESOLVEFUNC(PEM_write_bio_DSAPrivateKey) + RESOLVEFUNC(PEM_write_bio_RSAPrivateKey) + RESOLVEFUNC(SSL_CTX_use_RSAPrivateKey) + + RESOLVEFUNC(DSA_new) + RESOLVEFUNC(DSA_free) + + RESOLVEFUNC(RSA_new) + RESOLVEFUNC(RSA_free) + + RESOLVEFUNC(DH_bits) + RESOLVEFUNC(DSA_bits) + RESOLVEFUNC(RSA_bits) #ifndef OPENSSL_NO_EC - RESOLVEFUNC(EVP_PKEY_set1_EC_KEY) - RESOLVEFUNC(EVP_PKEY_get1_EC_KEY) - RESOLVEFUNC(PEM_read_bio_EC_PUBKEY) - RESOLVEFUNC(PEM_read_bio_ECPrivateKey) - RESOLVEFUNC(PEM_write_bio_EC_PUBKEY) - RESOLVEFUNC(PEM_write_bio_ECPrivateKey) - RESOLVEFUNC(EC_KEY_get0_group) - RESOLVEFUNC(EC_GROUP_get_degree) - RESOLVEFUNC(EC_KEY_dup) - RESOLVEFUNC(EC_KEY_new_by_curve_name) - RESOLVEFUNC(EC_KEY_free) + RESOLVEFUNC(EVP_PKEY_set1_EC_KEY) + RESOLVEFUNC(EVP_PKEY_get1_EC_KEY) + RESOLVEFUNC(PEM_read_bio_EC_PUBKEY) + RESOLVEFUNC(PEM_read_bio_ECPrivateKey) + RESOLVEFUNC(PEM_write_bio_EC_PUBKEY) + RESOLVEFUNC(PEM_write_bio_ECPrivateKey) + RESOLVEFUNC(EC_KEY_get0_group) + RESOLVEFUNC(EC_GROUP_get_degree) + RESOLVEFUNC(EC_KEY_dup) + RESOLVEFUNC(EC_KEY_new_by_curve_name) + RESOLVEFUNC(EC_KEY_free) #endif // OPENSSL_NO_EC #endif // OPENSSL_NO_DEPRECATED_3_0 - RESOLVEFUNC(SSL_get_verify_result) - RESOLVEFUNC(SSL_new) - RESOLVEFUNC(SSL_get_SSL_CTX) - RESOLVEFUNC(SSL_ctrl) - RESOLVEFUNC(SSL_read) - RESOLVEFUNC(SSL_set_accept_state) - RESOLVEFUNC(SSL_set_bio) - RESOLVEFUNC(SSL_set_connect_state) - RESOLVEFUNC(SSL_shutdown) - RESOLVEFUNC(SSL_in_init) - RESOLVEFUNC(SSL_get_shutdown) - RESOLVEFUNC(SSL_set_session) - RESOLVEFUNC(SSL_SESSION_free) - RESOLVEFUNC(SSL_get1_session) - RESOLVEFUNC(SSL_get_session) - RESOLVEFUNC(SSL_set_ex_data) - RESOLVEFUNC(SSL_get_ex_data) - RESOLVEFUNC(SSL_get_ex_data_X509_STORE_CTX_idx) + RESOLVEFUNC(SSL_get_verify_result) + RESOLVEFUNC(SSL_new) + RESOLVEFUNC(SSL_get_SSL_CTX) + RESOLVEFUNC(SSL_ctrl) + RESOLVEFUNC(SSL_read) + RESOLVEFUNC(SSL_set_accept_state) + RESOLVEFUNC(SSL_set_bio) + RESOLVEFUNC(SSL_set_connect_state) + RESOLVEFUNC(SSL_shutdown) + RESOLVEFUNC(SSL_in_init) + RESOLVEFUNC(SSL_get_shutdown) + RESOLVEFUNC(SSL_set_session) + RESOLVEFUNC(SSL_SESSION_free) + RESOLVEFUNC(SSL_get1_session) + RESOLVEFUNC(SSL_get_session) + RESOLVEFUNC(SSL_set_ex_data) + RESOLVEFUNC(SSL_get_ex_data) + RESOLVEFUNC(SSL_get_ex_data_X509_STORE_CTX_idx) #ifndef OPENSSL_NO_PSK - RESOLVEFUNC(SSL_set_psk_client_callback) - RESOLVEFUNC(SSL_set_psk_server_callback) - RESOLVEFUNC(SSL_CTX_use_psk_identity_hint) + RESOLVEFUNC(SSL_set_psk_client_callback) + RESOLVEFUNC(SSL_set_psk_server_callback) + RESOLVEFUNC(SSL_CTX_use_psk_identity_hint) #endif // !OPENSSL_NO_PSK - RESOLVEFUNC(SSL_write) - RESOLVEFUNC(X509_NAME_entry_count) - RESOLVEFUNC(X509_NAME_get_entry) - RESOLVEFUNC(X509_NAME_ENTRY_get_data) - RESOLVEFUNC(X509_NAME_ENTRY_get_object) - RESOLVEFUNC(X509_PUBKEY_get) - RESOLVEFUNC(X509_STORE_free) - RESOLVEFUNC(X509_STORE_new) - RESOLVEFUNC(X509_STORE_add_cert) - RESOLVEFUNC(X509_STORE_CTX_free) - RESOLVEFUNC(X509_STORE_CTX_init) - RESOLVEFUNC(X509_STORE_CTX_new) - RESOLVEFUNC(X509_STORE_CTX_set_purpose) - RESOLVEFUNC(X509_STORE_CTX_get_error) - RESOLVEFUNC(X509_STORE_CTX_get_error_depth) - RESOLVEFUNC(X509_STORE_CTX_get_current_cert) - RESOLVEFUNC(X509_STORE_CTX_get0_store) - RESOLVEFUNC(X509_cmp) - RESOLVEFUNC(X509_STORE_CTX_get_ex_data) - RESOLVEFUNC(X509_dup) - RESOLVEFUNC(X509_print) - RESOLVEFUNC(X509_digest) - RESOLVEFUNC(X509_EXTENSION_get_object) - RESOLVEFUNC(X509_free) - RESOLVEFUNC(X509_gmtime_adj) - RESOLVEFUNC(ASN1_TIME_free) - RESOLVEFUNC(X509_get_ext) - RESOLVEFUNC(X509_get_ext_count) - RESOLVEFUNC(X509_get_ext_d2i) - RESOLVEFUNC(X509V3_EXT_get) - RESOLVEFUNC(X509V3_EXT_d2i) - RESOLVEFUNC(X509_EXTENSION_get_critical) - RESOLVEFUNC(X509_EXTENSION_get_data) - RESOLVEFUNC(BASIC_CONSTRAINTS_free) - RESOLVEFUNC(AUTHORITY_KEYID_free) - RESOLVEFUNC(GENERAL_NAME_free) - RESOLVEFUNC(ASN1_STRING_print) - RESOLVEFUNC(X509_check_issued) - RESOLVEFUNC(X509_get_issuer_name) - RESOLVEFUNC(X509_get_subject_name) - RESOLVEFUNC(X509_get_serialNumber) - RESOLVEFUNC(X509_verify_cert) - RESOLVEFUNC(d2i_X509) - RESOLVEFUNC(i2d_X509) + RESOLVEFUNC(SSL_write) + RESOLVEFUNC(X509_NAME_entry_count) + RESOLVEFUNC(X509_NAME_get_entry) + RESOLVEFUNC(X509_NAME_ENTRY_get_data) + RESOLVEFUNC(X509_NAME_ENTRY_get_object) + RESOLVEFUNC(X509_PUBKEY_get) + RESOLVEFUNC(X509_STORE_free) + RESOLVEFUNC(X509_STORE_new) + RESOLVEFUNC(X509_STORE_add_cert) + RESOLVEFUNC(X509_STORE_CTX_free) + RESOLVEFUNC(X509_STORE_CTX_init) + RESOLVEFUNC(X509_STORE_CTX_new) + RESOLVEFUNC(X509_STORE_CTX_set_purpose) + RESOLVEFUNC(X509_STORE_CTX_get_error) + RESOLVEFUNC(X509_STORE_CTX_get_error_depth) + RESOLVEFUNC(X509_STORE_CTX_get_current_cert) + RESOLVEFUNC(X509_STORE_CTX_get0_store) + RESOLVEFUNC(X509_cmp) + RESOLVEFUNC(X509_STORE_CTX_get_ex_data) + RESOLVEFUNC(X509_dup) + RESOLVEFUNC(X509_print) + RESOLVEFUNC(X509_digest) + RESOLVEFUNC(X509_EXTENSION_get_object) + RESOLVEFUNC(X509_free) + RESOLVEFUNC(X509_gmtime_adj) + RESOLVEFUNC(ASN1_TIME_free) + RESOLVEFUNC(X509_get_ext) + RESOLVEFUNC(X509_get_ext_count) + RESOLVEFUNC(X509_get_ext_d2i) + RESOLVEFUNC(X509V3_EXT_get) + RESOLVEFUNC(X509V3_EXT_d2i) + RESOLVEFUNC(X509_EXTENSION_get_critical) + RESOLVEFUNC(X509_EXTENSION_get_data) + RESOLVEFUNC(BASIC_CONSTRAINTS_free) + RESOLVEFUNC(AUTHORITY_KEYID_free) + RESOLVEFUNC(GENERAL_NAME_free) + RESOLVEFUNC(ASN1_STRING_print) + RESOLVEFUNC(X509_check_issued) + RESOLVEFUNC(X509_get_issuer_name) + RESOLVEFUNC(X509_get_subject_name) + RESOLVEFUNC(X509_get_serialNumber) + RESOLVEFUNC(X509_verify_cert) + RESOLVEFUNC(d2i_X509) + RESOLVEFUNC(i2d_X509) #if OPENSSL_VERSION_MAJOR < 3 - RESOLVEFUNC(SSL_CTX_load_verify_locations) + RESOLVEFUNC(SSL_CTX_load_verify_locations) #else - RESOLVEFUNC(SSL_CTX_load_verify_dir) + RESOLVEFUNC(SSL_CTX_load_verify_dir) #endif // OPENSSL_VERSION_MAJOR - RESOLVEFUNC(i2d_SSL_SESSION) - RESOLVEFUNC(d2i_SSL_SESSION) + RESOLVEFUNC(i2d_SSL_SESSION) + RESOLVEFUNC(d2i_SSL_SESSION) #ifndef OPENSSL_NO_NEXTPROTONEG - RESOLVEFUNC(SSL_select_next_proto) - RESOLVEFUNC(SSL_CTX_set_next_proto_select_cb) - RESOLVEFUNC(SSL_get0_next_proto_negotiated) - RESOLVEFUNC(SSL_set_alpn_protos) - RESOLVEFUNC(SSL_CTX_set_alpn_select_cb) - RESOLVEFUNC(SSL_get0_alpn_selected) + RESOLVEFUNC(SSL_select_next_proto) + RESOLVEFUNC(SSL_CTX_set_next_proto_select_cb) + RESOLVEFUNC(SSL_get0_next_proto_negotiated) + RESOLVEFUNC(SSL_set_alpn_protos) + RESOLVEFUNC(SSL_CTX_set_alpn_select_cb) + RESOLVEFUNC(SSL_get0_alpn_selected) #endif // !OPENSSL_NO_NEXTPROTONEG #if QT_CONFIG(dtls) - RESOLVEFUNC(SSL_CTX_set_cookie_generate_cb) - RESOLVEFUNC(SSL_CTX_set_cookie_verify_cb) - RESOLVEFUNC(DTLS_server_method) - RESOLVEFUNC(DTLS_client_method) + RESOLVEFUNC(SSL_CTX_set_cookie_generate_cb) + RESOLVEFUNC(SSL_CTX_set_cookie_verify_cb) + RESOLVEFUNC(DTLS_server_method) + RESOLVEFUNC(DTLS_client_method) #endif // dtls - RESOLVEFUNC(CRYPTO_malloc) - RESOLVEFUNC(DH_new) - RESOLVEFUNC(DH_free) - RESOLVEFUNC(d2i_DHparams) - RESOLVEFUNC(i2d_DHparams) - RESOLVEFUNC(DH_check) - RESOLVEFUNC(BN_bin2bn) + RESOLVEFUNC(CRYPTO_malloc) + RESOLVEFUNC(BN_bin2bn) #ifndef OPENSSL_NO_EC - RESOLVEFUNC(EC_get_builtin_curves) + RESOLVEFUNC(EC_get_builtin_curves) #endif // OPENSSL_NO_EC - RESOLVEFUNC(PKCS12_parse) - RESOLVEFUNC(d2i_PKCS12_bio) - RESOLVEFUNC(PKCS12_free) + RESOLVEFUNC(PKCS12_parse) + RESOLVEFUNC(d2i_PKCS12_bio) + RESOLVEFUNC(PKCS12_free) + return true; + }(); - symbolsResolved.storeRelease(true); - return true; + return symbolsResolved; } #endif // QT_CONFIG(library) diff --git a/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h b/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h index 97999d4c39..a93c110b3f 100644 --- a/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h +++ b/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h @@ -185,7 +185,11 @@ QT_BEGIN_NAMESPACE // **************** Static declarations ****************** #endif // !defined QT_LINKED_OPENSSL - +#if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 3 +typedef uint64_t qssloptions; +#else +typedef unsigned long qssloptions; +#endif // TODO: the following lines previously were a part of 1.1 - specific header. // To reduce the amount of the change, I'm directly copying and pasting the // content of the header here. Later, can be better sorted/split into groups, @@ -209,7 +213,7 @@ void q_OPENSSL_sk_push(OPENSSL_STACK *st, void *data); void q_OPENSSL_sk_free(OPENSSL_STACK *a); void * q_OPENSSL_sk_value(OPENSSL_STACK *a, int b); int q_SSL_session_reused(SSL *a); -unsigned long q_SSL_CTX_set_options(SSL_CTX *ctx, unsigned long op); +qssloptions q_SSL_CTX_set_options(SSL_CTX *ctx, qssloptions op); int q_OPENSSL_init_ssl(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings); size_t q_SSL_get_client_random(SSL *a, unsigned char *out, size_t outlen); size_t q_SSL_SESSION_get_master_key(const SSL_SESSION *session, unsigned char *out, size_t outlen); @@ -229,7 +233,6 @@ void q_X509_STORE_set_verify_cb(X509_STORE *ctx, X509_STORE_CTX_verify_cb verify int q_X509_STORE_set_ex_data(X509_STORE *ctx, int idx, void *data); void *q_X509_STORE_get_ex_data(X509_STORE *r, int idx); STACK_OF(X509) *q_X509_STORE_CTX_get0_chain(X509_STORE_CTX *ctx); -void q_DH_get0_pqg(const DH *dh, const BIGNUM **p, const BIGNUM **q, const BIGNUM **g); # define q_SSL_load_error_strings() q_OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS \ | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL) @@ -387,7 +390,6 @@ int q_OBJ_obj2nid(const ASN1_OBJECT *a); #define q_EVP_get_digestbynid(a) q_EVP_get_digestbyname(q_OBJ_nid2sn(a)) EVP_PKEY *q_PEM_read_bio_PrivateKey(BIO *a, EVP_PKEY **b, pem_password_cb *c, void *d); -DH *q_PEM_read_bio_DHparams(BIO *a, DH **b, pem_password_cb *c, void *d); int q_PEM_write_bio_PrivateKey(BIO *a, EVP_PKEY *b, const EVP_CIPHER *c, unsigned char *d, int e, pem_password_cb *f, void *g); int q_PEM_write_bio_PrivateKey_traditional(BIO *a, EVP_PKEY *b, const EVP_CIPHER *c, unsigned char *d, @@ -500,14 +502,21 @@ X509 *q_X509_STORE_CTX_get_current_cert(X509_STORE_CTX *ctx); X509_STORE *q_X509_STORE_CTX_get0_store(X509_STORE_CTX *ctx); // Diffie-Hellman support +#ifndef OPENSSL_NO_DEPRECATED_3_0 DH *q_DH_new(); void q_DH_free(DH *dh); +int q_DH_check(DH *dh, int *codes); +void q_DH_get0_pqg(const DH *dh, const BIGNUM **p, const BIGNUM **q, const BIGNUM **g); + DH *q_d2i_DHparams(DH **a, const unsigned char **pp, long length); int q_i2d_DHparams(DH *a, unsigned char **p); -int q_DH_check(DH *dh, int *codes); + +DH *q_PEM_read_bio_DHparams(BIO *a, DH **b, pem_password_cb *c, void *d); +#endif // OPENSSL_NO_DEPRECATED_3_0 BIGNUM *q_BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); #define q_SSL_CTX_set_tmp_dh(ctx, dh) q_SSL_CTX_ctrl((ctx), SSL_CTRL_SET_TMP_DH, 0, (char *)dh) +#define q_SSL_CTX_set_dh_auto(ctx, onoff) q_SSL_CTX_ctrl(ctx,SSL_CTRL_SET_DH_AUTO,onoff,NULL) #ifndef OPENSSL_NO_EC // EC Diffie-Hellman support @@ -658,6 +667,7 @@ void *q_CRYPTO_malloc(size_t num, const char *file, int line); #define q_OPENSSL_malloc(num) q_CRYPTO_malloc(num, "", 0) void q_CRYPTO_free(void *str, const char *file, int line); # define q_OPENSSL_free(addr) q_CRYPTO_free(addr, "", 0) +int q_CRYPTO_memcmp(const void * in_a, const void * in_b, size_t len); void q_SSL_set_info_callback(SSL *ssl, void (*cb) (const SSL *ssl, int type, int val)); const char *q_SSL_alert_type_string(int value); diff --git a/src/plugins/tls/openssl/qtls_openssl.cpp b/src/plugins/tls/openssl/qtls_openssl.cpp index 27abf1bc8d..57d09a649b 100644 --- a/src/plugins/tls/openssl/qtls_openssl.cpp +++ b/src/plugins/tls/openssl/qtls_openssl.cpp @@ -92,7 +92,7 @@ QSslCertificate findCertificateToFetch(const QList<QSslError> &tlsErrors, bool c if (checkAIA) { const auto extensions = certToFetch.extensions(); for (const auto &ext : extensions) { - if (ext.oid() == QStringLiteral("1.3.6.1.5.5.7.1.1")) // See RFC 4325 + if (ext.oid() == u"1.3.6.1.5.5.7.1.1") // See RFC 4325 return certToFetch; } //The only reason we check this extensions is because an application set trusted @@ -490,7 +490,7 @@ void TlsCryptographOpenSSL::init(QSslSocket *qObj, QSslSocketPrivate *dObj) handshakeInterrupted = false; fetchAuthorityInformation = false; - caToFetch = QSslCertificate{}; + caToFetch.reset(); } void TlsCryptographOpenSSL::checkSettingSslContext(std::shared_ptr<QSslContext> tlsContext) @@ -577,7 +577,7 @@ bool TlsCryptographOpenSSL::startHandshake() auto configuration = q->sslConfiguration(); if (!errorsReportedFromCallback) { const auto &peerCertificateChain = configuration.peerCertificateChain(); - for (const auto ¤tError : qAsConst(lastErrors)) { + for (const auto ¤tError : std::as_const(lastErrors)) { emit q->peerVerifyError(QTlsPrivate::X509CertificateOpenSSL::openSSLErrorToQSslError(currentError.code, peerCertificateChain.value(currentError.depth))); if (q->state() != QAbstractSocket::ConnectedState) @@ -697,7 +697,7 @@ bool TlsCryptographOpenSSL::startHandshake() // Translate errors from the error list into QSslErrors. errors.reserve(errors.size() + errorList.size()); - for (const auto &error : qAsConst(errorList)) + for (const auto &error : std::as_const(errorList)) errors << X509CertificateOpenSSL::openSSLErrorToQSslError(error.code, peerCertificateChain.value(error.depth)); if (!errors.isEmpty()) { @@ -749,7 +749,7 @@ void TlsCryptographOpenSSL::enableHandshakeContinuation() void TlsCryptographOpenSSL::cancelCAFetch() { fetchAuthorityInformation = false; - caToFetch = QSslCertificate{}; + caToFetch.reset(); } void TlsCryptographOpenSSL::continueHandshake() @@ -1726,11 +1726,11 @@ unsigned TlsCryptographOpenSSL::pskClientTlsCallback(const char *hint, char *ide return 0; // Copy data back into OpenSSL - const int identityLength = qMin(authenticator.identity().length(), authenticator.maximumIdentityLength()); + const int identityLength = qMin(authenticator.identity().size(), authenticator.maximumIdentityLength()); std::memcpy(identity, authenticator.identity().constData(), identityLength); identity[identityLength] = 0; - const int pskLength = qMin(authenticator.preSharedKey().length(), authenticator.maximumPreSharedKeyLength()); + const int pskLength = qMin(authenticator.preSharedKey().size(), authenticator.maximumPreSharedKeyLength()); std::memcpy(psk, authenticator.preSharedKey().constData(), pskLength); return pskLength; } @@ -1752,7 +1752,7 @@ unsigned TlsCryptographOpenSSL::pskServerTlsCallback(const char *identity, unsig return 0; // Copy data back into OpenSSL - const int pskLength = qMin(authenticator.preSharedKey().length(), authenticator.maximumPreSharedKeyLength()); + const int pskLength = qMin(authenticator.preSharedKey().size(), authenticator.maximumPreSharedKeyLength()); std::memcpy(psk, authenticator.preSharedKey().constData(), pskLength); return pskLength; } @@ -1803,7 +1803,7 @@ void TlsCryptographOpenSSL::caRootLoaded(QSslCertificate cert, QSslCertificate t Q_ASSERT(q); //Done, fetched already: - caToFetch = QSslCertificate{}; + caToFetch.reset(); if (fetchAuthorityInformation) { if (!q->sslConfiguration().caCertificates().contains(trustedRoot)) diff --git a/src/plugins/tls/openssl/qtls_openssl_p.h b/src/plugins/tls/openssl/qtls_openssl_p.h index 31fede2ace..65d21a395b 100644 --- a/src/plugins/tls/openssl/qtls_openssl_p.h +++ b/src/plugins/tls/openssl/qtls_openssl_p.h @@ -120,7 +120,7 @@ private: bool handshakeInterrupted = false; bool fetchAuthorityInformation = false; - QSslCertificate caToFetch; + std::optional<QSslCertificate> caToFetch; bool inSetAndEmitError = false; bool pendingFatalAlert = false; diff --git a/src/plugins/tls/openssl/qtlsbackend_openssl.cpp b/src/plugins/tls/openssl/qtlsbackend_openssl.cpp index ea31086fad..d73515724b 100644 --- a/src/plugins/tls/openssl/qtlsbackend_openssl.cpp +++ b/src/plugins/tls/openssl/qtlsbackend_openssl.cpp @@ -17,7 +17,7 @@ #include <QtNetwork/qssl.h> #include <QtCore/qdir.h> -#include <QtCore/qdiriterator.h> +#include <QtCore/qdirlisting.h> #include <QtCore/qlist.h> #include <QtCore/qmutex.h> #include <QtCore/qscopeguard.h> @@ -31,9 +31,13 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -Q_LOGGING_CATEGORY(lcTlsBackend, "qt.tlsbackend.ossl"); +#if defined(Q_OS_WIN) || defined(Q_OS_MACOS) +constexpr auto DefaultWarningLevel = QtCriticalMsg; +#else +constexpr auto DefaultWarningLevel = QtDebugMsg; +#endif -Q_GLOBAL_STATIC(QRecursiveMutex, qt_opensslInitMutex) +Q_LOGGING_CATEGORY(lcTlsBackend, "qt.tlsbackend.ossl", DefaultWarningLevel); static void q_loadCiphersForConnection(SSL *connection, QList<QSslCipher> &ciphers, QList<QSslCipher> &defaultCiphers) @@ -59,8 +63,6 @@ static void q_loadCiphersForConnection(SSL *connection, QList<QSslCipher> &ciphe } } -bool QTlsBackendOpenSSL::s_libraryLoaded = false; -bool QTlsBackendOpenSSL::s_loadedCiphersAndCerts = false; int QTlsBackendOpenSSL::s_indexForSSLExtraData = -1; QString QTlsBackendOpenSSL::getErrorsFromOpenSsl() @@ -92,12 +94,10 @@ void QTlsBackendOpenSSL::clearErrorQueue() bool QTlsBackendOpenSSL::ensureLibraryLoaded() { - if (!q_resolveOpenSslSymbols()) - return false; - - const QMutexLocker locker(qt_opensslInitMutex()); + static bool libraryLoaded = []() { + if (!q_resolveOpenSslSymbols()) + return false; - if (!s_libraryLoaded) { // Initialize OpenSSL. if (q_OPENSSL_init_ssl(0, nullptr) != 1) return false; @@ -119,10 +119,10 @@ bool QTlsBackendOpenSSL::ensureLibraryLoaded() return false; } - s_libraryLoaded = true; - } + return true; + }(); - return true; + return libraryLoaded; } QString QTlsBackendOpenSSL::backendName() const @@ -175,11 +175,24 @@ void QTlsBackendOpenSSL::ensureInitialized() const void QTlsBackendOpenSSL::ensureCiphersAndCertsLoaded() const { - const QMutexLocker locker(qt_opensslInitMutex()); + Q_CONSTINIT static bool initializationStarted = false; + Q_CONSTINIT static QAtomicInt initialized = Q_BASIC_ATOMIC_INITIALIZER(0); + Q_CONSTINIT static QRecursiveMutex initMutex; - if (s_loadedCiphersAndCerts) + if (initialized.loadAcquire()) return; - s_loadedCiphersAndCerts = true; + + const QMutexLocker locker(&initMutex); + + if (initializationStarted || initialized.loadAcquire()) + return; + + // Indicate that the initialization has already started in the current + // thread in case of recursive calls. The atomic variable cannot be used + // for this because it is checked without holding the init mutex. + initializationStarted = true; + + auto guard = qScopeGuard([] { initialized.storeRelease(1); }); resetDefaultCiphers(); resetDefaultEllipticCurves(); @@ -191,11 +204,11 @@ void QTlsBackendOpenSSL::ensureCiphersAndCertsLoaded() const #elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) // check whether we can enable on-demand root-cert loading (i.e. check whether the sym links are there) const QList<QByteArray> dirs = QSslSocketPrivate::unixRootCertDirectories(); - QStringList symLinkFilter; - symLinkFilter << "[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].[0-9]"_L1; + const QStringList symLinkFilter{ + u"[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].[0-9]"_s}; for (const auto &dir : dirs) { - QDirIterator iterator(QLatin1StringView(dir), symLinkFilter, QDir::Files); - if (iterator.hasNext()) { + QDirListing dirList(QString::fromLatin1(dir), symLinkFilter, QDir::Files); + if (dirList.cbegin() != dirList.cend()) { // Not empty QSslSocketPrivate::setRootCertOnDemandLoadingSupported(true); break; } @@ -350,7 +363,9 @@ QList<QSslCertificate> systemCaCertificates() QList<QSslCertificate> systemCerts; #if defined(Q_OS_WIN) HCERTSTORE hSystemStore; - hSystemStore = CertOpenSystemStoreW(0, L"ROOT"); + hSystemStore = + CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, + CERT_STORE_READONLY_FLAG | CERT_SYSTEM_STORE_CURRENT_USER, L"ROOT"); if (hSystemStore) { PCCERT_CONTEXT pc = nullptr; while (1) { @@ -379,13 +394,12 @@ QList<QSslCertificate> systemCaCertificates() currentDir.setNameFilters(QStringList{QStringLiteral("*.pem"), QStringLiteral("*.crt")}); for (const auto &directory : directories) { currentDir.setPath(QLatin1StringView(directory)); - QDirIterator it(currentDir); - while (it.hasNext()) { + for (const auto &dirEntry : QDirListing(currentDir)) { // use canonical path here to not load the same certificate twice if symlinked - certFiles.insert(it.nextFileInfo().canonicalFilePath()); + certFiles.insert(dirEntry.canonicalFilePath()); } } - for (const QString& file : qAsConst(certFiles)) + for (const QString& file : std::as_const(certFiles)) systemCerts.append(QSslCertificate::fromPath(file, QSsl::Pem)); } #endif // platform diff --git a/src/plugins/tls/openssl/qtlsbackend_openssl_p.h b/src/plugins/tls/openssl/qtlsbackend_openssl_p.h index 1c5e68e0c6..b9f1f95df0 100644 --- a/src/plugins/tls/openssl/qtlsbackend_openssl_p.h +++ b/src/plugins/tls/openssl/qtlsbackend_openssl_p.h @@ -41,16 +41,13 @@ public: static void logAndClearErrorQueue(); static void clearErrorQueue(); - static bool ensureLibraryLoaded(); // Index used in SSL_get_ex_data to get the matching TlsCryptographerOpenSSL: - static bool s_libraryLoaded; - static bool s_loadedCiphersAndCerts; static int s_indexForSSLExtraData; static QString msgErrorsDuringHandshake(); static QSslCipher qt_OpenSSL_cipher_to_QSslCipher(const SSL_CIPHER *cipher); private: - + static bool ensureLibraryLoaded(); QString backendName() const override; bool isValid() const override; long tlsLibraryVersionNumber() const override; diff --git a/src/plugins/tls/openssl/qtlskey_openssl.cpp b/src/plugins/tls/openssl/qtlskey_openssl.cpp index b17e1d9d74..294fc2ffcd 100644 --- a/src/plugins/tls/openssl/qtlskey_openssl.cpp +++ b/src/plugins/tls/openssl/qtlskey_openssl.cpp @@ -101,19 +101,19 @@ QByteArray TlsKeyOpenSSL::derFromPem(const QByteArray &pem, QMap<QByteArray, QBy QByteArray der(pem); int headerIndex = der.indexOf(header); - int footerIndex = der.indexOf(footer, headerIndex + header.length()); + int footerIndex = der.indexOf(footer, headerIndex + header.size()); if (type() != QSsl::PublicKey) { if (headerIndex == -1 || footerIndex == -1) { header = pkcs8Header(true); footer = pkcs8Footer(true); headerIndex = der.indexOf(header); - footerIndex = der.indexOf(footer, headerIndex + header.length()); + footerIndex = der.indexOf(footer, headerIndex + header.size()); } if (headerIndex == -1 || footerIndex == -1) { header = pkcs8Header(false); footer = pkcs8Footer(false); headerIndex = der.indexOf(header); - footerIndex = der.indexOf(footer, headerIndex + header.length()); + footerIndex = der.indexOf(footer, headerIndex + header.size()); } } if (headerIndex == -1 || footerIndex == -1) @@ -124,7 +124,7 @@ QByteArray TlsKeyOpenSSL::derFromPem(const QByteArray &pem, QMap<QByteArray, QBy if (der.contains("Proc-Type:")) { // taken from QHttpNetworkReplyPrivate::parseHeader int i = 0; - while (i < der.length()) { + while (i < der.size()) { int j = der.indexOf(':', i); // field-name if (j == -1) break; @@ -143,7 +143,7 @@ QByteArray TlsKeyOpenSSL::derFromPem(const QByteArray &pem, QMap<QByteArray, QBy int length = i -(hasCR ? 1: 0) - j; value += der.mid(j, length).trimmed(); j = ++i; - } while (i < der.length() && (der.at(i) == ' ' || der.at(i) == '\t')); + } while (i < der.size() && (der.at(i) == ' ' || der.at(i) == '\t')); if (i == -1) break; // something is wrong @@ -222,7 +222,7 @@ Qt::HANDLE TlsKeyOpenSSL::handle() const #else qCWarning(lcTlsBackend, "This version of OpenSSL disabled direct manipulation with RSA/DSA/DH/EC_KEY structures, consider using QSsl::Opaque instead."); - return Qt::HANDLE(nullptr); + return Qt::HANDLE(genericKey); #endif } diff --git a/src/plugins/tls/openssl/qwindowscarootfetcher.cpp b/src/plugins/tls/openssl/qwindowscarootfetcher.cpp index 82ad3abfd0..a18aae0b71 100644 --- a/src/plugins/tls/openssl/qwindowscarootfetcher.cpp +++ b/src/plugins/tls/openssl/qwindowscarootfetcher.cpp @@ -245,3 +245,5 @@ QHCertStorePointer QWindowsCaRootFetcher::createAdditionalStore() const } QT_END_NAMESPACE + +#include "moc_qwindowscarootfetcher_p.cpp" diff --git a/src/plugins/tls/openssl/qx509_openssl.cpp b/src/plugins/tls/openssl/qx509_openssl.cpp index 29f98755bd..0cd3749f88 100644 --- a/src/plugins/tls/openssl/qx509_openssl.cpp +++ b/src/plugins/tls/openssl/qx509_openssl.cpp @@ -12,12 +12,13 @@ #include <QtNetwork/qsslsocket.h> #include <QtNetwork/qhostaddress.h> -#include <QtCore/qvarlengtharray.h> -#include <QtCore/qscopeguard.h> -#include <QtCore/qdatetime.h> -#include <QtCore/qiodevice.h> #include <QtCore/qendian.h> +#include <QtCore/qdatetime.h> #include <QtCore/qhash.h> +#include <QtCore/qiodevice.h> +#include <QtCore/qscopeguard.h> +#include <QtCore/qtimezone.h> +#include <QtCore/qvarlengtharray.h> QT_BEGIN_NAMESPACE @@ -77,7 +78,7 @@ QDateTime dateTimeFromASN1(const ASN1_TIME *aTime) if (q_ASN1_TIME_to_tm(aTime, &lTime)) { QDate resDate(lTime.tm_year + 1900, lTime.tm_mon + 1, lTime.tm_mday); QTime resTime(lTime.tm_hour, lTime.tm_min, lTime.tm_sec); - result = QDateTime(resDate, resTime, Qt::UTC); + result = QDateTime(resDate, resTime, QTimeZone::UTC); } return result; @@ -616,7 +617,7 @@ QList<QSslError> X509CertificateOpenSSL::verify(const QList<QSslCertificate> &ca const QString &hostName) { // This was previously QSslSocketPrivate::verify(). - if (certificateChain.count() <= 0) + if (certificateChain.size() <= 0) return {QSslError(QSslError::UnspecifiedError)}; QList<QSslError> errors; @@ -658,7 +659,7 @@ QList<QSslError> X509CertificateOpenSSL::verify(const QList<QSslCertificate> &ca // Build the chain of intermediate certificates STACK_OF(X509) *intermediates = nullptr; - if (certificateChain.length() > 1) { + if (certificateChain.size() > 1) { intermediates = (STACK_OF(X509) *) q_OPENSSL_sk_new_null(); if (!intermediates) { @@ -710,7 +711,7 @@ QList<QSslError> X509CertificateOpenSSL::verify(const QList<QSslCertificate> &ca // Translate errors from the error list into QSslErrors. errors.reserve(errors.size() + lastErrors.size()); - for (const auto &error : qAsConst(lastErrors)) + for (const auto &error : std::as_const(lastErrors)) errors << openSSLErrorToQSslError(error.code, certificateChain.value(error.depth)); return errors; diff --git a/src/plugins/tls/schannel/CMakeLists.txt b/src/plugins/tls/schannel/CMakeLists.txt index f03964069a..a7f7fcd99f 100644 --- a/src/plugins/tls/schannel/CMakeLists.txt +++ b/src/plugins/tls/schannel/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QSchannelBackendPlugin OUTPUT_NAME qschannelbackend CLASS_NAME QSchannelBackend @@ -26,4 +29,6 @@ qt_internal_add_plugin(QSchannelBackendPlugin secur32 bcrypt ncrypt + DEFINES + QT_NO_CAST_FROM_ASCII ) diff --git a/src/plugins/tls/schannel/qtls_schannel.cpp b/src/plugins/tls/schannel/qtls_schannel.cpp index 58e74357d8..a244a90ebc 100644 --- a/src/plugins/tls/schannel/qtls_schannel.cpp +++ b/src/plugins/tls/schannel/qtls_schannel.cpp @@ -26,17 +26,11 @@ #include <security.h> #include <schnlsp.h> -#if NTDDI_VERSION >= NTDDI_WINBLUE && !defined(Q_CC_MINGW) +#if NTDDI_VERSION >= NTDDI_WINBLUE && defined(SECBUFFER_APPLICATION_PROTOCOLS) // ALPN = Application Layer Protocol Negotiation #define SUPPORTS_ALPN 1 #endif -// Redstone 5/1809 has all the API available, but TLS 1.3 is not enabled until a later version of -// Win 10, checked at runtime in supportsTls13() -#if defined(NTDDI_WIN10_RS5) && NTDDI_VERSION >= NTDDI_WIN10_RS5 -#define SUPPORTS_TLS13 1 -#endif - // Not defined in MinGW #ifndef SECBUFFER_ALERT #define SECBUFFER_ALERT 17 @@ -93,6 +87,12 @@ #ifndef SP_PROT_TLS1_3 #define SP_PROT_TLS1_3 (SP_PROT_TLS1_3_CLIENT | SP_PROT_TLS1_3_SERVER) #endif +#ifndef BCRYPT_ECDH_ALGORITHM +#define BCRYPT_ECDH_ALGORITHM L"ECDH" +#endif +#ifndef BCRYPT_ECDSA_ALGORITHM +#define BCRYPT_ECDSA_ALGORITHM L"ECDSA" +#endif /* @future!: @@ -114,10 +114,6 @@ - Check if SEC_I_INCOMPLETE_CREDENTIALS is still returned for both "missing certificate" and "missing PSK" when calling InitializeSecurityContext in "performHandshake". - Medium priority: - - Setting cipher-suites (or ALG_ID) - - People have survived without it in WinRT - Low priority: - Possibly make RAII wrappers for SecBuffer (which I commonly create QScopeGuards for) @@ -133,40 +129,341 @@ Q_LOGGING_CATEGORY(lcTlsBackendSchannel, "qt.tlsbackend.schannel"); QByteArray _q_makePkcs12(const QList<QSslCertificate> &certs, const QSslKey &key, const QString &passPhrase); +namespace { +bool supportsTls13(); +} + namespace QTlsPrivate { -QList<QSslCipher> defaultCiphers() +QList<QSslCipher> defaultCiphers(); + +struct SchannelCipherInfo { + const char *openSslCipherSuite; + const char *schannelCipherSuite; + const char *keyExchangeMethod; + const char *authenticationMethod; + const char *encryptionMethod; + int encryptionBits; + const char *hashMethod; + QList<QSsl::SslProtocol> protocols; +}; + +// The list of supported ciphers according to +// https://learn.microsoft.com/en-us/windows/win32/secauthn/tls-cipher-suites-in-windows-server-2022 +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +std::array<SchannelCipherInfo, 44> schannelCipherInfo = {{ + {"TLS_AES_256_GCM_SHA384", "TLS_AES_256_GCM_SHA384", "", "", "AES", 256, "SHA384", {QSsl::TlsV1_3}}, + {"TLS_AES_128_GCM_SHA256", "TLS_AES_128_GCM_SHA256", "", "", "AES", 128, "SHA256", {QSsl::TlsV1_3}}, + {"ECDHE-ECDSA-AES256-GCM-SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "ECDH", "ECDSA", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"ECDHE-ECDSA-AES128-GCM-SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "ECDH", "ECDSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"ECDHE-RSA-AES256-GCM-SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "ECDH", "RSA", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"ECDHE-RSA-AES128-GCM-SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "ECDH", "RSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"DHE-RSA-AES256-GCM-SHA384", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", "DH", "RSA", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"DHE-RSA-AES128-GCM-SHA256", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", "DH", "RSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"ECDHE-ECDSA-AES256-SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", "ECDH", "ECDSA", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"ECDHE-ECDSA-AES128-SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "ECDH", "ECDSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"ECDHE-RSA-AES256-SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "ECDH", "RSA", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"ECDHE-RSA-AES128-SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "ECDH", "RSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"ECDHE-ECDSA-AES256-SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "ECDH", "ECDSA", "AES", 256, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"ECDHE-ECDSA-AES128-SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "ECDH", "ECDSA", "AES", 128, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"ECDHE-RSA-AES256-SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "ECDH", "RSA", "AES", 256, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"ECDHE-RSA-AES128-SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "ECDH", "RSA", "AES", 128, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"AES256-GCM-SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384", "RSA", "RSA", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"AES128-GCM-SHA256", "TLS_RSA_WITH_AES_128_GCM_SHA256", "RSA", "RSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"AES256-SHA256", "TLS_RSA_WITH_AES_256_CBC_SHA256", "RSA", "RSA", "AES", 256, "SHA256", {QSsl::TlsV1_2}}, + {"AES128-SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA256", "RSA", "RSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"AES256-SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "RSA", "RSA", "AES", 256, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"AES128-SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "RSA", "RSA", "AES", 128, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"DES-CBC3-SHA", "TLS_RSA_WITH_3DES_EDE_CBC_SHA", "RSA", "RSA", "3DES", 168, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"NULL-SHA256", "TLS_RSA_WITH_NULL_SHA256", "RSA", "RSA", "", 0, "SHA256", {QSsl::TlsV1_2}}, + {"NULL-SHA", "TLS_RSA_WITH_NULL_SHA", "RSA", "RSA", "", 0, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + + // the following cipher suites are not enabled by default in schannel provider + {"TLS_CHACHA20_POLY1305_SHA256", "TLS_CHACHA20_POLY1305_SHA256", "", "", "CHACHA20_POLY1305", 0, "", {QSsl::TlsV1_3}}, + {"DHE-RSA-AES256-SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "DH", "RSA", "AES", 256, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"DHE-RSA-AES128-SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "DH", "RSA", "AES", 128, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"DHE-DSS-AES256-SHA256", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", "DH", "DSA", "AES", 256, "SHA256", {QSsl::TlsV1_2}}, + {"DHE-DSS-AES128-SHA256", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", "DH", "DSA", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"DHE-DSS-AES256-SHA", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA", "DH", "DSA", "AES", 256, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"DHE-DSS-AES128-SHA", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", "DH", "DSA", "AES", 128, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"EDH-DSS-DES-CBC3-SHA", "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", "DH", "DSA", "3DES", 168, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"RC4-SHA", "TLS_RSA_WITH_RC4_128_SHA", "RSA", "RSA", "RC4", 128, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"RC4-MD5", "TLS_RSA_WITH_RC4_128_MD5", "RSA", "RSA", "RC4", 128, "MD5", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"DES-CBC-SHA", "TLS_RSA_WITH_DES_CBC_SHA", "RSA", "RSA", "DES", 56, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"EDH-DSS-DES-CBC-SHA", "TLS_DHE_DSS_WITH_DES_CBC_SHA", "DH", "DSA", "DES", 56, "SHA1", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + {"NULL-MD5", "TLS_RSA_WITH_NULL_MD5", "RSA", "RSA", "", 0, "MD5", {QSsl::TlsV1_2, QSsl::TlsV1_1, QSsl::TlsV1_0}}, + + // PSK cipher suites + {"PSK-AES256-GCM-SHA384", "TLS_PSK_WITH_AES_256_GCM_SHA384", "PSK", "", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"PSK-AES128-GCM-SHA256", "TLS_PSK_WITH_AES_128_GCM_SHA256", "PSK", "", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"PSK-AES256-CBC-SHA384", "TLS_PSK_WITH_AES_256_CBC_SHA384", "PSK", "", "AES", 256, "SHA384", {QSsl::TlsV1_2}}, + {"PSK-AES128-CBC-SHA256", "TLS_PSK_WITH_AES_128_CBC_SHA256", "PSK", "", "AES", 128, "SHA256", {QSsl::TlsV1_2}}, + {"PSK-NULL-SHA384", "TLS_PSK_WITH_NULL_SHA384", "PSK", "", "", 0, "SHA384", {QSsl::TlsV1_2}}, + {"PSK-NULL-SHA256", "TLS_PSK_WITH_NULL_SHA256", "PSK", "", "", 0, "SHA256", {QSsl::TlsV1_2}}, +}}; +QT_WARNING_POP + +const SchannelCipherInfo *cipherInfoByOpenSslName(const QString &name) +{ + for (const auto &cipherInfo : schannelCipherInfo) { + if (name == QLatin1StringView(cipherInfo.openSslCipherSuite)) + return &cipherInfo; + } + + return nullptr; +} + +UNICODE_STRING cbcChainingMode = { + sizeof(BCRYPT_CHAIN_MODE_CBC) - 2, + sizeof(BCRYPT_CHAIN_MODE_CBC), + const_cast<PWSTR>(BCRYPT_CHAIN_MODE_CBC) +}; + +UNICODE_STRING gcmChainingMode = { + sizeof(BCRYPT_CHAIN_MODE_GCM) - 2, + sizeof(BCRYPT_CHAIN_MODE_GCM), + const_cast<PWSTR>(BCRYPT_CHAIN_MODE_GCM) +}; + +/** + Determines which algorithms are not used by the requested ciphers to build + up a black list that can be passed to SCH_CREDENTIALS. + */ +QList<CRYPTO_SETTINGS> cryptoSettingsForCiphers(const QList<QSslCipher> &ciphers) +{ + static const QList<QSslCipher> defaultCipherList = defaultCiphers(); + + if (defaultCipherList == ciphers) { + // the ciphers have not been restricted for this session, so no black listing needed + return {}; + } + + QList<const SchannelCipherInfo*> cipherInfo; + + for (const auto &cipher : ciphers) { + if (cipher.isNull()) + continue; + + const auto *info = cipherInfoByOpenSslName(cipher.name()); + if (!cipherInfo.contains(info)) + cipherInfo.append(info); + } + + QList<CRYPTO_SETTINGS> cryptoSettings; + + const auto assignUnicodeString = [](UNICODE_STRING &unicodeString, const wchar_t *characters) { + unicodeString.Length = static_cast<USHORT>(wcslen(characters) * sizeof(WCHAR)); + unicodeString.MaximumLength = unicodeString.Length + sizeof(UNICODE_NULL); + unicodeString.Buffer = const_cast<wchar_t*>(characters); + }; + + // black list of key exchange algorithms + const auto allKeyExchangeAlgorithms = {BCRYPT_RSA_ALGORITHM, + BCRYPT_ECDH_ALGORITHM, + BCRYPT_DH_ALGORITHM}; + + for (const auto &algorithm : allKeyExchangeAlgorithms) { + const auto method = QStringView(algorithm); + + const auto usesMethod = [method](const SchannelCipherInfo *info) { + return QLatin1StringView(info->keyExchangeMethod) == method; + }; + + const bool exclude = std::none_of(cipherInfo.cbegin(), cipherInfo.cend(), usesMethod); + + if (exclude) { + CRYPTO_SETTINGS settings = {}; + settings.eAlgorithmUsage = TlsParametersCngAlgUsageKeyExchange; + assignUnicodeString(settings.strCngAlgId, algorithm); + cryptoSettings.append(settings); + } + } + + // black list of authentication algorithms + const auto allAuthenticationAlgorithms = {BCRYPT_RSA_ALGORITHM, + BCRYPT_DSA_ALGORITHM, + BCRYPT_ECDSA_ALGORITHM, + BCRYPT_DH_ALGORITHM}; + + for (const auto &algorithm : allAuthenticationAlgorithms) { + const auto method = QStringView(algorithm); + + const auto usesMethod = [method](const SchannelCipherInfo *info) { + return QLatin1StringView(info->authenticationMethod) == method; + }; + + const bool exclude = std::none_of(cipherInfo.begin(), cipherInfo.end(), usesMethod); + + if (exclude) { + CRYPTO_SETTINGS settings = {}; + settings.eAlgorithmUsage = TlsParametersCngAlgUsageSignature; + assignUnicodeString(settings.strCngAlgId, algorithm); + cryptoSettings.append(settings); + } + } + + + // black list of encryption algorithms + const auto allEncryptionAlgorithms = {BCRYPT_AES_ALGORITHM, + BCRYPT_RC4_ALGORITHM, + BCRYPT_DES_ALGORITHM, + BCRYPT_3DES_ALGORITHM}; + + for (const auto &algorithm : allEncryptionAlgorithms) { + const auto method = QStringView(algorithm); + + if (method == QLatin1StringView("AES")) { + bool uses128Bit = false; + bool uses256Bit = false; + bool usesGcm = false; + bool usesCbc = false; + for (const auto *info : cipherInfo) { + if (QLatin1StringView(info->encryptionMethod) == method) { + uses128Bit = uses128Bit || (info->encryptionBits == 128); + uses256Bit = uses256Bit || (info->encryptionBits == 256); + usesGcm = usesGcm || + QLatin1StringView(info->schannelCipherSuite).contains("_GCM_"_L1); + usesCbc = usesCbc || + QLatin1StringView(info->schannelCipherSuite).contains("_CBC_"_L1); + } + } + + CRYPTO_SETTINGS settings = {}; + settings.eAlgorithmUsage = TlsParametersCngAlgUsageCipher; + assignUnicodeString(settings.strCngAlgId, algorithm); + + if (usesGcm && !usesCbc) { + settings.cChainingModes = 1; + settings.rgstrChainingModes = &cbcChainingMode; + } else if (!usesGcm && usesCbc) { + settings.cChainingModes = 1; + settings.rgstrChainingModes = &gcmChainingMode; + } + + if (!uses128Bit && uses256Bit) { + settings.dwMinBitLength = 256; + cryptoSettings.append(settings); + } else if (uses128Bit && !uses256Bit) { + settings.dwMaxBitLength = 128; + cryptoSettings.append(settings); + } else if (!uses128Bit && !uses256Bit) { + cryptoSettings.append(settings); + } + } else { + const auto usesMethod = [method](const SchannelCipherInfo *info) { + return QLatin1StringView(info->encryptionMethod) == method; + }; + + const bool exclude = std::none_of(cipherInfo.begin(), cipherInfo.end(), usesMethod); + + if (exclude) { + CRYPTO_SETTINGS settings = {}; + settings.eAlgorithmUsage = TlsParametersCngAlgUsageCipher; + assignUnicodeString(settings.strCngAlgId, algorithm); + cryptoSettings.append(settings); + } + } + } + + // black list of hash algorithms + const auto allHashAlgorithms = {BCRYPT_MD5_ALGORITHM, + BCRYPT_SHA1_ALGORITHM, + BCRYPT_SHA256_ALGORITHM, + BCRYPT_SHA384_ALGORITHM}; + + for (const auto &algorithm : allHashAlgorithms) { + const auto method = QStringView(algorithm); + + const auto usesMethod = [method](const SchannelCipherInfo *info) { + return QLatin1StringView(info->hashMethod) == method; + }; + + const bool exclude = std::none_of(cipherInfo.begin(), cipherInfo.end(), usesMethod); + + if (exclude) { + CRYPTO_SETTINGS settings = {}; + settings.eAlgorithmUsage = TlsParametersCngAlgUsageDigest; + assignUnicodeString(settings.strCngAlgId, algorithm); + cryptoSettings.append(settings); + } + } + + return cryptoSettings; +} + +QList<QSslCipher> ciphersByName(QStringView schannelSuiteName) { - // Previously the code was in QSslSocketBackendPrivate. QList<QSslCipher> ciphers; - // @temp (I hope), stolen from qsslsocket_winrt.cpp - const QString protocolStrings[] = { QStringLiteral("TLSv1"), QStringLiteral("TLSv1.1"), - QStringLiteral("TLSv1.2"), QStringLiteral("TLSv1.3") }; + + for (const auto &cipher : schannelCipherInfo) { + if (QLatin1StringView(cipher.schannelCipherSuite) == schannelSuiteName) { + for (const auto &protocol : cipher.protocols) { QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED - const QSsl::SslProtocol protocols[] = { QSsl::TlsV1_0, QSsl::TlsV1_1, - QSsl::TlsV1_2, QSsl::TlsV1_3 }; + const QString protocolName = ( + protocol == QSsl::TlsV1_0 ? QStringLiteral("TLSv1.0") : + protocol == QSsl::TlsV1_1 ? QStringLiteral("TLSv1.1") : + protocol == QSsl::TlsV1_2 ? QStringLiteral("TLSv1.2") : + protocol == QSsl::TlsV1_3 ? QStringLiteral("TLSv1.3") : + QString()); QT_WARNING_POP - const int size = ARRAYSIZE(protocols); - static_assert(size == ARRAYSIZE(protocolStrings)); - ciphers.reserve(size); - for (int i = 0; i < size; ++i) { - const QSslCipher cipher = QTlsBackend::createCipher(QStringLiteral("Schannel"), - protocols[i], protocolStrings[i]); - ciphers.append(cipher); + ciphers.append(QTlsBackend::createCiphersuite(QLatin1StringView(cipher.openSslCipherSuite), + QLatin1StringView(cipher.keyExchangeMethod), + QLatin1StringView(cipher.encryptionMethod), + QLatin1StringView(cipher.authenticationMethod), + cipher.encryptionBits, + protocol, protocolName)); + } + } } return ciphers; - } -} // namespace QTlsPrivate +QList<QSslCipher> defaultCiphers() +{ + ULONG contextFunctionsCount = {}; + PCRYPT_CONTEXT_FUNCTIONS contextFunctions = {}; -namespace { -bool supportsTls13(); + const auto status = BCryptEnumContextFunctions(CRYPT_LOCAL, L"SSL", NCRYPT_SCHANNEL_INTERFACE, + &contextFunctionsCount, &contextFunctions); + if (!NT_SUCCESS(status)) { + qCWarning(lcTlsBackendSchannel, "Failed to enumerate ciphers"); + return {}; + } + + const bool supportsV13 = supportsTls13(); + + QList<QSslCipher> ciphers; + + for (ULONG index = 0; index < contextFunctions->cFunctions; ++index) { + const auto suiteName = QStringView(contextFunctions->rgpszFunctions[index]); + + const QList<QSslCipher> allCiphers = ciphersByName(suiteName); + + for (const auto &cipher : allCiphers) { + if (!supportsV13 && (cipher.protocol() == QSsl::TlsV1_3)) + continue; + + ciphers.append(cipher); + } + } + + BCryptFreeBuffer(contextFunctions); + + return ciphers; +} + +bool containsTls13Cipher(const QList<QSslCipher> &ciphers) +{ + return std::any_of(ciphers.cbegin(), ciphers.cend(), + [](const QSslCipher &cipher) { return cipher.protocol() == QSsl::TlsV1_3; }); } +} // namespace QTlsPrivate + bool QSchannelBackend::s_loadedCiphersAndCerts = false; Q_GLOBAL_STATIC(QRecursiveMutex, qt_schannel_mutex) @@ -299,7 +596,11 @@ QList<QSslCertificate> QSchannelBackend::systemCaCertificatesImplementation() // Similar to non-Darwin version found in qtlsbackend_openssl.cpp, // QTlsPrivate::systemCaCertificates function. QList<QSslCertificate> systemCerts; - auto hSystemStore = QHCertStorePointer(CertOpenSystemStore(0, L"ROOT")); + + auto hSystemStore = QHCertStorePointer( + CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, + CERT_STORE_READONLY_FLAG | CERT_SYSTEM_STORE_CURRENT_USER, L"ROOT")); + if (hSystemStore) { PCCERT_CONTEXT pc = nullptr; while ((pc = CertFindCertificateInStore(hSystemStore.get(), X509_ASN_ENCODING, 0, @@ -320,6 +621,11 @@ QTlsPrivate::X509DerReaderPtr QSchannelBackend::X509DerReader() const return QTlsPrivate::X509CertificateGeneric::certificatesFromDer; } +QTlsPrivate::X509Pkcs12ReaderPtr QSchannelBackend::X509Pkcs12Reader() const +{ + return QTlsPrivate::X509CertificateSchannel::importPkcs12; +} + namespace { SecBuffer createSecBuffer(void *ptr, unsigned long length, unsigned long bufferType) @@ -384,7 +690,6 @@ QString schannelErrorToString(qint32 status) bool supportsTls13() { -#ifdef SUPPORTS_TLS13 static bool supported = []() { const auto current = QOperatingSystemVersion::current(); // 20221 just happens to be the preview version I run on my laptop where I tested TLS 1.3. @@ -392,10 +697,8 @@ bool supportsTls13() QOperatingSystemVersion(QOperatingSystemVersion::Windows, 10, 0, 20221); return current >= minimum; }(); + return supported; -#else - return false; -#endif } DWORD toSchannelProtocol(QSsl::SslProtocol protocol) @@ -460,17 +763,15 @@ QT_WARNING_POP return protocols; } -#ifdef SUPPORTS_TLS13 // In the new API that descended down upon us we are not asked which protocols we want // but rather which protocols we don't want. So now we have this function to disable // anything that is not enabled. -DWORD toSchannelProtocolNegated(QSsl::SslProtocol protocol) +DWORD negatedSchannelProtocols(DWORD wantedProtocols) { DWORD protocols = SP_PROT_ALL; // all protocols - protocols &= ~toSchannelProtocol(protocol); // minus the one(s) we want + protocols &= ~wantedProtocols; // minus the one(s) we want return protocols; } -#endif /*! \internal @@ -508,8 +809,7 @@ bool netscapeWrongCertType(const QList<QSslCertificateExtension> &extensions, bo const auto netscapeIt = std::find_if( extensions.cbegin(), extensions.cend(), [](const QSslCertificateExtension &extension) { - const auto netscapeCertType = QStringLiteral("2.16.840.1.113730.1.1"); - return extension.oid() == netscapeCertType; + return extension.oid() == u"2.16.840.1.113730.1.1"; }); if (netscapeIt != extensions.cend()) { const QByteArray netscapeCertTypeByte = netscapeIt->value().toByteArray(); @@ -680,6 +980,10 @@ qint64 checkIncompleteData(const SecBuffer &secBuffer) return 0; } +DWORD defaultCredsFlag() +{ + return qEnvironmentVariableIsSet("QT_SCH_DEFAULT_CREDS") ? 0 : SCH_CRED_NO_DEFAULT_CREDS; +} } // anonymous namespace @@ -719,6 +1023,10 @@ bool TlsCryptographSchannel::sendToken(void *token, unsigned long tokenLength, b Q_ASSERT(d); auto *plainSocket = d->plainTcpSocket(); Q_ASSERT(plainSocket); + if (plainSocket->state() == QAbstractSocket::UnconnectedState || !plainSocket->isValid() + || !plainSocket->isOpen()) { + return false; + } const qint64 written = plainSocket->write(static_cast<const char *>(token), tokenLength); if (written != qint64(tokenLength)) { @@ -781,7 +1089,7 @@ bool TlsCryptographSchannel::acquireCredentialsHandle() Q_ASSERT(schannelState == SchannelState::InitializeHandshake); const bool isClient = d->tlsMode() == QSslSocket::SslClientMode; - const DWORD protocols = toSchannelProtocol(configuration.protocol()); + DWORD protocols = toSchannelProtocol(configuration.protocol()); if (protocols == DWORD(-1)) { setErrorAndEmit(d, QAbstractSocket::SslInvalidUserDataError, QSslSocket::tr("Invalid protocol chosen")); @@ -835,80 +1143,50 @@ bool TlsCryptographSchannel::acquireCredentialsHandle() certsCount = 1; Q_ASSERT(localCertContext); } - void *credentials = nullptr; -#ifdef SUPPORTS_TLS13 + + const QList<QSslCipher> ciphers = configuration.ciphers(); + if (!ciphers.isEmpty() && !containsTls13Cipher(ciphers)) + protocols &= ~SP_PROT_TLS1_3; + + QList<CRYPTO_SETTINGS> cryptoSettings; + if (!ciphers.isEmpty()) + cryptoSettings = cryptoSettingsForCiphers(ciphers); + TLS_PARAMETERS tlsParameters = { 0, nullptr, - toSchannelProtocolNegated(configuration.protocol()), // what protocols to disable + negatedSchannelProtocols(protocols), // what protocols to disable + static_cast<DWORD>(cryptoSettings.size()), + (cryptoSettings.isEmpty() ? nullptr : cryptoSettings.data()), + 0 + }; + + SCH_CREDENTIALS credentials = { + SCH_CREDENTIALS_VERSION, 0, + certsCount, + &localCertContext, nullptr, - 0 + 0, + nullptr, + 0, + SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT | defaultCredsFlag(), + 1, + &tlsParameters }; - if (supportsTls13()) { - SCH_CREDENTIALS *cred = new SCH_CREDENTIALS{ - SCH_CREDENTIALS_VERSION, - 0, - certsCount, - &localCertContext, - nullptr, - 0, - nullptr, - 0, - SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT - | SCH_CRED_NO_DEFAULT_CREDS, - 1, - &tlsParameters - }; - credentials = cred; - } else -#endif // SUPPORTS_TLS13 - { - SCHANNEL_CRED *cred = new SCHANNEL_CRED{ - SCHANNEL_CRED_VERSION, // dwVersion - certsCount, // cCreds - &localCertContext, // paCred (certificate(s) containing a private key for authentication) - nullptr, // hRootStore - - 0, // cMappers (reserved) - nullptr, // aphMappers (reserved) - - 0, // cSupportedAlgs - nullptr, // palgSupportedAlgs (nullptr = system default) - - protocols, // grbitEnabledProtocols - 0, // dwMinimumCipherStrength (0 = system default) - 0, // dwMaximumCipherStrength (0 = system default) - 0, // dwSessionLifespan (0 = schannel default, 10 hours) - SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT - | SCH_CRED_NO_DEFAULT_CREDS, // dwFlags - 0 // dwCredFormat (must be 0) - }; - credentials = cred; - } - Q_ASSERT(credentials != nullptr); TimeStamp expiration{}; auto status = AcquireCredentialsHandle(nullptr, // pszPrincipal (unused) const_cast<wchar_t *>(UNISP_NAME), // pszPackage isClient ? SECPKG_CRED_OUTBOUND : SECPKG_CRED_INBOUND, // fCredentialUse nullptr, // pvLogonID (unused) - credentials, // pAuthData + &credentials, // pAuthData nullptr, // pGetKeyFn (unused) nullptr, // pvGetKeyArgument (unused) &credentialHandle, // phCredential &expiration // ptsExpir ); -#ifdef SUPPORTS_TLS13 - if (supportsTls13()) { - delete static_cast<SCH_CREDENTIALS *>(credentials); - } else -#endif // SUPPORTS_TLS13 - { - delete static_cast<SCHANNEL_CRED *>(credentials); - } - if (status != SEC_E_OK) { setErrorAndEmit(d, QAbstractSocket::SslInternalError, schannelErrorToString(status)); return false; @@ -1113,7 +1391,8 @@ bool TlsCryptographSchannel::performHandshake() auto *plainSocket = d->plainTcpSocket(); Q_ASSERT(plainSocket); - if (plainSocket->state() == QAbstractSocket::UnconnectedState) { + if (plainSocket->state() == QAbstractSocket::UnconnectedState || !plainSocket->isValid() + || !plainSocket->isOpen()) { setErrorAndEmit(d, QAbstractSocket::RemoteHostClosedError, QSslSocket::tr("The TLS/SSL connection has been closed")); return false; @@ -1282,6 +1561,11 @@ bool TlsCryptographSchannel::verifyHandshake() // Get session cipher info status = QueryContextAttributes(&contextHandle, + SECPKG_ATTR_CIPHER_INFO, + &cipherInfo); + CHECK_STATUS(status); + + status = QueryContextAttributes(&contextHandle, SECPKG_ATTR_CONNECTION_INFO, &connectionInfo); CHECK_STATUS(status); @@ -1434,6 +1718,7 @@ void TlsCryptographSchannel::reset() deallocateContext(); freeCredentialsHandle(); // in case we already had one (@future: session resumption requires re-use) + cipherInfo = {}; connectionInfo = {}; streamSizes = {}; @@ -1483,8 +1768,10 @@ void TlsCryptographSchannel::transmit() return; // This function should not have been called // Can happen if called through QSslSocket::abort->QSslSocket::close->QSslSocket::flush->here - if (plainSocket->state() == QAbstractSocket::SocketState::UnconnectedState) + if (plainSocket->state() == QAbstractSocket::UnconnectedState || !plainSocket->isValid() + || !plainSocket->isOpen()) { return; + } if (schannelState != SchannelState::Done) { continueHandshake(); @@ -1556,132 +1843,131 @@ void TlsCryptographSchannel::transmit() } } - if (q->isEncrypted()) { // Decrypt data from remote - int totalRead = 0; - bool hadIncompleteData = false; - const auto readBufferMaxSize = d->maxReadBufferSize(); - while (!readBufferMaxSize || buffer.size() < readBufferMaxSize) { - if (missingData > plainSocket->bytesAvailable() - && (!readBufferMaxSize || readBufferMaxSize >= missingData)) { + int totalRead = 0; + bool hadIncompleteData = false; + const auto readBufferMaxSize = d->maxReadBufferSize(); + while (!readBufferMaxSize || buffer.size() < readBufferMaxSize) { + if (missingData > plainSocket->bytesAvailable() + && (!readBufferMaxSize || readBufferMaxSize >= missingData)) { #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, "We're still missing %lld bytes, will check later.", - missingData); + qCDebug(lcTlsBackendSchannel, "We're still missing %lld bytes, will check later.", + missingData); #endif - break; - } + break; + } - missingData = 0; - const qint64 bytesRead = readToBuffer(intermediateBuffer, plainSocket); + missingData = 0; + const qint64 bytesRead = readToBuffer(intermediateBuffer, plainSocket); #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, "Read %lld encrypted bytes from the socket", bytesRead); + qCDebug(lcTlsBackendSchannel, "Read %lld encrypted bytes from the socket", bytesRead); #endif - if (intermediateBuffer.length() == 0 || (hadIncompleteData && bytesRead == 0)) { + if (intermediateBuffer.length() == 0 || (hadIncompleteData && bytesRead == 0)) { #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, - (hadIncompleteData ? "No new data received, leaving loop!" - : "Nothing to decrypt, leaving loop!")); + qCDebug(lcTlsBackendSchannel, + hadIncompleteData ? "No new data received, leaving loop!" + : "Nothing to decrypt, leaving loop!"); #endif - break; - } - hadIncompleteData = false; + break; + } + hadIncompleteData = false; #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, "Total amount of bytes to decrypt: %d", - intermediateBuffer.length()); + qCDebug(lcTlsBackendSchannel, "Total amount of bytes to decrypt: %d", + intermediateBuffer.length()); #endif - SecBuffer dataBuffer[4]{ - createSecBuffer(intermediateBuffer, SECBUFFER_DATA), - createSecBuffer(nullptr, 0, SECBUFFER_EMPTY), - createSecBuffer(nullptr, 0, SECBUFFER_EMPTY), - createSecBuffer(nullptr, 0, SECBUFFER_EMPTY) - }; - SecBufferDesc message{ - SECBUFFER_VERSION, - ARRAYSIZE(dataBuffer), - dataBuffer - }; - auto status = DecryptMessage(&contextHandle, &message, 0, nullptr); - if (status == SEC_E_OK || status == SEC_I_RENEGOTIATE || status == SEC_I_CONTEXT_EXPIRED) { - // There can still be 0 output even if it succeeds, this is fine - if (dataBuffer[1].cbBuffer > 0) { - // It is always decrypted in-place. - // But [0] is the STREAM_HEADER, [1] is the DATA and [2] is the STREAM_TRAILER. - // The pointers in all of those still point into 'intermediateBuffer'. - buffer.append(static_cast<char *>(dataBuffer[1].pvBuffer), - dataBuffer[1].cbBuffer); - totalRead += dataBuffer[1].cbBuffer; + SecBuffer dataBuffer[4]{ + createSecBuffer(intermediateBuffer, SECBUFFER_DATA), + createSecBuffer(nullptr, 0, SECBUFFER_EMPTY), + createSecBuffer(nullptr, 0, SECBUFFER_EMPTY), + createSecBuffer(nullptr, 0, SECBUFFER_EMPTY) + }; + SecBufferDesc message{ + SECBUFFER_VERSION, + ARRAYSIZE(dataBuffer), + dataBuffer + }; + auto status = DecryptMessage(&contextHandle, &message, 0, nullptr); + if (status == SEC_E_OK || status == SEC_I_RENEGOTIATE || status == SEC_I_CONTEXT_EXPIRED) { + // There can still be 0 output even if it succeeds, this is fine + if (dataBuffer[1].cbBuffer > 0) { + // It is always decrypted in-place. + // But [0] is the STREAM_HEADER, [1] is the DATA and [2] is the STREAM_TRAILER. + // The pointers in all of those still point into 'intermediateBuffer'. + buffer.append(static_cast<char *>(dataBuffer[1].pvBuffer), + dataBuffer[1].cbBuffer); + totalRead += dataBuffer[1].cbBuffer; #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, "Decrypted %lu bytes. New read buffer size: %d", - dataBuffer[1].cbBuffer, buffer.size()); + qCDebug(lcTlsBackendSchannel, "Decrypted %lu bytes. New read buffer size: %d", + dataBuffer[1].cbBuffer, buffer.size()); #endif - } - if (dataBuffer[3].BufferType == SECBUFFER_EXTRA) { - // https://docs.microsoft.com/en-us/windows/desktop/secauthn/extra-buffers-returned-by-schannel - // dataBuffer[3].cbBuffer indicates the amount of bytes _NOT_ processed, - // the rest need to be stored. - retainExtraData(intermediateBuffer, dataBuffer[3]); - } else { - intermediateBuffer.resize(0); - } } + if (dataBuffer[3].BufferType == SECBUFFER_EXTRA) { + // https://docs.microsoft.com/en-us/windows/desktop/secauthn/extra-buffers-returned-by-schannel + // dataBuffer[3].cbBuffer indicates the amount of bytes _NOT_ processed, + // the rest need to be stored. + retainExtraData(intermediateBuffer, dataBuffer[3]); + } else { + intermediateBuffer.resize(0); + } + } - if (status == SEC_E_INCOMPLETE_MESSAGE) { - missingData = checkIncompleteData(dataBuffer[0]); + if (status == SEC_E_INCOMPLETE_MESSAGE) { + missingData = checkIncompleteData(dataBuffer[0]); #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, - "We didn't have enough data to decrypt anything, will try again!"); + qCDebug(lcTlsBackendSchannel, "We didn't have enough data to decrypt anything, will try again!"); #endif - // We try again, but if we don't get any more data then we leave - hadIncompleteData = true; - } else if (status == SEC_E_INVALID_HANDLE) { - // I don't think this should happen, if it does we're done... - qCWarning(lcTlsBackendSchannel, "The internal SSPI handle is invalid!"); - Q_UNREACHABLE(); - } else if (status == SEC_E_INVALID_TOKEN) { - qCWarning(lcTlsBackendSchannel, "Got SEC_E_INVALID_TOKEN!"); - Q_UNREACHABLE(); // Happened once due to a bug, but shouldn't generally happen(?) - } else if (status == SEC_E_MESSAGE_ALTERED) { - // The message has been altered, disconnect now. - shutdown = true; // skips sending the shutdown alert - disconnectFromHost(); - setErrorAndEmit(d, QAbstractSocket::SslInternalError, - schannelErrorToString(status)); - break; - } else if (status == SEC_E_OUT_OF_SEQUENCE) { - // @todo: I don't know if this one is actually "fatal".. - // This path might never be hit as it seems this is for connection-oriented connections, - // while SEC_E_MESSAGE_ALTERED is for stream-oriented ones (what we use). - shutdown = true; // skips sending the shutdown alert - disconnectFromHost(); - setErrorAndEmit(d, QAbstractSocket::SslInternalError, - schannelErrorToString(status)); - break; - } else if (status == SEC_I_CONTEXT_EXPIRED) { - // 'remote' has initiated a shutdown - disconnectFromHost(); - setErrorAndEmit(d, QAbstractSocket::RemoteHostClosedError, - schannelErrorToString(status)); - break; - } else if (status == SEC_I_RENEGOTIATE) { - // 'remote' wants to renegotiate + // We try again, but if we don't get any more data then we leave + hadIncompleteData = true; + } else if (status == SEC_E_INVALID_HANDLE) { + // I don't think this should happen, if it does we're done... + qCWarning(lcTlsBackendSchannel, "The internal SSPI handle is invalid!"); + Q_UNREACHABLE(); + } else if (status == SEC_E_INVALID_TOKEN) { + // Supposedly we have an invalid token, it's under-documented what + // this means, so to be safe we disconnect. + shutdown = true; + disconnectFromHost(); + setErrorAndEmit(d, QAbstractSocket::SslInternalError, schannelErrorToString(status)); + break; + } else if (status == SEC_E_MESSAGE_ALTERED) { + // The message has been altered, disconnect now. + shutdown = true; // skips sending the shutdown alert + disconnectFromHost(); + setErrorAndEmit(d, QAbstractSocket::SslInternalError, + schannelErrorToString(status)); + break; + } else if (status == SEC_E_OUT_OF_SEQUENCE) { + // @todo: I don't know if this one is actually "fatal".. + // This path might never be hit as it seems this is for connection-oriented connections, + // while SEC_E_MESSAGE_ALTERED is for stream-oriented ones (what we use). + shutdown = true; // skips sending the shutdown alert + disconnectFromHost(); + setErrorAndEmit(d, QAbstractSocket::SslInternalError, + schannelErrorToString(status)); + break; + } else if (status == SEC_I_CONTEXT_EXPIRED) { + // 'remote' has initiated a shutdown + disconnectFromHost(); + break; + } else if (status == SEC_I_RENEGOTIATE) { + // 'remote' wants to renegotiate #ifdef QSSLSOCKET_DEBUG - qCDebug(lcTlsBackendSchannel, "The peer wants to renegotiate."); + qCDebug(lcTlsBackendSchannel, "The peer wants to renegotiate."); #endif - schannelState = SchannelState::Renegotiate; - renegotiating = true; + schannelState = SchannelState::Renegotiate; + renegotiating = true; - // We need to call 'continueHandshake' or else there's no guarantee it ever gets called - continueHandshake(); - break; - } + // We need to call 'continueHandshake' or else there's no guarantee it ever gets called + continueHandshake(); + break; } + } - if (totalRead) { - if (bool *readyReadEmittedPointer = d->readyReadPointer()) - *readyReadEmittedPointer = true; - emit q->readyRead(); - emit q->channelReadyRead(0); - } + if (totalRead) { + if (bool *readyReadEmittedPointer = d->readyReadPointer()) + *readyReadEmittedPointer = true; + emit q->readyRead(); + emit q->channelReadyRead(0); } } @@ -1776,30 +2062,36 @@ void TlsCryptographSchannel::disconnectFromHost() if (SecIsValidHandle(&contextHandle)) { if (!shutdown) { shutdown = true; - if (plainSocket->state() != QAbstractSocket::UnconnectedState) { - if (q->isEncrypted()) { - // Read as much as possible because this is likely our last chance - qint64 tempMax = d->maxReadBufferSize(); - d->setMaxReadBufferSize(0); - transmit(); - d->setMaxReadBufferSize(tempMax); - sendShutdown(); - } + if (plainSocket->state() != QAbstractSocket::UnconnectedState && q->isEncrypted()) { + sendShutdown(); + transmit(); } } } - if (plainSocket->state() != QAbstractSocket::UnconnectedState) - plainSocket->disconnectFromHost(); + plainSocket->disconnectFromHost(); } void TlsCryptographSchannel::disconnected() { Q_ASSERT(d); + auto *plainSocket = d->plainTcpSocket(); + Q_ASSERT(plainSocket); + d->setEncrypted(false); shutdown = true; - d->setEncrypted(false); - deallocateContext(); - freeCredentialsHandle(); + if (plainSocket->bytesAvailable() > 0 || hasUndecryptedData()) { + // Read as much as possible because this is likely our last chance + qint64 tempMax = d->maxReadBufferSize(); + d->setMaxReadBufferSize(0); // Unlimited + transmit(); + d->setMaxReadBufferSize(tempMax); + // Since there were bytes still available we don't want to deallocate + // our context yet. It will happen later, when the socket is re-used or + // destroyed. + } else { + deallocateContext(); + freeCredentialsHandle(); + } } QSslCipher TlsCryptographSchannel::sessionCipher() const @@ -1807,8 +2099,17 @@ QSslCipher TlsCryptographSchannel::sessionCipher() const Q_ASSERT(q); if (!q->isEncrypted()) - return QSslCipher(); - return QSslCipher(QStringLiteral("Schannel"), sessionProtocol()); + return {}; + + const auto sessionProtocol = toQtSslProtocol(connectionInfo.dwProtocol); + + const auto ciphers = ciphersByName(QStringView(cipherInfo.szCipherSuite)); + for (const auto& cipher : ciphers) { + if (cipher.protocol() == sessionProtocol) + return cipher; + } + + return {}; } QSsl::SslProtocol TlsCryptographSchannel::sessionProtocol() const @@ -1992,7 +2293,10 @@ bool TlsCryptographSchannel::verifyCertContext(CERT_CONTEXT *certContext) // the Ca list, not just included during verification. // That being said, it's not trivial to add the root certificates (if and only if they // came from the system root store). And I don't see this mentioned in our documentation. - auto rootStore = QHCertStorePointer(CertOpenSystemStore(0, L"ROOT")); + auto rootStore = QHCertStorePointer( + CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, + CERT_STORE_READONLY_FLAG | CERT_SYSTEM_STORE_CURRENT_USER, L"ROOT")); + if (!rootStore) { #ifdef QSSLSOCKET_DEBUG qCWarning(lcTlsBackendSchannel, "Failed to open the system root CA certificate store!"); @@ -2106,10 +2410,40 @@ bool TlsCryptographSchannel::verifyCertContext(CERT_CONTEXT *certContext) verifyDepth = DWORD(q->peerVerifyDepth()); const auto &caCertificates = q->sslConfiguration().caCertificates(); + + if (!rootCertOnDemandLoadingAllowed() + && !(chain->TrustStatus.dwErrorStatus & CERT_TRUST_IS_PARTIAL_CHAIN) + && (q->peerVerifyMode() == QSslSocket::VerifyPeer + || (isClient && q->peerVerifyMode() == QSslSocket::AutoVerifyPeer))) { + // When verifying a peer Windows "helpfully" builds a chain that + // may include roots from the system store. But we don't want that if + // the user has set their own CA certificates. + // Since Windows claims this is not a partial chain the root is included + // and we have to check that it is one of our configured CAs. + CERT_CHAIN_ELEMENT *element = chain->rgpElement[chain->cElement - 1]; + QSslCertificate certificate = getCertificateFromChainElement(element); + if (!caCertificates.contains(certificate)) { + auto error = QSslError(QSslError::CertificateUntrusted, certificate); + sslErrors += error; + emit q->peerVerifyError(error); + if (q->state() != QAbstractSocket::ConnectedState) + return false; + } + } + QList<QSslCertificate> peerCertificateChain; for (DWORD i = 0; i < verifyDepth; i++) { CERT_CHAIN_ELEMENT *element = chain->rgpElement[i]; QSslCertificate certificate = getCertificateFromChainElement(element); + if (certificate.isNull()) { + const auto &previousCert = !peerCertificateChain.isEmpty() ? peerCertificateChain.last() + : QSslCertificate(); + auto error = QSslError(QSslError::SslError::UnableToGetIssuerCertificate, previousCert); + sslErrors += error; + emit q->peerVerifyError(error); + if (previousCert.isNull() || q->state() != QAbstractSocket::ConnectedState) + return false; + } const QList<QSslCertificateExtension> extensions = certificate.extensions(); #ifdef QSSLSOCKET_DEBUG @@ -2257,7 +2591,7 @@ bool TlsCryptographSchannel::verifyCertContext(CERT_CONTEXT *certContext) } if (!peerCertificateChain.isEmpty()) - QTlsBackend::storePeerCertificate(d, peerCertificateChain.first()); + QTlsBackend::storePeerCertificate(d, peerCertificateChain.constFirst()); const auto &configuration = q->sslConfiguration(); // Probably, updated by QTlsBackend::storePeerCertificate etc. // @Note: Somewhat copied from qsslsocket_mac.cpp diff --git a/src/plugins/tls/schannel/qtls_schannel_p.h b/src/plugins/tls/schannel/qtls_schannel_p.h index f6bfdbc7cf..fab8777249 100644 --- a/src/plugins/tls/schannel/qtls_schannel_p.h +++ b/src/plugins/tls/schannel/qtls_schannel_p.h @@ -93,6 +93,7 @@ private: QSslSocket *q = nullptr; QSslSocketPrivate *d = nullptr; + SecPkgContext_CipherInfo cipherInfo = {}; SecPkgContext_ConnectionInfo connectionInfo = {}; SecPkgContext_StreamSizes streamSizes = {}; diff --git a/src/plugins/tls/schannel/qtlsbackend_schannel_p.h b/src/plugins/tls/schannel/qtlsbackend_schannel_p.h index a70f8922a6..7c2d675e79 100644 --- a/src/plugins/tls/schannel/qtlsbackend_schannel_p.h +++ b/src/plugins/tls/schannel/qtlsbackend_schannel_p.h @@ -57,6 +57,7 @@ private: QTlsPrivate::X509PemReaderPtr X509PemReader() const override; QTlsPrivate::X509DerReaderPtr X509DerReader() const override; + QTlsPrivate::X509Pkcs12ReaderPtr X509Pkcs12Reader() const override; static bool s_loadedCiphersAndCerts; }; diff --git a/src/plugins/tls/schannel/qx509_schannel.cpp b/src/plugins/tls/schannel/qx509_schannel.cpp index 5a5e625268..d9d82dce29 100644 --- a/src/plugins/tls/schannel/qx509_schannel.cpp +++ b/src/plugins/tls/schannel/qx509_schannel.cpp @@ -1,9 +1,11 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qtlsbackend_schannel_p.h" #include "qtlskey_schannel_p.h" #include "qx509_schannel_p.h" +#include <QtCore/private/qsystemerror_p.h> #include <QtNetwork/private/qsslcertificate_p.h> #include <memory> @@ -39,13 +41,173 @@ QSslCertificate X509CertificateSchannel::QSslCertificate_from_CERT_CONTEXT(const QByteArray derData = QByteArray((const char *)certificateContext->pbCertEncoded, certificateContext->cbCertEncoded); QSslCertificate certificate(derData, QSsl::Der); - - auto *certBackend = QTlsBackend::backend<X509CertificateSchannel>(certificate); - Q_ASSERT(certBackend); - certBackend->certificateContext = CertDuplicateCertificateContext(certificateContext); + if (!certificate.isNull()) { + auto *certBackend = QTlsBackend::backend<X509CertificateSchannel>(certificate); + Q_ASSERT(certBackend); + certBackend->certificateContext = CertDuplicateCertificateContext(certificateContext); + } return certificate; } +bool X509CertificateSchannel::importPkcs12(QIODevice *device, QSslKey *key, QSslCertificate *cert, + QList<QSslCertificate> *caCertificates, + const QByteArray &passPhrase) +{ + // These are required + Q_ASSERT(device); + Q_ASSERT(key); + Q_ASSERT(cert); + + QByteArray pkcs12data = device->readAll(); + if (pkcs12data.size() == 0) + return false; + + CRYPT_DATA_BLOB dataBlob; + dataBlob.cbData = pkcs12data.size(); + dataBlob.pbData = reinterpret_cast<BYTE*>(pkcs12data.data()); + + const auto password = QString::fromUtf8(passPhrase); + + const DWORD flags = (CRYPT_EXPORTABLE | PKCS12_NO_PERSIST_KEY | PKCS12_PREFER_CNG_KSP); + + auto certStore = QHCertStorePointer(PFXImportCertStore(&dataBlob, + reinterpret_cast<LPCWSTR>(password.utf16()), + flags)); + + if (!certStore) { + qCWarning(lcTlsBackendSchannel, "Failed to import PFX data: %s", + qPrintable(QSystemError::windowsString())); + return false; + } + + // first extract the certificate with the private key + const auto certContext = QPCCertContextPointer(CertFindCertificateInStore(certStore.get(), + X509_ASN_ENCODING | + PKCS_7_ASN_ENCODING, + 0, + CERT_FIND_HAS_PRIVATE_KEY, + nullptr, nullptr)); + + if (!certContext) { + qCWarning(lcTlsBackendSchannel, "Failed to find certificate in PFX store: %s", + qPrintable(QSystemError::windowsString())); + return false; + } + + *cert = QSslCertificate_from_CERT_CONTEXT(certContext.get()); + + // retrieve the private key for the certificate + NCRYPT_KEY_HANDLE keyHandle = {}; + DWORD keyHandleSize = sizeof(keyHandle); + if (!CertGetCertificateContextProperty(certContext.get(), CERT_NCRYPT_KEY_HANDLE_PROP_ID, + &keyHandle, &keyHandleSize)) { + qCWarning(lcTlsBackendSchannel, "Failed to find private key handle in certificate context: %s", + qPrintable(QSystemError::windowsString())); + return false; + } + + SECURITY_STATUS securityStatus = ERROR_SUCCESS; + + // we need the 'NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG' to make NCryptExportKey succeed + DWORD policy = (NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG); + DWORD policySize = sizeof(policy); + + securityStatus = NCryptSetProperty(keyHandle, NCRYPT_EXPORT_POLICY_PROPERTY, + reinterpret_cast<BYTE*>(&policy), policySize, 0); + if (securityStatus != ERROR_SUCCESS) { + qCWarning(lcTlsBackendSchannel, "Failed to update export policy of private key: 0x%x", + static_cast<unsigned int>(securityStatus)); + return false; + } + + DWORD blobSize = {}; + securityStatus = NCryptExportKey(keyHandle, {}, BCRYPT_RSAFULLPRIVATE_BLOB, + nullptr, nullptr, 0, &blobSize, 0); + if (securityStatus != ERROR_SUCCESS) { + qCWarning(lcTlsBackendSchannel, "Failed to retrieve private key size: 0x%x", + static_cast<unsigned int>(securityStatus)); + return false; + } + + std::vector<BYTE> blob(blobSize); + securityStatus = NCryptExportKey(keyHandle, {}, BCRYPT_RSAFULLPRIVATE_BLOB, + nullptr, blob.data(), blobSize, &blobSize, 0); + if (securityStatus != ERROR_SUCCESS) { + qCWarning(lcTlsBackendSchannel, "Failed to retrieve private key from certificate: 0x%x", + static_cast<unsigned int>(securityStatus)); + return false; + } + + DWORD privateKeySize = {}; + + if (!CryptEncodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, CNG_RSA_PRIVATE_KEY_BLOB, + blob.data(), nullptr, &privateKeySize)) { + qCWarning(lcTlsBackendSchannel, "Failed to encode private key to key info: %s", + qPrintable(QSystemError::windowsString())); + return false; + } + + std::vector<BYTE> privateKeyData(privateKeySize); + + CRYPT_PRIVATE_KEY_INFO privateKeyInfo = {}; + privateKeyInfo.Algorithm.pszObjId = const_cast<PSTR>(szOID_RSA_RSA); + privateKeyInfo.PrivateKey.cbData = privateKeySize; + privateKeyInfo.PrivateKey.pbData = privateKeyData.data(); + + if (!CryptEncodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + CNG_RSA_PRIVATE_KEY_BLOB, blob.data(), + privateKeyInfo.PrivateKey.pbData, &privateKeyInfo.PrivateKey.cbData)) { + qCWarning(lcTlsBackendSchannel, "Failed to encode private key to key info: %s", + qPrintable(QSystemError::windowsString())); + return false; + } + + + DWORD derSize = {}; + + if (!CryptEncodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS_PRIVATE_KEY_INFO, + &privateKeyInfo, nullptr, &derSize)) { + qCWarning(lcTlsBackendSchannel, "Failed to encode key info to DER format: %s", + qPrintable(QSystemError::windowsString())); + + return false; + } + + QByteArray derData(derSize, Qt::Uninitialized); + + if (!CryptEncodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS_PRIVATE_KEY_INFO, + &privateKeyInfo, reinterpret_cast<BYTE*>(derData.data()), &derSize)) { + qCWarning(lcTlsBackendSchannel, "Failed to encode key info to DER format: %s", + qPrintable(QSystemError::windowsString())); + + return false; + } + + *key = QSslKey(derData, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey); + if (key->isNull()) { + qCWarning(lcTlsBackendSchannel, "Failed to parse private key from DER format"); + return false; + } + + // fetch all the remaining certificates as CA certificates + if (caCertificates) { + PCCERT_CONTEXT caCertContext = nullptr; + while ((caCertContext = CertFindCertificateInStore(certStore.get(), + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_ANY, nullptr, caCertContext))) { + if (CertCompareCertificate(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + certContext->pCertInfo, caCertContext->pCertInfo)) + continue; // ignore the certificate with private key + + auto caCertificate = QSslCertificate_from_CERT_CONTEXT(caCertContext); + + caCertificates->append(caCertificate); + } + } + + return true; +} + } // namespace QTlsPrivate QT_END_NAMESPACE diff --git a/src/plugins/tls/schannel/qx509_schannel_p.h b/src/plugins/tls/schannel/qx509_schannel_p.h index 17b91983f2..4625c9584c 100644 --- a/src/plugins/tls/schannel/qx509_schannel_p.h +++ b/src/plugins/tls/schannel/qx509_schannel_p.h @@ -36,6 +36,10 @@ public: Qt::HANDLE handle() const override; static QSslCertificate QSslCertificate_from_CERT_CONTEXT(const CERT_CONTEXT *certificateContext); + + static bool importPkcs12(QIODevice *device, QSslKey *key, QSslCertificate *cert, + QList<QSslCertificate> *caCertificates, + const QByteArray &passPhrase); private: const CERT_CONTEXT *certificateContext = nullptr; diff --git a/src/plugins/tls/securetransport/CMakeLists.txt b/src/plugins/tls/securetransport/CMakeLists.txt index 0355049157..bb560229e8 100644 --- a/src/plugins/tls/securetransport/CMakeLists.txt +++ b/src/plugins/tls/securetransport/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + qt_internal_add_plugin(QSecureTransportBackendPlugin OUTPUT_NAME qsecuretransportbackend CLASS_NAME QSecureTransportBackend diff --git a/src/plugins/tls/securetransport/qtls_st.cpp b/src/plugins/tls/securetransport/qtls_st.cpp index d2e5132752..48b7f3364f 100644 --- a/src/plugins/tls/securetransport/qtls_st.cpp +++ b/src/plugins/tls/securetransport/qtls_st.cpp @@ -315,42 +315,38 @@ void TlsCryptographSecureTransport::continueHandshake() qCDebug(lcSecureTransport) << d->plainTcpSocket() << "connection encrypted"; #endif -#if QT_DARWIN_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13_4, __IPHONE_11_0, __TVOS_11_0, __WATCHOS_4_0) // Unlike OpenSSL, Secure Transport does not allow to negotiate protocols via // a callback during handshake. We can only set our list of preferred protocols // (and send it during handshake) and then receive what our peer has sent to us. // And here we can finally try to find a match (if any). const auto &configuration = q->sslConfiguration(); - if (__builtin_available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)) { - const auto &requestedProtocols = configuration.allowedNextProtocols(); - if (const int requestedCount = requestedProtocols.size()) { - QTlsBackend::setAlpnStatus(d, QSslConfiguration::NextProtocolNegotiationNone); - QTlsBackend::setNegotiatedProtocol(d, {}); - - QCFType<CFArrayRef> cfArray; - const OSStatus result = SSLCopyALPNProtocols(context, &cfArray); - if (result == errSecSuccess && cfArray && CFArrayGetCount(cfArray)) { - const int size = CFArrayGetCount(cfArray); - QList<QString> peerProtocols(size); - for (int i = 0; i < size; ++i) - peerProtocols[i] = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(cfArray, i)); - - for (int i = 0; i < requestedCount; ++i) { - const auto requestedName = QString::fromLatin1(requestedProtocols[i]); - for (int j = 0; j < size; ++j) { - if (requestedName == peerProtocols[j]) { - QTlsBackend::setNegotiatedProtocol(d, requestedName.toLatin1()); - QTlsBackend::setAlpnStatus(d, QSslConfiguration::NextProtocolNegotiationNegotiated); - break; - } - } - if (configuration.nextProtocolNegotiationStatus() == QSslConfiguration::NextProtocolNegotiationNegotiated) + const auto &requestedProtocols = configuration.allowedNextProtocols(); + if (const int requestedCount = requestedProtocols.size()) { + QTlsBackend::setAlpnStatus(d, QSslConfiguration::NextProtocolNegotiationNone); + QTlsBackend::setNegotiatedProtocol(d, {}); + + QCFType<CFArrayRef> cfArray; + const OSStatus result = SSLCopyALPNProtocols(context, &cfArray); + if (result == errSecSuccess && cfArray && CFArrayGetCount(cfArray)) { + const int size = CFArrayGetCount(cfArray); + QList<QString> peerProtocols(size); + for (int i = 0; i < size; ++i) + peerProtocols[i] = QString::fromCFString((CFStringRef)CFArrayGetValueAtIndex(cfArray, i)); + + for (int i = 0; i < requestedCount; ++i) { + const auto requestedName = QString::fromLatin1(requestedProtocols[i]); + for (int j = 0; j < size; ++j) { + if (requestedName == peerProtocols[j]) { + QTlsBackend::setNegotiatedProtocol(d, requestedName.toLatin1()); + QTlsBackend::setAlpnStatus(d, QSslConfiguration::NextProtocolNegotiationNegotiated); break; + } } + if (configuration.nextProtocolNegotiationStatus() == QSslConfiguration::NextProtocolNegotiationNegotiated) + break; } } } -#endif // QT_DARWIN_PLATFORM_SDK_EQUAL_OR_ABOVE if (!renegotiating) emit q->encrypted(); @@ -377,6 +373,7 @@ void TlsCryptographSecureTransport::disconnectFromHost() if (context) { if (!shutdown) { SSLClose(context); + context.reset(nullptr); shutdown = true; } } @@ -694,35 +691,31 @@ bool TlsCryptographSecureTransport::initSslContext() return false; } -#if QT_DARWIN_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13_4, __IPHONE_11_0, __TVOS_11_0, __WATCHOS_4_0) - if (__builtin_available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)) { - const auto protocolNames = configuration.allowedNextProtocols(); - QCFType<CFMutableArrayRef> cfNames(CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks)); - if (cfNames) { - for (const QByteArray &name : protocolNames) { - if (name.size() > 255) { - qCWarning(lcSecureTransport) << "TLS ALPN extension" << name - << "is too long and will be ignored."; - continue; - } else if (name.isEmpty()) { - continue; - } - QCFString cfName(QString::fromLatin1(name).toCFString()); - CFArrayAppendValue(cfNames, cfName); + const auto protocolNames = configuration.allowedNextProtocols(); + QCFType<CFMutableArrayRef> cfNames(CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks)); + if (cfNames) { + for (const QByteArray &name : protocolNames) { + if (name.size() > 255) { + qCWarning(lcSecureTransport) << "TLS ALPN extension" << name + << "is too long and will be ignored."; + continue; + } else if (name.isEmpty()) { + continue; } + QCFString cfName(QString::fromLatin1(name).toCFString()); + CFArrayAppendValue(cfNames, cfName); + } - if (CFArrayGetCount(cfNames)) { - // Up to the application layer to check that negotiation - // failed, and handle this non-TLS error, we do not handle - // the result of this call as an error: - if (SSLSetALPNProtocols(context, cfNames) != errSecSuccess) - qCWarning(lcSecureTransport) << "SSLSetALPNProtocols failed - too long protocol names?"; - } - } else { - qCWarning(lcSecureTransport) << "failed to allocate ALPN names array"; + if (CFArrayGetCount(cfNames)) { + // Up to the application layer to check that negotiation + // failed, and handle this non-TLS error, we do not handle + // the result of this call as an error: + if (SSLSetALPNProtocols(context, cfNames) != errSecSuccess) + qCWarning(lcSecureTransport) << "SSLSetALPNProtocols failed - too long protocol names?"; } + } else { + qCWarning(lcSecureTransport) << "failed to allocate ALPN names array"; } -#endif // QT_DARWIN_PLATFORM_SDK_EQUAL_OR_ABOVE if (mode == QSslSocket::SslClientMode) { // enable Server Name Indication (SNI) @@ -1083,7 +1076,7 @@ bool TlsCryptographSecureTransport::verifyPeerTrust() QTlsBackend::storePeerCertificate(d, peerCertificateChain.at(0)); // Check the whole chain for blacklisting (including root, as we check for subjectInfo and issuer): - for (const QSslCertificate &cert : qAsConst(peerCertificateChain)) { + for (const QSslCertificate &cert : std::as_const(peerCertificateChain)) { if (QSslCertificatePrivate::isBlacklisted(cert) && !canIgnoreVerify) { const QSslError error(QSslError::CertificateBlacklisted, cert); errors << error; @@ -1134,8 +1127,6 @@ bool TlsCryptographSecureTransport::verifyPeerTrust() QCFType<CFDataRef> certData = cert.toDer().toCFData(); if (QCFType<SecCertificateRef> secRef = SecCertificateCreateWithData(nullptr, certData)) CFArrayAppendValue(certArray, secRef); - else - qCWarning(lcSecureTransport, "Failed to create SecCertificate from QSslCertificate"); } SecTrustSetAnchorCertificates(trust, certArray); diff --git a/src/plugins/tls/securetransport/qtls_st_p.h b/src/plugins/tls/securetransport/qtls_st_p.h index 16f7104e67..2903ef4815 100644 --- a/src/plugins/tls/securetransport/qtls_st_p.h +++ b/src/plugins/tls/securetransport/qtls_st_p.h @@ -27,6 +27,11 @@ #include <QtNetwork/qabstractsocket.h> #include <QtNetwork/private/qsslsocket_p.h> +#warning SecureTransport was deprecated in macOS 10.15 and iOS 13, \ +and is no longer supported. We should be using Network.framework instead. \ +See QTBUG-85231 for more information. +QT_WARNING_DISABLE_DEPRECATED + #include <Security/Security.h> #include <Security/SecureTransport.h> diff --git a/src/plugins/tls/securetransport/qtlsbackend_st.cpp b/src/plugins/tls/securetransport/qtlsbackend_st.cpp index 4c385b5af6..54e45d1720 100644 --- a/src/plugins/tls/securetransport/qtlsbackend_st.cpp +++ b/src/plugins/tls/securetransport/qtlsbackend_st.cpp @@ -357,3 +357,5 @@ QTlsPrivate::TlsCryptograph *QSecureTransportBackend::createTlsCryptograph() con QT_END_NAMESPACE + +#include "moc_qtlsbackend_st_p.cpp" diff --git a/src/plugins/tls/shared/qasn1element.cpp b/src/plugins/tls/shared/qasn1element.cpp index cbf038f569..97be46866d 100644 --- a/src/plugins/tls/shared/qasn1element.cpp +++ b/src/plugins/tls/shared/qasn1element.cpp @@ -6,13 +6,17 @@ #include <QtCore/qdatastream.h> #include <QtCore/qdatetime.h> +#include <QtCore/qtimezone.h> #include <QtCore/qlist.h> #include <QDebug> +#include <private/qtools_p.h> #include <limits> QT_BEGIN_NAMESPACE +using namespace QtMiscUtils; + typedef QMap<QByteArray, QByteArray> OidNameMap; static OidNameMap createOidMap() { @@ -213,11 +217,6 @@ QDateTime QAsn1Element::toDateTime() const // QDateTime::fromString is lenient and accepts +- signs in front // of the year; but ASN.1 doesn't allow them. - const auto isAsciiDigit = [](char c) - { - return c >= '0' && c <= '9'; - }; - if (!isAsciiDigit(mValue[0])) return result; @@ -226,31 +225,28 @@ QDateTime QAsn1Element::toDateTime() const return result; if (mType == UtcTimeType && mValue.size() == 13) { - result = QDateTime::fromString(QString::fromLatin1(mValue), - QStringLiteral("yyMMddHHmmsst")); - if (!result.isValid()) - return result; - - Q_ASSERT(result.timeSpec() == Qt::UTC); - - QDate date = result.date(); - // RFC 2459: // Where YY is greater than or equal to 50, the year shall be // interpreted as 19YY; and // // Where YY is less than 50, the year shall be interpreted as 20YY. // - // QDateTime interprets the 'yy' format as 19yy, so we may need to adjust - // the year (bring it in the [1950, 2049] range). - if (date.year() < 1950) - result.setDate(date.addYears(100)); + // so use 1950 as base year. + constexpr int rfc2459CenturyStart = 1950; + const QLatin1StringView inputView(mValue); + QDate date = QDate::fromString(inputView.first(6), u"yyMMdd", rfc2459CenturyStart); + if (!date.isValid()) + return result; - Q_ASSERT(result.date().year() >= 1950); - Q_ASSERT(result.date().year() <= 2049); + Q_ASSERT(date.year() >= rfc2459CenturyStart); + Q_ASSERT(date.year() < 100 + rfc2459CenturyStart); + + QTime time = QTime::fromString(inputView.sliced(6, 6), u"HHmmss"); + if (!time.isValid()) + return result; + result = QDateTime(date, time, QTimeZone::UTC); } else if (mType == GeneralizedTimeType && mValue.size() == 15) { - result = QDateTime::fromString(QString::fromLatin1(mValue), - QStringLiteral("yyyyMMddHHmmsst")); + result = QDateTime::fromString(QString::fromLatin1(mValue), u"yyyyMMddHHmmsst"); } return result; diff --git a/src/plugins/tls/shared/qsslsocket_mac_shared.cpp b/src/plugins/tls/shared/qsslsocket_mac_shared.cpp index f40d2fb770..1257240ee2 100644 --- a/src/plugins/tls/shared/qsslsocket_mac_shared.cpp +++ b/src/plugins/tls/shared/qsslsocket_mac_shared.cpp @@ -6,6 +6,7 @@ #include <QtNetwork/qsslcertificate.h> +#include <QtCore/qloggingcategory.h> #include <QtCore/qglobal.h> #include <QtCore/qdebug.h> @@ -21,6 +22,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcX509, "qt.mac.shared.x509"); + #ifdef Q_OS_MACOS namespace { @@ -74,6 +77,52 @@ bool isCaCertificateTrusted(SecCertificateRef cfCert, int domain) return false; } +bool canDERBeParsed(CFDataRef derData, const QSslCertificate &qtCert) +{ + // We are observing certificates, that while accepted when we copy them + // from the keychain(s), later give us 'Failed to create SslCertificate + // from QSslCertificate'. It's interesting to know at what step the failure + // occurred. Let's check it and skip it below if it's not valid. + + auto checkDer = [](CFDataRef derData, const char *source) + { + Q_ASSERT(source); + Q_ASSERT(derData); + + const auto cfLength = CFDataGetLength(derData); + if (cfLength <= 0) { + qCWarning(lcX509) << source << "returned faulty DER data with invalid length."; + return false; + } + + QCFType<SecCertificateRef> secRef = SecCertificateCreateWithData(nullptr, derData); + if (!secRef) { + qCWarning(lcX509) << source << "returned faulty DER data which cannot be parsed back."; + return false; + } + return true; + }; + + if (!checkDer(derData, "SecCertificateCopyData")) { + qCDebug(lcX509) << "Faulty QSslCertificate is:" << qtCert;// Just in case we managed to parse something. + return false; + } + + // Generic parser failed? + if (qtCert.isNull()) { + qCWarning(lcX509, "QSslCertificate failed to parse DER"); + return false; + } + + const QCFType<CFDataRef> qtDerData = qtCert.toDer().toCFData(); + if (!checkDer(qtDerData, "QSslCertificate")) { + qCWarning(lcX509) << "Faulty QSslCertificate is:" << qtCert; + return false; + } + + return true; +} + } // unnamed namespace #endif // Q_OS_MACOS @@ -94,8 +143,19 @@ QList<QSslCertificate> systemCaCertificates() SecCertificateRef cfCert = (SecCertificateRef)CFArrayGetValueAtIndex(cfCerts, i); QCFType<CFDataRef> derData = SecCertificateCopyData(cfCert); if (isCaCertificateTrusted(cfCert, dom)) { - if (derData) - systemCerts << QSslCertificate(QByteArray::fromCFData(derData), QSsl::Der); + if (derData) { + const auto newCert = QSslCertificate(QByteArray::fromCFData(derData), QSsl::Der); + if (!canDERBeParsed(derData, newCert)) { + // Last attempt to get some information about the certificate: + CFShow(cfCert); + continue; + } + systemCerts << newCert; + } else { + // "Returns NULL if the data passed in the certificate parameter + // is not a valid certificate object." + qCWarning(lcX509, "SecCertificateCopyData returned invalid DER data (nullptr)."); + } } } } diff --git a/src/plugins/tls/shared/qwincrypt_p.h b/src/plugins/tls/shared/qwincrypt_p.h index 1b1f0f16c0..48ca4247fa 100644 --- a/src/plugins/tls/shared/qwincrypt_p.h +++ b/src/plugins/tls/shared/qwincrypt_p.h @@ -40,6 +40,16 @@ struct QHCertStoreDeleter { // A simple RAII type used by Schannel code and Window CA fetcher class: using QHCertStorePointer = std::unique_ptr<void, QHCertStoreDeleter>; +struct QPCCertContextDeleter { + void operator()(PCCERT_CONTEXT context) const + { + CertFreeCertificateContext(context); + } +}; + +// A simple RAII type used by Schannel code +using QPCCertContextPointer = std::unique_ptr<const CERT_CONTEXT, QPCCertContextDeleter>; + QT_END_NAMESPACE #endif // QWINCRYPT_P_H diff --git a/src/plugins/tls/shared/qx509_generic.cpp b/src/plugins/tls/shared/qx509_generic.cpp index cfe2786680..5006db1a72 100644 --- a/src/plugins/tls/shared/qx509_generic.cpp +++ b/src/plugins/tls/shared/qx509_generic.cpp @@ -118,7 +118,7 @@ QList<QSslCertificate> X509CertificateGeneric::certificatesFromPem(const QByteAr QByteArray decoded = QByteArray::fromBase64( QByteArray::fromRawData(pem.data() + startPos, endPos - startPos)); - certificates << certificatesFromDer(decoded, 1);; + certificates << certificatesFromDer(decoded, 1); } return certificates; @@ -188,7 +188,7 @@ bool X509CertificateGeneric::parse(const QByteArray &data) if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType) return false; - QByteArray issuerDer = data.mid(dataStream.device()->pos() - elem.value().length(), elem.value().length()); + QByteArray issuerDer = data.mid(dataStream.device()->pos() - elem.value().size(), elem.value().size()); issuerInfoEntries = elem.toInfo(); // validity period @@ -215,7 +215,7 @@ bool X509CertificateGeneric::parse(const QByteArray &data) if (!elem.read(certStream) || elem.type() != QAsn1Element::SequenceType) return false; - QByteArray subjectDer = data.mid(dataStream.device()->pos() - elem.value().length(), elem.value().length()); + QByteArray subjectDer = data.mid(dataStream.device()->pos() - elem.value().size(), elem.value().size()); subjectInfoEntries = elem.toInfo(); subjectMatchesIssuer = issuerDer == subjectDer; @@ -285,7 +285,7 @@ bool X509CertificateGeneric::parse(const QByteArray &data) case QAsn1Element::IpAddressType: { QHostAddress ipAddress; QByteArray ipAddrValue = nameElem.value(); - switch (ipAddrValue.length()) { + switch (ipAddrValue.size()) { case 4: // IPv4 ipAddress = QHostAddress(qFromBigEndian(*reinterpret_cast<quint32 *>(ipAddrValue.data()))); break; diff --git a/src/plugins/tracing/CMakeLists.txt b/src/plugins/tracing/CMakeLists.txt new file mode 100644 index 0000000000..823e11c174 --- /dev/null +++ b/src/plugins/tracing/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +function(make_includable input_file output_file) + get_filename_component(infile "${CMAKE_CURRENT_SOURCE_DIR}/${input_file}" ABSOLUTE) + set(outfile ${CMAKE_CURRENT_BINARY_DIR}/${output_file}) + file(READ ${infile} content) + set(content "R\"(${content})\"") + file(WRITE ${outfile} "${content}") +endfunction(make_includable) + +make_includable(metadata_template.txt metadata_template.h) + +qt_internal_add_plugin(QCtfTracePlugin + CLASS_NAME QCtfTracePlugin + PLUGIN_TYPE tracing + SOURCES + qctflib_p.h qctflib.cpp metadata_template.txt qctfplugin.cpp qctfplugin_p.h + qctfserver_p.h qctfserver.cpp + LIBRARIES + Qt::Core Qt::CorePrivate Qt::Network +) + +qt_internal_extend_target(QCtfTracePlugin CONDITION QT_FEATURE_zstd + LIBRARIES + WrapZSTD::WrapZSTD +) + +qt_internal_extend_target(QCtfTracePlugin CONDITION (QT_FEATURE_cxx17_filesystem) AND (GCC AND (QMAKE_GCC_MAJOR_VERSION LESS 9)) + LINK_OPTIONS + "-lstdc++fs" +) diff --git a/src/plugins/tracing/metadata_template.txt b/src/plugins/tracing/metadata_template.txt new file mode 100644 index 0000000000..5f27e79da5 --- /dev/null +++ b/src/plugins/tracing/metadata_template.txt @@ -0,0 +1,77 @@ +/* CTF 1.8 */ + +typealias integer { size = 8; align = 8; signed = false; } := uint8_t; +typealias integer { size = 16; align = 8; signed = false; } := uint16_t; +typealias integer { size = 32; align = 8; signed = false; } := uint32_t; +typealias integer { size = 64; align = 8; signed = false; } := uint64_t; +typealias integer { size = 8; align = 8; signed = true; } := int8_t; +typealias integer { size = 16; align = 8; signed = true; } := int16_t; +typealias integer { size = 32; align = 8; signed = true; } := int32_t; +typealias integer { size = 64; align = 8; signed = true; } := int64_t; +typealias integer { size = 32; align = 8; signed = true; base = 16; } := intptr32_t; +typealias integer { size = 64; align = 8; signed = true; base = 16; } := intptr64_t; +typealias floating_point { exp_dig = 8; mant_dig = 24; align = 8; byte_order = native; } := float; +typealias floating_point { exp_dig = 11; mant_dig = 53; align = 8; byte_order = native; } := double; + +typealias enum : integer { size = 8; } { + false, + true +} := Boolean; + +trace { + major = 1; + minor = 8; + uuid = "$TRACE_UUID"; + byte_order = $ENDIANNESS; + packet.header := struct { + uint32_t magic; + uint8_t uuid[16]; + uint32_t stream_id; + } align(8); +}; + +env { + domain = "ust"; + tracer_name = "qtctf"; + tracer_major = 1; + tracer_minor = 0; + architecture_bit_width = $ARC_BIT_WIDTH; + trace_name = "$SESSION_NAME"; + trace_creation_datetime = "$CREATION_TIME"; + hostname = "$HOST_NAME"; +}; + +clock { + name = "$CLOCK_NAME"; + uuid = "53836526-5a62-4de0-93c8-3a1970afab23"; + description = "$CLOCK_TYPE"; + freq = $CLOCK_FREQUENCY; + offset = $CLOCK_OFFSET; +}; + +typealias integer { + size = 64; align = 8; signed = false; + map = clock.monotonic.value; +} := uint64_clock_monotonic_t; + +struct packet_context { + uint64_clock_monotonic_t timestamp_begin; + uint64_clock_monotonic_t timestamp_end; + uint64_t content_size; + uint64_t packet_size; + uint64_t packet_seq_num; + uint64_t events_discarded; + uint32_t thread_id; + string thread_name; +} align(8); + +struct event_header { + uint32_t id; + uint64_clock_monotonic_t timestamp; +} align(8); + +stream { + id = 0; + event.header := struct event_header; + packet.context := struct packet_context; +}; diff --git a/src/plugins/tracing/qctflib.cpp b/src/plugins/tracing/qctflib.cpp new file mode 100644 index 0000000000..fe3946d27c --- /dev/null +++ b/src/plugins/tracing/qctflib.cpp @@ -0,0 +1,447 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#define BUILD_LIBRARY +#include <qstring.h> +#include <qthread.h> +#include <stdio.h> +#include <qjsondocument.h> +#include <qjsonarray.h> +#include <qjsonobject.h> +#include <qfileinfo.h> +#include <qrect.h> +#include <qsize.h> +#include <qmetaobject.h> +#include <qendian.h> +#include <qplatformdefs.h> +#include "qctflib_p.h" + +#if QT_CONFIG(cxx17_filesystem) +#include <filesystem> +#endif + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(lcDebugTrace, "qt.core.ctf", QtWarningMsg) + +static const size_t packetHeaderSize = 24 + 6 * 8 + 4; +static const size_t packetSize = 4096; + +static const char traceMetadataTemplate[] = +#include "metadata_template.h" +; +static const size_t traceMetadataSize = sizeof(traceMetadataTemplate); + +static inline QString allLiteral() { return QStringLiteral("all"); } +static inline QString defaultLiteral() { return QStringLiteral("default"); } + + +template <typename T> +static QByteArray &operator<<(QByteArray &arr, T val) +{ + static_assert(std::is_arithmetic_v<T>); + arr.append(reinterpret_cast<char *>(&val), sizeof(val)); + return arr; +} + +static FILE *openFile(const QString &filename, const QString &mode) +{ +#ifdef Q_OS_WINDOWS + return _wfopen(qUtf16Printable(filename), qUtf16Printable(mode)); +#else + return fopen(qPrintable(filename), qPrintable(mode)); +#endif +} + +QCtfLibImpl *QCtfLibImpl::s_instance = nullptr; + +QCtfLib *QCtfLibImpl::instance() +{ + if (!s_instance) + s_instance = new QCtfLibImpl(); + return s_instance; +} + +void QCtfLibImpl::cleanup() +{ + delete s_instance; + s_instance = nullptr; +} + +void QCtfLibImpl::handleSessionChange() +{ + m_sessionChanged = true; +} + +void QCtfLibImpl::handleStatusChange(QCtfServer::ServerStatus status) +{ + switch (status) { + case QCtfServer::Error: { + m_serverClosed = true; + } break; + default: + break; + } +} + +void QCtfLibImpl::buildMetadata() +{ + const QString mhn = QSysInfo::machineHostName(); + QString metadata = QString::fromUtf8(traceMetadataTemplate, traceMetadataSize); + metadata.replace(QStringLiteral("$TRACE_UUID"), s_TraceUuid.toString(QUuid::WithoutBraces)); + metadata.replace(QStringLiteral("$ARC_BIT_WIDTH"), QString::number(Q_PROCESSOR_WORDSIZE * 8)); + metadata.replace(QStringLiteral("$SESSION_NAME"), m_session.name); + metadata.replace(QStringLiteral("$CREATION_TIME"), m_datetime.toString(Qt::ISODate)); + metadata.replace(QStringLiteral("$HOST_NAME"), mhn); + metadata.replace(QStringLiteral("$CLOCK_FREQUENCY"), QStringLiteral("1000000000")); + metadata.replace(QStringLiteral("$CLOCK_NAME"), QStringLiteral("monotonic")); + metadata.replace(QStringLiteral("$CLOCK_TYPE"), QStringLiteral("Monotonic clock")); + metadata.replace(QStringLiteral("$CLOCK_OFFSET"), QString::number(m_datetime.toMSecsSinceEpoch() * 1000000)); + metadata.replace(QStringLiteral("$ENDIANNESS"), QSysInfo::ByteOrder == QSysInfo::BigEndian ? u"be"_s : u"le"_s); + writeMetadata(metadata, true); +} + +QCtfLibImpl::QCtfLibImpl() +{ + QString location = qEnvironmentVariable("QTRACE_LOCATION"); + if (location.isEmpty()) { + qCInfo(lcDebugTrace) << "QTRACE_LOCATION not set"; + return; + } + + if (location.startsWith(u"tcp")) { + QUrl url(location); + m_server.reset(new QCtfServer()); + m_server->setCallback(this); + m_server->setHost(url.host()); + m_server->setPort(url.port()); + m_server->startServer(); + m_streaming = true; + m_session.tracepoints.append(allLiteral()); + m_session.name = defaultLiteral(); + } else { +#if !QT_CONFIG(cxx17_filesystem) + qCWarning(lcDebugTrace) << "Unable to use filesystem"; + return; +#endif + // Check if the location is writable + if (QT_ACCESS(qPrintable(location), W_OK) != 0) { + qCWarning(lcDebugTrace) << "Unable to write to location"; + return; + } + const QString filename = location + u"/session.json"; + FILE *file = openFile(qPrintable(filename), "rb"_L1); + if (!file) { + qCWarning(lcDebugTrace) << "unable to open session file: " + << filename << ", " << qt_error_string(); + m_location = location; + m_session.tracepoints.append(allLiteral()); + m_session.name = defaultLiteral(); + } else { + QT_STATBUF stat; + if (QT_FSTAT(QT_FILENO(file), &stat) != 0) { + qCWarning(lcDebugTrace) << "Unable to stat session file, " << qt_error_string(); + return; + } + qsizetype filesize = qMin(stat.st_size, std::numeric_limits<qsizetype>::max()); + QByteArray data(filesize, Qt::Uninitialized); + qsizetype size = static_cast<qsizetype>(fread(data.data(), 1, filesize, file)); + fclose(file); + if (size != filesize) + return; + QJsonDocument json(QJsonDocument::fromJson(data)); + QJsonObject obj = json.object(); + bool valid = false; + if (!obj.isEmpty()) { + const auto it = obj.begin(); + if (it.value().isArray()) { + m_session.name = it.key(); + for (auto var : it.value().toArray()) + m_session.tracepoints.append(var.toString()); + valid = true; + } + } + if (!valid) { + qCWarning(lcDebugTrace) << "Session file is not valid"; + m_session.tracepoints.append(allLiteral()); + m_session.name = defaultLiteral(); + } + m_location = location + u"/ust"; +#if QT_CONFIG(cxx17_filesystem) + std::filesystem::create_directory(qPrintable(m_location), qPrintable(location)); +#endif + } + clearLocation(); + } + + m_session.all = m_session.tracepoints.contains(allLiteral()); + // Get datetime to when the timer was started to store the offset to epoch time for the traces + m_datetime = QDateTime::currentDateTime().toUTC(); + m_timer.start(); + if (!m_streaming) + buildMetadata(); +} + +void QCtfLibImpl::clearLocation() +{ +#if QT_CONFIG(cxx17_filesystem) + const std::filesystem::path location{qUtf16Printable(m_location)}; + for (auto const& dirEntry : std::filesystem::directory_iterator{location}) + { + const auto path = dirEntry.path(); +#if __cplusplus > 201703L + if (dirEntry.is_regular_file() + && path.filename().wstring().starts_with(std::wstring_view(L"channel_")) + && !path.has_extension()) { +#else + const auto strview = std::wstring_view(L"channel_"); + const auto sub = path.filename().wstring().substr(0, strview.length()); + if (dirEntry.is_regular_file() && sub.compare(strview) == 0 + && !path.has_extension()) { +#endif + if (!std::filesystem::remove(path)) { + qCInfo(lcDebugTrace) << "Unable to clear output location."; + break; + } + } + } +#endif +} + +void QCtfLibImpl::writeMetadata(const QString &metadata, bool overwrite) +{ + if (m_streaming) { + auto mt = metadata.toUtf8(); + mt.resize(mt.size() - 1); + m_server->bufferData(QStringLiteral("metadata"), mt, overwrite); + } else { + FILE *file = nullptr; + file = openFile(qPrintable(m_location + "/metadata"_L1), overwrite ? "w+b"_L1: "ab"_L1); + if (!file) + return; + + if (!overwrite) + fputs("\n", file); + + // data contains zero at the end, hence size - 1. + const QByteArray data = metadata.toUtf8(); + fwrite(data.data(), data.size() - 1, 1, file); + fclose(file); + } +} + +void QCtfLibImpl::writeCtfPacket(QCtfLibImpl::Channel &ch) +{ + FILE *file = nullptr; + if (!m_streaming) + file = openFile(ch.channelName, "ab"_L1); + if (file || m_streaming) { + /* Each packet contains header and context, which are defined in the metadata.txt */ + QByteArray packet; + packet << s_CtfHeaderMagic; + packet.append(QByteArrayView(s_TraceUuid.toBytes())); + + packet << quint32(0); + packet << ch.minTimestamp; + packet << ch.maxTimestamp; + packet << quint64(ch.data.size() + packetHeaderSize + ch.threadNameLength) * 8u; + packet << quint64(packetSize) * 8u; + packet << ch.seqnumber++; + packet << quint64(0); + packet << ch.threadIndex; + if (ch.threadName.size()) + packet.append(ch.threadName); + packet << (char)0; + + Q_ASSERT(ch.data.size() + packetHeaderSize + ch.threadNameLength <= packetSize); + Q_ASSERT(packet.size() == qsizetype(packetHeaderSize + ch.threadNameLength)); + if (m_streaming) { + ch.data.resize(packetSize - packet.size(), 0); + packet += ch.data; + m_server->bufferData(QString::fromLatin1(ch.channelName), packet, false); + } else { + fwrite(packet.data(), packet.size(), 1, file); + ch.data.resize(packetSize - packet.size(), 0); + fwrite(ch.data.data(), ch.data.size(), 1, file); + fclose(file); + } + } +} + +QCtfLibImpl::Channel::~Channel() +{ + impl->writeCtfPacket(*this); + impl->removeChannel(this); +} + +QCtfLibImpl::~QCtfLibImpl() +{ + if (!m_server.isNull()) + m_server->stopServer(); + qDeleteAll(m_eventPrivs); +} + +void QCtfLibImpl::removeChannel(Channel *ch) +{ + const QMutexLocker lock(&m_mutex); + m_channels.removeOne(ch); +} + +bool QCtfLibImpl::tracepointEnabled(const QCtfTracePointEvent &point) +{ + if (m_sessionChanged) { + const QMutexLocker lock(&m_mutex); + buildMetadata(); + m_session.name = m_server->sessionName(); + m_session.tracepoints = m_server->sessionTracepoints().split(';'); + m_session.all = m_session.tracepoints.contains(allLiteral()); + m_sessionChanged = false; + for (const auto &meta : m_additionalMetadata) + writeMetadata(meta->metadata); + for (auto *priv : m_eventPrivs) + writeMetadata(priv->metadata); + quint64 timestamp = m_timer.nsecsElapsed(); + for (auto *ch : m_channels) { + writeCtfPacket(*ch); + ch->data.clear(); + ch->minTimestamp = ch->maxTimestamp = timestamp; + } + } + if (m_streaming && (m_serverClosed || (!m_server->bufferOnIdle() && m_server->status() == QCtfServer::Idle))) + return false; + return m_session.all || m_session.tracepoints.contains(point.provider.provider); +} + +static QString toMetadata(const QString &provider, const QString &name, const QString &metadata, quint32 eventId) +{ +/* + generates event structure: +event { + name = provider:tracepoint_name; + id = eventId; + stream_id = 0; + loglevel = 13; + fields := struct { + metadata + }; +}; +*/ + return QStringView(u"event {\n name = \"") + provider + QLatin1Char(':') + name + u"\";\n" + + u" id = " + QString::number(eventId) + u";\n" + + u" stream_id = 0;\n loglevel = 13;\n fields := struct {\n " + + metadata + u"\n };\n};\n"; +} + +QCtfTracePointPrivate *QCtfLibImpl::initializeTracepoint(const QCtfTracePointEvent &point) +{ + QMutexLocker lock(&m_mutex); + QCtfTracePointPrivate *priv = point.d; + if (!point.d) { + if (const auto &it = m_eventPrivs.find(point.eventName); it != m_eventPrivs.end()) { + priv = *it; + } else { + priv = new QCtfTracePointPrivate(); + m_eventPrivs.insert(point.eventName, priv); + priv->id = eventId(); + priv->metadata = toMetadata(point.provider.provider, point.eventName, point.metadata, priv->id); + } + } + return priv; +} + +void QCtfLibImpl::doTracepoint(const QCtfTracePointEvent &point, const QByteArray &arr) +{ + QCtfTracePointPrivate *priv = point.d; + quint64 timestamp = 0; + QThread *thread = nullptr; + if (m_streaming && m_serverClosed) + return; + { + QMutexLocker lock(&m_mutex); + if (!priv->metadataWritten) { + priv->metadataWritten = true; + auto providerMetadata = point.provider.metadata; + while (providerMetadata) { + registerMetadata(*providerMetadata); + providerMetadata = providerMetadata->next; + } + if (m_newAdditionalMetadata.size()) { + for (const QString &name : m_newAdditionalMetadata) + writeMetadata(m_additionalMetadata[name]->metadata); + m_newAdditionalMetadata.clear(); + } + writeMetadata(priv->metadata); + } + timestamp = m_timer.nsecsElapsed(); + } + if (arr.size() != point.size) { + if (arr.size() < point.size) + return; + if (arr.size() > point.size && !point.variableSize && !point.metadata.isEmpty()) + return; + } + + thread = QThread::currentThread(); + if (thread == nullptr) + return; + + Channel &ch = m_threadData.localData(); + + if (ch.channelName[0] == 0) { + ch.impl = this; + m_channels.append(&ch); + m_threadIndices.insert(thread, m_threadIndices.size()); + sprintf(ch.channelName, "%s/channel_%d", qPrintable(m_location), m_threadIndices[thread]); + ch.minTimestamp = ch.maxTimestamp = timestamp; + ch.thread = thread; + ch.threadIndex = m_threadIndices[thread]; + ch.threadName = thread->objectName().toUtf8(); + if (ch.threadName.isEmpty()) { + const QMetaObject *obj = thread->metaObject(); + ch.threadName = QByteArray(obj->className()); + } + ch.threadNameLength = ch.threadName.size() + 1; + } + if (ch.locked) + return; + Q_ASSERT(ch.thread == thread); + ch.locked = true; + + QByteArray event; + event << priv->id << timestamp; + if (!point.metadata.isEmpty()) + event.append(arr); + + if (ch.threadNameLength + ch.data.size() + event.size() + packetHeaderSize >= packetSize) { + writeCtfPacket(ch); + ch.data = event; + ch.minTimestamp = ch.maxTimestamp = timestamp; + } else { + ch.data.append(event); + } + + ch.locked = false; + ch.maxTimestamp = timestamp; +} + +bool QCtfLibImpl::sessionEnabled() +{ + return !m_session.name.isEmpty(); +} + +int QCtfLibImpl::eventId() +{ + return m_eventId++; +} + +void QCtfLibImpl::registerMetadata(const QCtfTraceMetadata &metadata) +{ + if (m_additionalMetadata.contains(metadata.name)) + return; + + m_additionalMetadata.insert(metadata.name, &metadata); + m_newAdditionalMetadata.insert(metadata.name); +} + +QT_END_NAMESPACE diff --git a/src/plugins/tracing/qctflib_p.h b/src/plugins/tracing/qctflib_p.h new file mode 100644 index 0000000000..297d38ee50 --- /dev/null +++ b/src/plugins/tracing/qctflib_p.h @@ -0,0 +1,125 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QT_CTFLIB_H +#define QT_CTFLIB_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// + +#include <private/qctf_p.h> +#include "qctfplugin_p.h" +#include <qstring.h> +#include <qmutex.h> +#include <qelapsedtimer.h> +#include <qhash.h> +#include <qset.h> +#include <qthreadstorage.h> +#include <qthread.h> +#include <qloggingcategory.h> +#include "qctfserver_p.h" + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcDebugTrace) + +struct QCtfTracePointPrivate +{ + QString metadata; + quint32 id = 0; + quint32 payloadSize = 0; + bool metadataWritten = false; +}; + +class QCtfLibImpl : public QCtfLib, public QCtfServer::ServerCallback +{ + struct Session + { + QString name; + QStringList tracepoints; + bool all = false; + }; + struct Channel + { + char channelName[512]; + QByteArray data; + quint64 minTimestamp = 0; + quint64 maxTimestamp = 0; + quint64 seqnumber = 0; + QThread *thread = nullptr; + quint32 threadIndex = 0; + QByteArray threadName; + quint32 threadNameLength = 0; + bool locked = false; + QCtfLibImpl *impl = nullptr; + Channel() + { + memset(channelName, 0, sizeof(channelName)); + } + + ~Channel(); + }; + +public: + QCtfLibImpl(); + ~QCtfLibImpl(); + + bool tracepointEnabled(const QCtfTracePointEvent &point) override; + void doTracepoint(const QCtfTracePointEvent &point, const QByteArray &arr) override; + bool sessionEnabled() override; + QCtfTracePointPrivate *initializeTracepoint(const QCtfTracePointEvent &point) override; + void registerMetadata(const QCtfTraceMetadata &metadata); + int eventId(); + void shutdown(bool *) override + { + + } + + static QCtfLib *instance(); + static void cleanup(); +private: + static QCtfLibImpl *s_instance; + QHash<QString, QCtfTracePointPrivate *> m_eventPrivs; + void removeChannel(Channel *ch); + void updateMetadata(const QCtfTracePointEvent &point); + void writeMetadata(const QString &metadata, bool overwrite = false); + void clearLocation(); + void handleSessionChange() override; + void handleStatusChange(QCtfServer::ServerStatus status) override; + void writeCtfPacket(Channel &ch); + void buildMetadata(); + + static constexpr QUuid s_TraceUuid = QUuid(0x3e589c95, 0xed11, 0xc159, 0x42, 0x02, 0x6a, 0x9b, 0x02, 0x00, 0x12, 0xac); + static constexpr quint32 s_CtfHeaderMagic = 0xC1FC1FC1; + + QMutex m_mutex; + QElapsedTimer m_timer; + QString m_metadata; + QString m_location; + Session m_session; + QHash<QThread*, quint32> m_threadIndices; + QThreadStorage<Channel> m_threadData; + QList<Channel *> m_channels; + QHash<QString, const QCtfTraceMetadata *> m_additionalMetadata; + QSet<QString> m_newAdditionalMetadata; + QDateTime m_datetime; + int m_eventId = 0; + bool m_streaming = false; + std::atomic_bool m_sessionChanged = false; + std::atomic_bool m_serverClosed = false; + QScopedPointer<QCtfServer> m_server; + friend struct Channel; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/tracing/qctfplugin.cpp b/src/plugins/tracing/qctfplugin.cpp new file mode 100644 index 0000000000..93e508e199 --- /dev/null +++ b/src/plugins/tracing/qctfplugin.cpp @@ -0,0 +1,63 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#define BUILD_LIBRARY +#include <qstring.h> +#include "qctfplugin_p.h" +#include "qctflib_p.h" + +QT_BEGIN_NAMESPACE + +class QCtfTracePlugin : public QCtfLib +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QCtfLib" FILE "trace.json") + Q_INTERFACES(QCtfLib) + +public: + QCtfTracePlugin() + { + + } + ~QCtfTracePlugin() + { + m_cleanup = true; + *m_shutdown = true; + QCtfLibImpl::cleanup(); + } + void shutdown(bool *shutdown) override + { + m_shutdown = shutdown; + } + bool tracepointEnabled(const QCtfTracePointEvent &point) override + { + if (m_cleanup) + return false; + return QCtfLibImpl::instance()->tracepointEnabled(point); + } + void doTracepoint(const QCtfTracePointEvent &point, const QByteArray &arr) override + { + if (m_cleanup) + return; + QCtfLibImpl::instance()->doTracepoint(point, arr); + } + bool sessionEnabled() override + { + if (m_cleanup) + return false; + return QCtfLibImpl::instance()->sessionEnabled(); + } + QCtfTracePointPrivate *initializeTracepoint(const QCtfTracePointEvent &point) override + { + if (m_cleanup) + return nullptr; + return QCtfLibImpl::instance()->initializeTracepoint(point); + } +private: + bool m_cleanup = false; + bool *m_shutdown = nullptr; +}; + +QT_END_NAMESPACE + +#include "qctfplugin.moc" diff --git a/src/plugins/tracing/qctfplugin_p.h b/src/plugins/tracing/qctfplugin_p.h new file mode 100644 index 0000000000..987c4d925f --- /dev/null +++ b/src/plugins/tracing/qctfplugin_p.h @@ -0,0 +1,28 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef Q_CTFPLUGIN_P_H +#define Q_CTFPLUGIN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// + +#include <private/qctf_p.h> +#include <qplugin.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_INTERFACE(QCtfLib, "org.qt-project.Qt.QCtfLib") + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/tracing/qctfserver.cpp b/src/plugins/tracing/qctfserver.cpp new file mode 100644 index 0000000000..d97c345e11 --- /dev/null +++ b/src/plugins/tracing/qctfserver.cpp @@ -0,0 +1,404 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qloggingcategory.h> +#include "qctfserver_p.h" + +#if QT_CONFIG(zstd) +#include <zstd.h> +#endif + +using namespace Qt::Literals::StringLiterals; + +Q_LOGGING_CATEGORY(lcCtfInfoTrace, "qt.core.ctfserver", QtWarningMsg) + +#if QT_CONFIG(zstd) +static QByteArray zstdCompress(ZSTD_CCtx *&context, const QByteArray &data, int compression) +{ + if (context == nullptr) + context = ZSTD_createCCtx(); + qsizetype size = data.size(); + size = ZSTD_COMPRESSBOUND(size); + QByteArray compressed(size, Qt::Uninitialized); + char *dst = compressed.data(); + size_t n = ZSTD_compressCCtx(context, dst, size, + data.constData(), data.size(), + compression); + if (ZSTD_isError(n)) { + qCWarning(lcCtfInfoTrace) << "Compression with zstd failed: " << QString::fromUtf8(ZSTD_getErrorName(n)); + return {}; + } + compressed.truncate(n); + return compressed; +} +#endif + +QCtfServer::QCtfServer(QObject *parent) + : QThread(parent) +{ + m_keySet << "cliendId"_L1 + << "clientVersion"_L1 + << "sessionName"_L1 + << "sessionTracepoints"_L1 + << "flags"_L1 + << "bufferSize"_L1 + << "compressionScheme"_L1; +} + +QCtfServer::~QCtfServer() +{ +#if QT_CONFIG(zstd) + ZSTD_freeCCtx(m_zstdCCtx); +#endif +} + +void QCtfServer::setHost(const QString &address) +{ + m_address = address; +} + +void QCtfServer::setPort(int port) +{ + m_port = port; +} + +void QCtfServer::setCallback(ServerCallback *cb) +{ + m_cb = cb; +} + +QString QCtfServer::sessionName() const +{ + return m_req.sessionName; +} + +QString QCtfServer::sessionTracepoints() const +{ + return m_req.sessionTracepoints; +} + +bool QCtfServer::bufferOnIdle() const +{ + return m_bufferOnIdle; +} + +QCtfServer::ServerStatus QCtfServer::status() const +{ + return m_status; +} + +void QCtfServer::setStatusAndNotify(ServerStatus status) +{ + m_status = status; + m_cb->handleStatusChange(status); +} + +void QCtfServer::bytesWritten(qint64 size) +{ + m_writtenSize += size; + if (m_writtenSize >= m_waitWriteSize && m_eventLoop) + m_eventLoop->exit(); +} + +void QCtfServer::initWrite() +{ + m_waitWriteSize = 0; + m_writtenSize = 0; +} + +bool QCtfServer::waitSocket() +{ + if (m_eventLoop) + m_eventLoop->exec(); + return m_socket->state() == QTcpSocket::ConnectedState; +} + +void QCtfServer::handleString(QCborStreamReader &cbor) +{ + const auto readString = [](QCborStreamReader &cbor) -> QString { + QString result; + auto r = cbor.readString(); + while (r.status == QCborStreamReader::Ok) { + result += r.data; + r = cbor.readString(); + } + + if (r.status == QCborStreamReader::Error) { + // handle error condition + result.clear(); + } + return result; + }; + do { + if (m_currentKey.isEmpty()) { + m_currentKey = readString(cbor); + } else { + switch (m_keySet.indexOf(m_currentKey)) { + case RequestSessionName: + m_req.sessionName = readString(cbor); + break; + case RequestSessionTracepoints: + m_req.sessionTracepoints = readString(cbor); + break; + case RequestCompressionScheme: + m_requestedCompressionScheme = readString(cbor); + break; + default: + // handle error + break; + } + m_currentKey.clear(); + } + if (cbor.lastError() == QCborError::EndOfFile) { + if (!waitSocket()) + return; + cbor.reparse(); + } + } while (cbor.lastError() == QCborError::EndOfFile); +} + +void QCtfServer::handleFixedWidth(QCborStreamReader &cbor) +{ + switch (m_keySet.indexOf(m_currentKey)) { + case RequestClientId: + if (!cbor.isUnsignedInteger()) + return; + m_req.clientId = cbor.toUnsignedInteger(); + break; + case RequestClientVersion: + if (!cbor.isUnsignedInteger()) + return; + m_req.clientVersion = cbor.toUnsignedInteger(); + break; + case RequestFlags: + if (!cbor.isUnsignedInteger()) + return; + m_req.flags = cbor.toUnsignedInteger(); + break; + case RequestBufferSize: + if (!cbor.isUnsignedInteger()) + return; + m_req.bufferSize = cbor.toUnsignedInteger(); + break; + default: + // handle error + break; + } + m_currentKey.clear(); +} + +void QCtfServer::readCbor(QCborStreamReader &cbor) +{ + switch (cbor.type()) { + case QCborStreamReader::UnsignedInteger: + case QCborStreamReader::NegativeInteger: + case QCborStreamReader::SimpleType: + case QCborStreamReader::Float16: + case QCborStreamReader::Float: + case QCborStreamReader::Double: + handleFixedWidth(cbor); + cbor.next(); + break; + case QCborStreamReader::ByteArray: + case QCborStreamReader::String: + handleString(cbor); + break; + case QCborStreamReader::Array: + case QCborStreamReader::Map: + cbor.enterContainer(); + while (cbor.lastError() == QCborError::NoError && cbor.hasNext()) + readCbor(cbor); + if (cbor.lastError() == QCborError::NoError) + cbor.leaveContainer(); + default: + break; + } +} + +void QCtfServer::writePacket(TracePacket &packet, QCborStreamWriter &cbor) +{ + cbor.startMap(4); + cbor.append("magic"_L1); + cbor.append(packet.PacketMagicNumber); + cbor.append("name"_L1); + cbor.append(QString::fromUtf8(packet.stream_name)); + cbor.append("flags"_L1); + cbor.append(packet.flags); + + cbor.append("data"_L1); + if (m_compression > 0) { + QByteArray compressed; +#if QT_CONFIG(zstd) + if (m_requestedCompressionScheme == QStringLiteral("zstd")) + compressed = zstdCompress(m_zstdCCtx, packet.stream_data, m_compression); + else +#endif + compressed = qCompress(packet.stream_data, m_compression); + + cbor.append(compressed); + } else { + cbor.append(packet.stream_data); + } + + cbor.endMap(); +} + +bool QCtfServer::recognizedCompressionScheme() const +{ + if (m_requestedCompressionScheme.isEmpty()) + return true; +#if QT_CONFIG(zstd) + if (m_requestedCompressionScheme == QStringLiteral("zstd")) + return true; +#endif + if (m_requestedCompressionScheme == QStringLiteral("zlib")) + return true; + return false; +} + +void QCtfServer::run() +{ + m_server = new QTcpServer(); + QHostAddress addr; + if (m_address.isEmpty()) + addr = QHostAddress(QHostAddress::Any); + else + addr = QHostAddress(m_address); + + qCInfo(lcCtfInfoTrace) << "Starting CTF server: " << m_address << ", port: " << m_port; + + while (m_stopping == 0) { + if (!m_server->isListening()) { + if (!m_server->listen(addr, m_port)) { + qCInfo(lcCtfInfoTrace) << "Unable to start server"; + m_stopping = 1; + setStatusAndNotify(Error); + } + } + setStatusAndNotify(Idle); + if (m_server->waitForNewConnection(-1)) { + qCInfo(lcCtfInfoTrace) << "client connection"; + m_eventLoop = new QEventLoop(); + m_socket = m_server->nextPendingConnection(); + + QObject::connect(m_socket, &QTcpSocket::readyRead, [&](){ + if (m_eventLoop) m_eventLoop->exit(); + }); + QObject::connect(m_socket, &QTcpSocket::bytesWritten, this, &QCtfServer::bytesWritten); + QObject::connect(m_socket, &QTcpSocket::disconnected, [&](){ + if (m_eventLoop) m_eventLoop->exit(); + }); + + m_server->close(); // Do not wait for more connections + setStatusAndNotify(Connected); + + if (waitSocket()) + { + QCborStreamReader cbor(m_socket); + + m_req = {}; + while (cbor.hasNext() && cbor.lastError() == QCborError::NoError) + readCbor(cbor); + + if (!m_req.isValid()) { + qCInfo(lcCtfInfoTrace) << "Invalid trace request."; + m_socket->close(); + } else { + m_compression = m_req.flags & CompressionMask; +#if QT_CONFIG(zstd) + m_compression = qMin(m_compression, ZSTD_maxCLevel()); +#else + m_compression = qMin(m_compression, 9); +#endif + m_bufferOnIdle = !(m_req.flags & DontBufferOnIdle); + + m_maxPackets = qMax(m_req.bufferSize / TracePacket::PacketSize, 16u); + + if (!recognizedCompressionScheme()) { + qCWarning(lcCtfInfoTrace) << "Client requested unrecognized compression scheme: " << m_requestedCompressionScheme; + m_requestedCompressionScheme.clear(); + m_compression = 0; + } + + qCInfo(lcCtfInfoTrace) << "request received: " << m_req.sessionName << ", " << m_req.sessionTracepoints; + + m_cb->handleSessionChange(); + { + TraceResponse resp; + resp.serverId = ServerId; + resp.serverVersion = 1; + resp.serverName = QStringLiteral("Ctf Server"); + + QCborStreamWriter cbor(m_socket); + cbor.startMap(m_compression ? 4 : 3); + cbor.append("serverId"_L1); + cbor.append(resp.serverId); + cbor.append("serverVersion"_L1); + cbor.append(resp.serverVersion); + cbor.append("serverName"_L1); + cbor.append(resp.serverName); + if (m_compression) { + cbor.append("compressionScheme"_L1); + cbor.append(m_requestedCompressionScheme); + } + cbor.endMap(); + } + + qCInfo(lcCtfInfoTrace) << "response sent, sending data"; + if (waitSocket()) { + while (m_socket->state() == QTcpSocket::ConnectedState) { + QList<TracePacket> packets; + { + QMutexLocker lock(&m_mutex); + while (m_packets.size() == 0) + m_bufferHasData.wait(&m_mutex); + packets = std::exchange(m_packets, {}); + } + + { + QCborStreamWriter cbor(m_socket); + for (TracePacket &packet : packets) { + writePacket(packet, cbor); + if (!waitSocket()) + break; + } + } + qCInfo(lcCtfInfoTrace) << packets.size() << " packets written"; + } + } + + qCInfo(lcCtfInfoTrace) << "client connection closed"; + } + } + delete m_eventLoop; + m_eventLoop = nullptr; + } else { + qCInfo(lcCtfInfoTrace) << "error: " << m_server->errorString(); + m_stopping = 1; + setStatusAndNotify(Error); + } + } +} + +void QCtfServer::startServer() +{ + start(); +} +void QCtfServer::stopServer() +{ + this->m_stopping = 1; + wait(); +} + +void QCtfServer::bufferData(const QString &stream, const QByteArray &data, quint32 flags) +{ + QMutexLocker lock(&m_mutex); + TracePacket packet; + packet.stream_name = stream.toUtf8(); + packet.stream_data = data; + packet.flags = flags; + m_packets.append(packet); + if (m_packets.size() > m_maxPackets) + m_packets.pop_front(); + m_bufferHasData.wakeOne(); +} diff --git a/src/plugins/tracing/qctfserver_p.h b/src/plugins/tracing/qctfserver_p.h new file mode 100644 index 0000000000..3660df7f09 --- /dev/null +++ b/src/plugins/tracing/qctfserver_p.h @@ -0,0 +1,194 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QT_CTFSERVER_H +#define QT_CTFSERVER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// + +#include <qbytearray.h> +#include <qdatastream.h> +#include <qthread.h> +#include <qmutex.h> +#include <qwaitcondition.h> +#include <qeventloop.h> +#include <QtNetwork/qtcpserver.h> +#include <QtNetwork/qtcpsocket.h> +#include <qcborstreamreader.h> +#include <qcborstreamwriter.h> +#include <qlist.h> + +typedef struct ZSTD_CCtx_s ZSTD_CCtx; + +QT_BEGIN_NAMESPACE + +class QCtfServer; +struct TracePacket +{ + static constexpr quint32 PacketMagicNumber = 0x100924da; + static constexpr quint32 PacketSize = 4096 + 9; + QByteArray stream_name; + QByteArray stream_data; + quint32 flags = 0; + + TracePacket() = default; + + TracePacket(const TracePacket &t) + { + stream_name = t.stream_name; + stream_data = t.stream_data; + flags = t.flags; + } + TracePacket &operator = (const TracePacket &t) + { + stream_name = t.stream_name; + stream_data = t.stream_data; + flags = t.flags; + return *this; + } + TracePacket(TracePacket &&t) + { + stream_name = std::move(t.stream_name); + stream_data = std::move(t.stream_data); + flags = t.flags; + } + TracePacket &operator = (TracePacket &&t) + { + stream_name = std::move(t.stream_name); + stream_data = std::move(t.stream_data); + flags = t.flags; + return *this; + } +}; + +auto constexpr operator""_MB(quint64 s) -> quint64 +{ + return s * 1024ul * 1024ul; +} + +struct TraceRequest +{ + quint32 clientId; + quint32 clientVersion; + quint32 flags; + quint32 bufferSize; + QString sessionName; + QString sessionTracepoints; + + static constexpr quint32 MaxBufferSize = 1024_MB; + + bool isValid() const + { + if (clientId != 0 && clientVersion != 0 && !sessionName.isEmpty() + && !sessionTracepoints.isEmpty() && bufferSize < MaxBufferSize) + return true; + return false; + } +}; + +struct TraceResponse +{ + quint32 serverId; + quint32 serverVersion; + QString serverName; +}; + +class QCtfServer : public QThread +{ + Q_OBJECT +public: + enum ServerStatus + { + Uninitialized, + Idle, + Connected, + Error, + }; + enum ServerFlags + { + CompressionMask = 255, + DontBufferOnIdle = 256, // not set -> the server is buffering even without client connection + // set -> the server is buffering only when client is connected + }; + enum RequestIds + { + RequestClientId = 0, + RequestClientVersion, + RequestSessionName, + RequestSessionTracepoints, + RequestFlags, + RequestBufferSize, + RequestCompressionScheme, + }; + + struct ServerCallback + { + virtual void handleSessionChange() = 0; + virtual void handleStatusChange(ServerStatus status) = 0; + }; + QCtfServer(QObject *parent = nullptr); + ~QCtfServer(); + void setCallback(ServerCallback *cb); + void setHost(const QString &address); + void setPort(int port); + void run() override; + void startServer(); + void stopServer(); + void bufferData(const QString &stream, const QByteArray &data, quint32 flags); + QString sessionName() const; + QString sessionTracepoints() const; + bool bufferOnIdle() const; + ServerStatus status() const; +private: + + void initWrite(); + void bytesWritten(qint64 size); + bool waitSocket(); + void readCbor(QCborStreamReader &cbor); + void handleString(QCborStreamReader &cbor); + void handleFixedWidth(QCborStreamReader &cbor); + bool recognizedCompressionScheme() const; + void setStatusAndNotify(ServerStatus status); + void writePacket(TracePacket &packet, QCborStreamWriter &cbor); + + QMutex m_mutex; + QWaitCondition m_bufferHasData; + QList<TracePacket> m_packets; + QString m_address; + QTcpServer *m_server = nullptr; + QTcpSocket *m_socket = nullptr; + QEventLoop *m_eventLoop = nullptr; + QList<QString> m_keySet; + TraceRequest m_req; + ServerCallback *m_cb = nullptr; + ServerStatus m_status = Uninitialized; + qint64 m_waitWriteSize = 0; + qint64 m_writtenSize = 0; + int m_port; + int m_compression = 0; + int m_maxPackets = DefaultMaxPackets; + QAtomicInt m_stopping; + bool m_bufferOnIdle = true; + QString m_currentKey; + QString m_requestedCompressionScheme; +#if QT_CONFIG(zstd) + ZSTD_CCtx *m_zstdCCtx = nullptr; +#endif + + static constexpr quint32 ServerId = 1; + static constexpr quint32 DefaultMaxPackets = 256; // 1 MB +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/plugins/tracing/trace.json b/src/plugins/tracing/trace.json new file mode 100644 index 0000000000..1b991122d4 --- /dev/null +++ b/src/plugins/tracing/trace.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "CTF" ] +} |