diff options
Diffstat (limited to 'src/plugins')
435 files changed, 15827 insertions, 7107 deletions
diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index dc7c45c2d8..d26d2aae44 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -25,6 +25,6 @@ if (TARGET Qt::Network) add_subdirectory(networkinformation) add_subdirectory(tls) endif() -if (QT_FEATURE_ctf AND QT_FEATURE_library) +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 313fafc15c..6d3cf2a925 100644 --- a/src/plugins/generic/CMakeLists.txt +++ b/src/plugins/generic/CMakeLists.txt @@ -19,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/tuiotouch/qtuiohandler.cpp b/src/plugins/generic/tuiotouch/qtuiohandler.cpp index a304963669..2815368e84 100644 --- a/src/plugins/generic/tuiotouch/qtuiohandler.cpp +++ b/src/plugins/generic/tuiotouch/qtuiohandler.cpp @@ -59,6 +59,7 @@ QTuioHandler::QTuioHandler(const QString &specification) case 180: case 270: rotationAngle = argValue; + break; default: break; } diff --git a/src/plugins/imageformats/ico/qicohandler.cpp b/src/plugins/imageformats/ico/qicohandler.cpp index 3bcf30bfc6..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); @@ -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/qjpeghandler.cpp b/src/plugins/imageformats/jpeg/qjpeghandler.cpp index 0d72ba01d0..59b73587c5 100644 --- a/src/plugins/imageformats/jpeg/qjpeghandler.cpp +++ b/src/plugins/imageformats/jpeg/qjpeghandler.cpp @@ -172,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; @@ -192,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 } @@ -206,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. @@ -348,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. @@ -493,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(); @@ -538,6 +550,10 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, 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; @@ -570,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]; @@ -654,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) @@ -689,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). @@ -700,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; @@ -745,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; @@ -759,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]; @@ -975,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)); @@ -1061,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 @@ -1078,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 @@ -1101,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; @@ -1136,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; @@ -1146,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/android/CMakeLists.txt b/src/plugins/networkinformation/android/CMakeLists.txt index f9ed4913e3..07d9201bbb 100644 --- a/src/plugins/networkinformation/android/CMakeLists.txt +++ b/src/plugins/networkinformation/android/CMakeLists.txt @@ -29,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..68a9381ad2 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 } sourceSets { diff --git a/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp b/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp index 203772971b..3c9f952968 100644 --- a/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp +++ b/src/plugins/networkinformation/android/wrapper/androidconnectivitymanager.cpp @@ -48,7 +48,7 @@ static void transportMediumChanged(JNIEnv *env, jobject obj, jint enumValue) } Q_DECLARE_JNI_NATIVE_METHOD(transportMediumChanged) -Q_DECLARE_JNI_TYPE(ConnectivityManager, "Landroid/net/ConnectivityManager;") +Q_DECLARE_JNI_CLASS(ConnectivityManager, "android/net/ConnectivityManager") AndroidConnectivityManager::AndroidConnectivityManager() { @@ -93,3 +93,5 @@ bool AndroidConnectivityManager::registerNatives() const } QT_END_NAMESPACE + +#include "moc_androidconnectivitymanager.cpp" 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 77501d0e5c..acd3754f4e 100644 --- a/src/plugins/networkinformation/networklistmanager/CMakeLists.txt +++ b/src/plugins/networkinformation/networklistmanager/CMakeLists.txt @@ -14,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 5b0a91df3e..caa5046751 100644 --- a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.cpp +++ b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagerevents.cpp @@ -102,16 +102,22 @@ bool QNetworkListManagerEvents::start() #if QT_CONFIG(cpp_winrt) using namespace winrt::Windows::Networking::Connectivity; using winrt::Windows::Foundation::IInspectable; - // 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(); - } - }); + 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 @@ -197,7 +203,9 @@ QNetworkInformation::TransportMedium getTransportMedium(const ConnectionProfile NetworkAdapter adapter(nullptr); try { adapter = profile.NetworkAdapter(); - } catch (...) { + } 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) @@ -225,7 +233,9 @@ QNetworkInformation::TransportMedium getTransportMedium(const ConnectionProfile ConnectionCost cost(nullptr); try { cost = profile.GetConnectionCost(); - } catch (...) { + } 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) @@ -241,7 +251,9 @@ void QNetworkListManagerEvents::emitWinRTUpdates() ConnectionProfile profile = nullptr; try { profile = NetworkInformation::GetInternetConnectionProfile(); - } catch (...) { + } 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) @@ -252,3 +264,5 @@ void QNetworkListManagerEvents::emitWinRTUpdates() #endif // QT_CONFIG(cpp_winrt) QT_END_NAMESPACE + +#include "moc_qnetworklistmanagerevents.cpp" diff --git a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp index a5b0179e37..766648486e 100644 --- a/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp +++ b/src/plugins/networkinformation/networklistmanager/qnetworklistmanagernetworkinformationbackend.cpp @@ -72,6 +72,7 @@ public: | QNetworkInformation::Feature::CaptivePortal #if QT_CONFIG(cpp_winrt) | QNetworkInformation::Feature::TransportMedium + | QNetworkInformation::Feature::Metered #endif ); } diff --git a/src/plugins/networkinformation/networkmanager/CMakeLists.txt b/src/plugins/networkinformation/networkmanager/CMakeLists.txt index e98ea84e3a..9d76dbe7b4 100644 --- a/src/plugins/networkinformation/networkmanager/CMakeLists.txt +++ b/src/plugins/networkinformation/networkmanager/CMakeLists.txt @@ -7,6 +7,7 @@ qt_internal_add_plugin(QNetworkManagerNetworkInformationPlugin 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 6ee84e06f8..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> @@ -100,39 +98,14 @@ bool isMeteredFromNMMetered(QNetworkManagerInterface::NMMetered metered) 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 { @@ -167,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 9ca862d398..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,6 +154,8 @@ private: std::optional<QDBusObjectPath> primaryConnectionDevicePath() const; QVariantMap propertyMap; + QNetworkManagerNetworkInformationBackend *backend = nullptr; + bool validDBusConnection = true; }; class PropertiesDBusInterface : public QDBusAbstractInterface 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 30c326d06f..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"/> @@ -62,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 a0036db31e..d74be4bedf 100644 --- a/src/plugins/platforminputcontexts/ibus/main.cpp +++ b/src/plugins/platforminputcontexts/ibus/main.cpp @@ -28,6 +28,8 @@ QIBusPlatformInputContext *QIbusPlatformInputContextPlugin::create(const QString qDBusRegisterMetaType<QIBusAttribute>(); qDBusRegisterMetaType<QIBusAttributeList>(); qDBusRegisterMetaType<QIBusText>(); + qDBusRegisterMetaType<QIBusPropTypeClientCommitPreedit>(); + qDBusRegisterMetaType<QIBusPropTypeContentType>(); return new QIBusPlatformInputContext; } 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 31a181eec2..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,102 +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(QLatin1String("SetCursorLocationRelative"), argumentList); + 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 @@ -156,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 49a44519b6..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> @@ -50,6 +51,13 @@ 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() { @@ -79,6 +87,7 @@ public: QList<QInputMethodEvent::Attribute> attributes; bool needsSurroundingText; QLocale locale; + PreeditFocusMode preeditFocusMode = PREEDIT_COMMIT; // for backward compatibility }; @@ -170,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(); @@ -317,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) @@ -586,6 +612,7 @@ void QIBusPlatformInputContext::connectToContextSignals() if (d->context) { 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())); @@ -596,8 +623,7 @@ void QIBusPlatformInputContext::connectToContextSignals() static inline bool checkNeedPortalSupport() { - return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, "flatpak-info"_L1).isEmpty() - || qEnvironmentVariableIsSet("SNAP"); + return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP"); } static bool shouldConnectIbusPortal() @@ -692,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; 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 bcf29dedd1..69071a22c2 100644 --- a/src/plugins/platforms/CMakeLists.txt +++ b/src/plugins/platforms/CMakeLists.txt @@ -42,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 f352d5d2ac..d5a275a76c 100644 --- a/src/plugins/platforms/android/CMakeLists.txt +++ b/src/plugins/platforms/android/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (C) 2022 The Qt Company Ltd. +# Copyright (C) 2023 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause ##################################################################### @@ -14,22 +14,20 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin 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 @@ -42,8 +40,19 @@ 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 + 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 @@ -75,4 +84,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/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 6486890018..db6c601f33 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -16,11 +16,11 @@ QT_BEGIN_NAMESPACE using namespace QNativeInterface; using namespace Qt::StringLiterals; -Q_DECLARE_JNI_TYPE(ContentResolverType, "Landroid/content/ContentResolver;"); -Q_DECLARE_JNI_TYPE(UriType, "Landroid/net/Uri;"); +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_TYPE(ParcelFileDescriptorType, "Landroid/os/ParcelFileDescriptor;"); -Q_DECLARE_JNI_TYPE(CursorType, "Landroid/database/Cursor;"); +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() @@ -194,10 +194,10 @@ QByteArray AndroidContentFileEngine::id() const return m_documentFile->id().toUtf8(); } -QDateTime AndroidContentFileEngine::fileTime(FileTime time) const +QDateTime AndroidContentFileEngine::fileTime(QFile::FileTime time) const { switch (time) { - case FileTime::ModificationTime: + case QFile::FileModificationTime: return m_documentFile->lastModified(); break; default: @@ -248,47 +248,37 @@ QString AndroidContentFileEngine::fileName(FileName f) const return QString(); } -QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, - const QStringList &filterNames) +QAbstractFileEngine::IteratorUniquePtr +AndroidContentFileEngine::beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) { - return new AndroidContentFileEngineIterator(filters, filterNames); -} - -QAbstractFileEngine::Iterator *AndroidContentFileEngine::endEntryList() -{ - 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 new AndroidContentFileEngine(fileName); -} + return {}; -AndroidContentFileEngineIterator::AndroidContentFileEngineIterator(QDir::Filters filters, - const QStringList &filterNames) - : QAbstractFileEngineIterator(filters, filterNames) -{ } -AndroidContentFileEngineIterator::~AndroidContentFileEngineIterator() +AndroidContentFileEngineIterator::AndroidContentFileEngineIterator( + const QString &path, QDir::Filters filters, const QStringList &filterNames) + : QAbstractFileEngineIterator(path, filters, filterNames) { } -QString AndroidContentFileEngineIterator::next() +AndroidContentFileEngineIterator::~AndroidContentFileEngineIterator() { - if (!hasNext()) - return QString(); - ++m_index; - return currentFilePath(); } -bool AndroidContentFileEngineIterator::hasNext() const +bool AndroidContentFileEngineIterator::advance() { if (m_index == -1 && m_files.isEmpty()) { const auto currentPath = path(); @@ -299,23 +289,32 @@ bool AndroidContentFileEngineIterator::hasNext() const if (iterDoc->isDirectory()) for (const auto &doc : iterDoc->listFiles()) m_files.append(doc); + if (m_files.isEmpty()) + return false; + m_index = 0; + return true; } - return m_index < (m_files.size() - 1); + if (m_index < m_files.size() - 1) { + ++m_index; + return true; + } + + return false; } QString AndroidContentFileEngineIterator::currentFileName() const { if (m_index < 0 || m_index > m_files.size()) return QString(); - // Returns a full path since contstructing a content path from the file name - // and a tree URI only will not point to a valid file URI. - return m_files.at(m_index)->uri().toString(); + return m_files.at(m_index)->name(); } QString AndroidContentFileEngineIterator::currentFilePath() const { - return currentFileName(); + if (m_index < 0 || m_index > m_files.size()) + return QString(); + return m_files.at(m_index)->uri().toString(); } // Start of Cursor @@ -465,7 +464,7 @@ const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory"); QString documentId(const QJniObject &uri) { return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "getDocumentId", uri.object()).toString(); } @@ -473,7 +472,7 @@ QString documentId(const QJniObject &uri) QString treeDocumentId(const QJniObject &uri) { return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "getTreeDocumentId", uri.object()).toString(); } @@ -481,7 +480,7 @@ QString treeDocumentId(const QJniObject &uri) QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId) { return QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "buildChildDocumentsUriUsingTree", uri.object<QtJniTypes::UriType>(), QJniObject::fromString(parentDocumentId).object<jstring>()); @@ -491,7 +490,7 @@ QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId) { return QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "buildDocumentUriUsingTree", treeUri.object<QtJniTypes::UriType>(), QJniObject::fromString(documentId).object<jstring>()); @@ -500,7 +499,7 @@ QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &d bool isDocumentUri(const QJniObject &uri) { return QJniObject::callStaticMethod<jboolean>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "isDocumentUri", QNativeInterface::QAndroidApplication::context(), uri.object<QtJniTypes::UriType>()); @@ -509,7 +508,7 @@ bool isDocumentUri(const QJniObject &uri) bool isTreeUri(const QJniObject &uri) { return QJniObject::callStaticMethod<jboolean>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "isTreeUri", uri.object<QtJniTypes::UriType>()); } @@ -518,7 +517,7 @@ QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mi const QString &displayName) { return QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "createDocument", contentResolverInstance().object<QtJniTypes::ContentResolverType>(), parentDocumentUri.object<QtJniTypes::UriType>(), @@ -533,7 +532,7 @@ bool deleteDocument(const QJniObject &documentUri) return {}; return QJniObject::callStaticMethod<jboolean>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "deleteDocument", contentResolverInstance().object<QtJniTypes::ContentResolverType>(), documentUri.object<QtJniTypes::UriType>()); @@ -548,7 +547,7 @@ QJniObject moveDocument(const QJniObject &sourceDocumentUri, return {}; return QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "moveDocument", contentResolverInstance().object<QtJniTypes::ContentResolverType>(), sourceDocumentUri.object<QtJniTypes::UriType>(), @@ -563,7 +562,7 @@ QJniObject renameDocument(const QJniObject &documentUri, const QString &displayN return {}; return QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::DocumentsContract>(), + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), "renameDocument", contentResolverInstance().object<QtJniTypes::ContentResolverType>(), documentUri.object<QtJniTypes::UriType>(), @@ -593,33 +592,38 @@ DocumentFile::DocumentFile(const QJniObject &uri, QJniObject parseUri(const QString &uri) { + QString uriToParse = uri; + if (uriToParse.contains(' ')) + uriToParse.replace(' ', QUrl::toPercentEncoding(" ")); + return QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::Uri>(), + QtJniTypes::Traits<QtJniTypes::Uri>::className(), "parse", - QJniObject::fromString(uri).object<jstring>()); + QJniObject::fromString(uriToParse).object<jstring>()); } DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName) { - const QJniObject uri = parseUri(fileName); + const QString encodedUri = QUrl(fileName).toEncoded(); + const QJniObject uri = parseUri(encodedUri); - if (DocumentsContract::isDocumentUri(uri)) + if (DocumentsContract::isDocumentUri(uri) || !DocumentsContract::isTreeUri(uri)) return fromSingleUri(uri); const QString documentType = "/document/"_L1; const QString treeType = "/tree/"_L1; - const int treeIndex = fileName.indexOf(treeType); - const int documentIndex = fileName.indexOf(documentType); + 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 = fileName.left(index); + const QString parentUrl = encodedUri.left(index); DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl)); - const QString baseName = fileName.mid(index); + const QString baseName = encodedUri.mid(index); const QString fileUrl = parentUrl + QUrl::toPercentEncoding(baseName); DocumentFilePtr docFile = std::make_shared<MakeableDocumentFile>(parseUri(fileUrl)); diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index 35db7b44b6..a5dd1b30f3 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -27,11 +27,11 @@ public: bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; QByteArray id() const override; bool caseSensitive() const override { return true; } - QDateTime fileTime(FileTime time) const override; + 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: void closeNativeFileDescriptor(); @@ -43,19 +43,22 @@ private: 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: diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp index 8990289dc4..da5b63ef21 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.cpp +++ b/src/plugins/platforms/android/androidjniaccessibility.cpp @@ -15,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 @@ -48,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. @@ -81,8 +82,7 @@ namespace QtAndroidAccessibility void initialize() { - QJniObject::callStaticMethod<void>(QtAndroid::applicationClass(), - "initializeAccessibility"); + QtAndroid::qtActivityDelegate().callMethod<void>("initializeAccessibility"); } bool isActive() @@ -127,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); @@ -351,16 +357,6 @@ namespace QtAndroidAccessibility return result && oldPosition != screenRect_helper(firstChildId, false); } - -#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); - static QString textFromValue(QAccessibleInterface *iface) { QString valueStr; @@ -557,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}, @@ -577,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 9bbbe80fe9..6e8e059334 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.h +++ b/src/plugins/platforms/android/androidjniaccessibility.h @@ -9,14 +9,16 @@ 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); diff --git a/src/plugins/platforms/android/androidjniclipboard.cpp b/src/plugins/platforms/android/androidjniclipboard.cpp deleted file mode 100644 index 81663cac0c..0000000000 --- a/src/plugins/platforms/android/androidjniclipboard.cpp +++ /dev/null @@ -1,96 +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->hasUrls()) { - QList<QUrl> urls = data->urls(); - for (const auto &u : std::as_const(urls)) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardUri", - "(Ljava/lang/String;)V", - QJniObject::fromString(u.toEncoded()).object()); - } - } else if (data->hasText()) { // hasText || hasUrls, so the order matter here. - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardText", "(Ljava/lang/String;)V", - QJniObject::fromString(data->text()).object()); - } else 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()); - } - } - - 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..d074e73b9e 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,25 +19,92 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); + using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtLayout, "org/qtproject/qt/android/QtLayout") + 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() + { + return qtActivityDelegate().callMethod<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", + qCDebug(lcQpaInputMethods) << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; + qtInputDelegate().callMethod<void>("updateSelection", selStart, selEnd, candidatesStart, @@ -44,39 +113,33 @@ namespace QtAndroidInput void showSoftwareKeyboard(int left, int top, int width, int height, int inputHints, int enterKeyType) { - QJniObject::callStaticMethod<void>(applicationClass(), - "showSoftwareKeyboard", - "(IIIIII)V", + qtInputDelegate().callMethod<void>("showSoftwareKeyboard", + QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), left, top, width, height, inputHints, enterKeyType); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; -#endif + 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 + qtInputDelegate().callMethod<void>("resetSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ RESETSOFTWAREKEYBOARD"; } void hideSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "hideSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ HIDESOFTWAREKEYBOARD"); -#endif + qtInputDelegate().callMethod<void>("hideSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ HIDESOFTWAREKEYBOARD"; } bool isSoftwareKeyboardVisible() { - return QJniObject::callStaticMethod<jboolean>(applicationClass(), "isSoftwareKeyboardVisible"); + return qtInputDelegate().callMethod<jboolean>("isSoftwareKeyboardVisible"); } QRect softwareKeyboardRect() @@ -86,81 +149,150 @@ namespace QtAndroidInput int getSelectHandleWidth() { - return QJniObject::callStaticMethod<jint>(applicationClass(), "getSelectHandleWidth"); + return qtInputDelegate().callMethod<jint>("getSelectHandleWidth"); } void updateHandles(int mode, QPoint editMenuPos, uint32_t editButtons, QPoint cursor, QPoint anchor, bool rtl) { - QJniObject::callStaticMethod<void>(applicationClass(), "updateHandles", "(IIIIIIIIZ)V", + qtInputDelegate().callMethod<void>("updateHandles", + QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), mode, editMenuPos.x(), editMenuPos.y(), editButtons, cursor.x(), cursor.y(), anchor.x(), anchor.y(), rtl); } - static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + // 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 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 +303,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 +351,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 +386,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 +427,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 +454,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 +466,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 +932,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 +945,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 +958,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 +975,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 5e4f383e61..9fdcf3936b 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> @@ -29,12 +31,16 @@ #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; @@ -44,11 +50,12 @@ 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 QtJniTypes::QtActivityDelegateBase m_activityDelegate = nullptr; +static QtJniTypes::QtInputDelegate m_inputDelegate = nullptr; static int m_pendingApplicationState = -1; static QBasicMutex m_platformMutex; @@ -67,10 +74,6 @@ static void *m_mainLibraryHnd = nullptr; static QList<QByteArray> m_applicationParams; static sem_t m_exitSemaphore, m_terminateSemaphore; -QHash<int, AndroidSurfaceClient *> m_surfaces; - -Q_CONSTINIT static QBasicMutex m_surfacesMutex; - static QAndroidPlatformIntegration *m_androidPlatformIntegration = nullptr; @@ -90,6 +93,8 @@ static const char m_methodErrorMsg[] = "Can't find method \"%s%s\""; Q_CONSTINIT static QBasicAtomicInt startQtAndroidPluginCalled = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_DECLARE_JNI_CLASS(QtEmbeddedDelegateFactory, "org/qtproject/qt/android/QtEmbeddedDelegateFactory") + namespace QtAndroid { QBasicMutex *platformInterfaceMutex() @@ -100,6 +105,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)) { @@ -125,6 +131,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; @@ -160,53 +181,96 @@ 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; + qtActivityDelegate().callMethod<void>("setSystemUiVisibility", jint(uiVisibility)); } - QtJniTypes::Service service() + // FIXME: avoid direct access to QtActivityDelegate + QtJniTypes::QtActivityDelegateBase qtActivityDelegate() { - return m_serviceObject; + using namespace QtJniTypes; + if (!m_activityDelegate.isValid()) { + if (isQtApplication()) { + auto context = QtAndroidPrivate::activity(); + m_activityDelegate = context.callMethod<QtActivityDelegateBase>("getActivityDelegate"); + } else { + m_activityDelegate = QJniObject::callStaticMethod<QtActivityDelegateBase>( + Traits<QtEmbeddedDelegateFactory>::className(), + "getActivityDelegate", + QtAndroidPrivate::activity()); + } + } + + return m_activityDelegate; } - void setSystemUiVisibility(SystemUiVisibility uiVisibility) + QtJniTypes::QtInputDelegate qtInputDelegate() { - QJniObject::callStaticMethod<void>(m_applicationClass, "setSystemUiVisibility", "(I)V", jint(uiVisibility)); + if (!m_inputDelegate.isValid()) { + m_inputDelegate = qtActivityDelegate().callMethod<QtJniTypes::QtInputDelegate>( + "getInputDelegate"); + } + + return m_inputDelegate; + } + + bool isQtApplication() + { + // 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 notifyAccessibilityLocationChange(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyAccessibilityLocationChange", - "(I)V", accessibilityObjectId); + qtActivityDelegate().callMethod<void>("notifyLocationChange", accessibilityObjectId); } void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectHide", "(II)V", - accessibilityObjectId, parentObjectId); + qtActivityDelegate().callMethod<void>("notifyObjectHide", + accessibilityObjectId, parentObjectId); + } + + void notifyObjectShow(uint parentObjectId) + { + qtActivityDelegate().callMethod<void>("notifyObjectShow", + parentObjectId); } void notifyObjectFocus(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectFocus","(I)V", accessibilityObjectId); + qtActivityDelegate().callMethod<void>("notifyObjectFocus", accessibilityObjectId); } void notifyValueChanged(uint accessibilityObjectId, jstring value) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyValueChanged", - "(ILjava/lang/String;)V", accessibilityObjectId, value); + qtActivityDelegate().callMethod<void>("notifyValueChanged", accessibilityObjectId, value); } void notifyScrolledEvent(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyScrolledEvent", "(I)V", - accessibilityObjectId); + qtActivityDelegate().callMethod<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) @@ -215,7 +279,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, @@ -303,62 +367,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, @@ -368,69 +376,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"); @@ -446,14 +391,14 @@ 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; - const char *nativeString = env->GetStringUTFChars(paramsString, 0); - const QStringList argsList = QProcess::splitCommand(QString::fromUtf8(nativeString)); - env->ReleaseStringUTFChars(paramsString, nativeString); + const QStringList argsList = QProcess::splitCommand(QJniObject(paramsString).toString()); for (const QString &arg : argsList) m_applicationParams.append(arg.toUtf8()); @@ -496,7 +441,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(); } @@ -527,7 +472,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); @@ -535,10 +481,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); @@ -583,10 +527,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) @@ -597,24 +537,16 @@ 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; 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, @@ -647,6 +579,7 @@ static void setDisplayMetrics(JNIEnv * /*env*/, jclass /*clazz*/, jint screenWid m_androidPlatformIntegration->setRefreshRate(refreshRate); } } +Q_DECLARE_JNI_NATIVE_METHOD(setDisplayMetrics) static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/) { @@ -666,10 +599,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) @@ -748,36 +677,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::setColorScheme( + 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, @@ -804,116 +739,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 }, - { "handleUiDarkModeChanged", "(I)V", (void *)handleUiDarkModeChanged }, - { "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 }, - { "handleScreenAdded", "(I)V", (void *)handleScreenAdded }, - { "handleScreenChanged", "(I)V", (void *)handleScreenChanged }, - { "handleScreenRemoved", "(I)V", (void *)handleScreenRemoved } + { "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 @@ -926,36 +876,29 @@ 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)) { __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 4879d7e5b2..99fff96d2b 100644 --- a/src/plugins/platforms/android/androidjnimain.h +++ b/src/plugins/platforms/android/androidjnimain.h @@ -12,6 +12,7 @@ #include <QImage> #include <private/qjnihelpers_p.h> +#include <QtCore/QJniObject> QT_BEGIN_NAMESPACE @@ -22,26 +23,22 @@ 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(); 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 +47,9 @@ namespace QtAndroid jobject assets(); AAssetManager *assetManager(); jclass applicationClass(); - QtJniTypes::Activity activity(); - QtJniTypes::Service service(); + + QtJniTypes::QtActivityDelegateBase qtActivityDelegate(); + QtJniTypes::QtInputDelegate qtInputDelegate(); // Keep synchronized with flags in ActivityDelegate.java enum SystemUiVisibility { @@ -67,10 +65,11 @@ namespace QtAndroid 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 notifyScrolledEvent(uint accessibilityObjectId); - void notifyQtAndroidPluginRunning(bool running); + void notifyNativePluginIntegrationReady(bool ready); const char *classErrorMsgFmt(); const char *methodErrorMsgFmt(); @@ -78,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 7aaa5d921c..8bf37d1af2 100644 --- a/src/plugins/platforms/android/androidjnimenu.cpp +++ b/src/plugins/platforms/android/androidjnimenu.cpp @@ -31,8 +31,6 @@ namespace QtAndroidMenu static QWindow *activeTopLevelWindow = nullptr; Q_CONSTINIT static QRecursiveMutex menuBarMutex; - static jmethodID openContextMenuMethodID = 0; - static jmethodID clearMenuMethodID = 0; static jmethodID addMenuItemMethodID = 0; static int menuNoneValue = 0; @@ -46,29 +44,31 @@ namespace QtAndroidMenu void resetMenuBar() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetOptionsMenu"); + qtActivityDelegate().callMethod<void>("resetOptionsMenu"); } void openOptionsMenu() { - QJniObject::callStaticMethod<void>(applicationClass(), "openOptionsMenu"); + qtActivityDelegate().callMethod<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()); + qtActivityDelegate().callMethod<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"); + qtActivityDelegate().callMethod<void>("closeContextMenu"); pendingContextMenus.clear(); } else { pendingContextMenus.removeOne(menu); @@ -211,8 +211,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 +251,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 +265,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 +275,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 +305,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,13 +316,16 @@ 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); @@ -328,8 +342,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 +355,7 @@ namespace QtAndroidMenu visibleMenu->aboutToHide(); visibleMenu = 0; if (!pendingContextMenus.empty()) - showContextMenu(pendingContextMenus.takeLast(), QRect(), env); + showContextMenu(pendingContextMenus.takeLast(), QRect()); } static JNINativeMethod methods[] = { @@ -378,17 +396,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 c7785486b0..4ea6536cef 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -142,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: @@ -171,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); } @@ -195,21 +191,9 @@ public: 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: @@ -307,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: @@ -367,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; } @@ -393,13 +379,14 @@ 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); @@ -407,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/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 4c111be829..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,6 +903,9 @@ void QAndroidInputContext::showInputPanel() if (query.isNull()) return; + if (!qGuiApp->focusWindow()->handle()) + return; // not a real window, probably VR/XR + disconnect(m_updateCursorPosConnection); m_updateCursorPosConnection = {}; @@ -1112,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; @@ -1534,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 61fc21a6bc..ea7f22295d 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp @@ -28,6 +28,8 @@ 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) { 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 25427d5ede..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()) { } @@ -150,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} }; @@ -162,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 3723e33371..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(); } @@ -146,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; 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 9f0b1c8b2f..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> @@ -54,36 +54,44 @@ Qt::ScreenOrientation QAndroidPlatformIntegration::m_orientation = Qt::PrimaryOr 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_TYPE(List, "Ljava/util/List;") +Q_DECLARE_JNI_CLASS(List, "java/util/List") namespace { QAndroidPlatformScreen* createScreenForDisplayId(int displayId) { - const QJniObject display = QJniObject::callStaticObjectMethod<QtJniTypes::Display>( - QtJniTypes::className<QtJniTypes::QtNative>(), - "getDisplay", - 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()) @@ -156,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(); } @@ -183,12 +187,10 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ if (Q_UNLIKELY(!eglBindAPI(EGL_OPENGL_ES_API))) qFatal("Could not bind GL_ES API"); - m_primaryDisplayId = QJniObject::getStaticField<jint>( - QtJniTypes::className<QtJniTypes::Display>(), "DEFAULT_DISPLAY"); - - const QJniObject nativeDisplaysList = QJniObject::callStaticObjectMethod<QtJniTypes::List>( - QtJniTypes::className<QtJniTypes::QtNative>(), - "getAvailableDisplays"); + 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) { @@ -227,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;"); @@ -269,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;"); @@ -304,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 @@ -316,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); } @@ -330,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); @@ -356,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()); @@ -370,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; @@ -381,7 +393,7 @@ QOffscreenSurface *QAndroidPlatformIntegration::createOffscreenSurface(ANativeWi QPlatformWindow *QAndroidPlatformIntegration::createPlatformWindow(QWindow *window) const { - if (!QtAndroid::activity()) + if (!isValidAndroidContextForRendering()) return nullptr; #if QT_CONFIG(vulkan) @@ -534,7 +546,7 @@ void QAndroidPlatformIntegration::setScreenSize(int width, int height) Qt::ColorScheme QAndroidPlatformIntegration::m_colorScheme = Qt::ColorScheme::Light; -void QAndroidPlatformIntegration::setColorScheme(Qt::ColorScheme colorScheme) +void QAndroidPlatformIntegration::updateColorScheme(Qt::ColorScheme colorScheme) { if (m_colorScheme == colorScheme) return; diff --git a/src/plugins/platforms/android/qandroidplatformintegration.h b/src/plugins/platforms/android/qandroidplatformintegration.h index 5f1126fafc..b7bfb58d1d 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.h +++ b/src/plugins/platforms/android/qandroidplatformintegration.h @@ -110,7 +110,7 @@ public: void flushPendingUpdates(); - static void setColorScheme(Qt::ColorScheme colorScheme); + static void updateColorScheme(Qt::ColorScheme colorScheme); static Qt::ColorScheme colorScheme() { return m_colorScheme; } #if QT_CONFIG(vulkan) QPlatformVulkanInstance *createPlatformVulkanInstance(QVulkanInstance *instance) const override; 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 b2fa2ed3e5..9e20b7ac4b 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" @@ -53,8 +52,13 @@ private: #endif 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_TYPE(DisplayMode, "Landroid/view/Display$Mode;") +Q_DECLARE_JNI_CLASS(DisplayMode, "android/view/Display$Mode") QAndroidPlatformScreen::QAndroidPlatformScreen(const QJniObject &displayObject) : QObject(), QPlatformScreen() @@ -79,14 +83,33 @@ QAndroidPlatformScreen::QAndroidPlatformScreen(const QJniObject &displayObject) if (!displayObject.isValid()) return; - m_size = QSize(displayObject.callMethod<jint>("getWidth"), displayObject.callMethod<jint>("getHeight")); 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"); - const jint currentModeId = currentMode.callMethod<jint>("getModeId"); + m_currentMode = currentMode.callMethod<jint>("getModeId"); const QJniObject supportedModes = displayObject.callObjectMethod<QtJniTypes::DisplayMode[]>( "getSupportedModes"); @@ -96,19 +119,9 @@ QAndroidPlatformScreen::QAndroidPlatformScreen(const QJniObject &displayObject) const auto size = env->GetArrayLength(modeArray); for (jsize i = 0; i < size; ++i) { const auto mode = QJniObject::fromLocalRef(env->GetObjectArrayElement(modeArray, i)); - const int physicalWidth = mode.callMethod<jint>("getPhysicalWidth"); - const int physicalHeight = mode.callMethod<jint>("getPhysicalHeight"); - - if (currentModeId == mode.callMethod<jint>("getModeId")) { - m_currentMode = i; - m_physicalSize = QSize { - physicalWidth, - physicalHeight - }; - } - m_modes << QPlatformScreen::Mode { - .size = QSize { physicalWidth, physicalHeight }, + .size = QSize { mode.callMethod<jint>("getPhysicalWidth"), + mode.callMethod<jint>("getPhysicalHeight") }, .refreshRate = mode.callMethod<jfloat>("getRefreshRate") }; } @@ -117,23 +130,18 @@ QAndroidPlatformScreen::QAndroidPlatformScreen(const QJniObject &displayObject) QAndroidPlatformScreen::~QAndroidPlatformScreen() { - if (m_surfaceId != -1) { - QtAndroid::destroySurface(m_surfaceId); - 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 @@ -145,16 +153,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()) @@ -164,83 +162,45 @@ void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) return; m_windowStack.prepend(window); - if (window->isRaster()) { - m_rasterSurfaces.ref(); - setDirty(window->geometry()); - } + QtAndroid::qtActivityDelegate().callMethod<void>("addTopLevelWindow", window->nativeWindow()); - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + 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()); - } + QtAndroid::qtActivityDelegate().callMethod<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); + QtAndroid::qtActivityDelegate().callMethod<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)); - } -} + QtAndroid::qtActivityDelegate().callMethod<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) @@ -292,7 +252,6 @@ void QAndroidPlatformScreen::setOrientation(Qt::ScreenOrientation orientation) void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) { - QMutexLocker lock(&m_surfaceMutex); if (m_availableGeometry == rect) return; @@ -313,165 +272,26 @@ void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) } } } - - if (m_surfaceId != -1) { - releaseSurface(); - QtAndroid::setSurfaceGeometry(m_surfaceId, rect); - } } void QAndroidPlatformScreen::applicationStateChanged(Qt::ApplicationState state) { for (QAndroidPlatformWindow *w : std::as_const(m_windowStack)) w->applicationStateChanged(state); - - if (state <= Qt::ApplicationHidden) { - lockSurface(); - QtAndroid::destroySurface(m_surfaceId); - m_surfaceId = -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 : std::as_const(m_windowStack)) { - if (window->window()->isVisible() && window->isRaster() && !qt_window_private(window->window())->compositing) { - hasVisibleRasterWindows = true; - break; - } - } - if (!hasVisibleRasterWindows) { - lockSurface(); - if (m_surfaceId != -1) { - QtAndroid::destroySurface(m_surfaceId); - releaseSurface(); - m_surfaceId = -1; - } - unlockSurface(); - return; - } - QMutexLocker lock(&m_surfaceMutex); - if (m_surfaceId == -1 && m_rasterSurfaces) { - m_surfaceId = 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 : std::as_const(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 @@ -494,67 +314,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 076530613b..d850d0db09 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.h +++ b/src/plugins/platforms/android/qandroidplatformscreen.h @@ -5,25 +5,22 @@ #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 <qpa/qplatformscreen_p.h> - -#include <android/native_window.h> +#include <QtGui/qscreen_platform.h> QT_BEGIN_NAMESPACE class QAndroidPlatformWindow; -class QAndroidPlatformScreen: public QObject, - public QPlatformScreen, public AndroidSurfaceClient, - public QNativeInterface::Private::QAndroidScreen + +class QAndroidPlatformScreen : public QObject, + public QPlatformScreen, + public QNativeInterface::QAndroidScreen { Q_OBJECT public: @@ -41,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); @@ -66,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; @@ -88,25 +75,9 @@ private: 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_surfaceId = -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 5964236420..39287aa905 100644 --- a/src/plugins/platforms/android/qandroidplatformservices.cpp +++ b/src/plugins/platforms/android/qandroidplatformservices.cpp @@ -24,19 +24,22 @@ 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_TYPE(UriType, "Landroid/net/Uri;") -Q_DECLARE_JNI_TYPE(FileType, "Ljava/io/File;") +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"); @@ -78,11 +81,11 @@ bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) const auto providerName = QJniObject::fromString(appId + ".qtprovider"_L1); const auto urlPath = QJniObject::fromString(url.path()); - const auto urlFile = QJniObject(QtJniTypes::className<QtJniTypes::File>(), + const auto urlFile = QJniObject(QtJniTypes::Traits<QtJniTypes::File>::className(), urlPath.object<jstring>()); const auto fileProviderUri = QJniObject::callStaticMethod<QtJniTypes::UriType>( - QtJniTypes::className<QtJniTypes::FileProvider>(), "getUriForFile", + QtJniTypes::Traits<QtJniTypes::FileProvider>::className(), "getUriForFile", QAndroidApplication::context(), providerName.object<jstring>(), urlFile.object<QtJniTypes::FileType>()); diff --git a/src/plugins/platforms/android/qandroidplatformtheme.cpp b/src/plugins/platforms/android/qandroidplatformtheme.cpp index f863529057..7b9072df69 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 { @@ -163,13 +166,6 @@ QJsonObject AndroidStyle::loadStyleData() Q_ASSERT(!stylePath.isEmpty()); - QString androidTheme = QLatin1StringView(qgetenv("QT_ANDROID_THEME")); - if (!androidTheme.isEmpty() && !androidTheme.endsWith(slashChar)) - androidTheme += slashChar; - - if (!androidTheme.isEmpty() && QFileInfo::exists(stylePath + androidTheme + "style.json"_L1)) - stylePath += androidTheme; - QFile f(stylePath + "style.json"_L1); if (!f.open(QIODevice::ReadOnly)) return QJsonObject(); @@ -400,21 +396,28 @@ void QAndroidPlatformTheme::updateStyle() 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(); } @@ -488,6 +491,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 bb8c5c4869..ce3d6d5f73 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(); @@ -40,6 +42,7 @@ public: Qt::ColorScheme colorScheme() const 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; 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 b1eba17d04..979f0fb98a 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformwindow.cpp @@ -14,40 +14,95 @@ QT_BEGIN_NAMESPACE -Q_CONSTINIT static QBasicAtomicInt winIdGenerator = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window") 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; + + if (parent()) { + QAndroidPlatformWindow *androidParent = static_cast<QAndroidPlatformWindow*>(parent()); + if (!androidParent->isEmbeddingContainer()) + m_nativeParentQtWindow = androidParent->nativeWindow(); + } + + m_nativeQtWindow = QJniObject::construct<QtJniTypes::QtWindow>( + QNativeInterface::QAndroidApplication::context(), + m_nativeParentQtWindow, + QtAndroid::qtInputDelegate()); + m_nativeViewId = m_nativeQtWindow.callMethod<jint>("getId"); - // initialGeometry returns in native pixels - const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( - window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), - availableDeviceIndependentGeometry.height()); + 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 +126,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 +177,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 +215,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 +251,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/cocoa/CMakeLists.txt b/src/plugins/platforms/cocoa/CMakeLists.txt index af8434daaa..92e681d8fb 100644 --- a/src/plugins/platforms/cocoa/CMakeLists.txt +++ b/src/plugins/platforms/cocoa/CMakeLists.txt @@ -56,6 +56,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin ${FWIOSurface} ${FWMetal} ${FWQuartzCore} + ${FWUniformTypeIdentifiers} Qt::Core Qt::CorePrivate Qt::Gui diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index dc2baa6961..c5e40a4087 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -117,7 +117,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; @@ -125,7 +124,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; @@ -137,6 +136,7 @@ static void populateRoleMap() roleMap[QAccessible::Note] = NSAccessibilityGroupRole; roleMap[QAccessible::ComplementaryContent] = NSAccessibilityGroupRole; roleMap[QAccessible::Graphic] = NSAccessibilityImageRole; + roleMap[QAccessible::Tree] = NSAccessibilityOutlineRole; } /* diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h index 1f121e2fd8..a96ab55735 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.h @@ -15,7 +15,9 @@ QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QMacAccessibilityElement, NSObject <NSAcces - (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 68e7947162..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,6 +85,8 @@ 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 @@ -105,12 +112,65 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of self = [super init]; if (self) { axid = anId; + m_rowIndex = -1; + m_columnIndex = -1; rows = nil; columns = nil; synthesizedRole = role; - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->tableInterface() && !synthesizedRole) - [self updateTableModel]; + // 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; @@ -126,16 +186,23 @@ 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; @@ -173,8 +240,11 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of - (NSMutableArray *)populateTableArray:(NSMutableArray *)array role:(NSAccessibilityRole)role count:(int)count { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid()) { + if (QAccessibleInterface *iface = self.qtInterface) { + if (array && int(array.count) != count) { + [array release]; + array = nil; + } if (!array) { array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count]; [array retain]; @@ -187,6 +257,10 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of 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 { @@ -198,29 +272,99 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of 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 { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid()) { + 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]; @@ -240,31 +384,33 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSString *) accessibilityRole { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) - return NSAccessibilityUnknownRole; + // shortcut for cells, rows, and columns in a table if (synthesizedRole) return synthesizedRole; - return QCocoaAccessible::macRole(iface); + 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 { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) + // 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 @@ -320,30 +466,39 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } 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 - QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithId:axid]; - Q_ASSERT(tableElement->rows); - NSUInteger rowIndex = [tableElement->rows indexOfObjectIdenticalTo:self]; - Q_ASSERT(rowIndex != NSNotFound); - int numColumns = table->columnCount(); - NSMutableArray<QMacAccessibilityElement *> *cells = - [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numColumns]; - for (int i = 0; i < numColumns; ++i) { - QAccessibleInterface *cell = table->cellAt(rowIndex, i); - if (cell && cell->isValid()) { - QAccessible::Id cellId = QAccessible::uniqueId(cell); - QMacAccessibilityElement *element = - [QMacAccessibilityElement elementWithId:cellId]; - if (element) { - [cells addObject:element]; - } - } - } - return NSAccessibilityUnignoredChildren(cells); + 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; + + 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 { // We're in the same window as our parent. return [self.accessibilityParent accessibilityWindow]; @@ -355,26 +510,35 @@ 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; - if (self.isManagedByParent) - 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) { @@ -389,23 +553,23 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of if (QAccessibleInterface *parent = iface->parent()) { if (parent->tableInterface()) { - if (QAccessibleTableCellInterface *cell = iface->tableCellInterface()) { - // parent of cell should be row - QAccessible::Id parentId = QAccessible::uniqueId(parent); - QMacAccessibilityElement *tableElement = - [QMacAccessibilityElement elementWithId:parentId]; - - const int rowIndex = cell->rowIndex(); - if (tableElement->rows && int([tableElement->rows count]) > rowIndex) { - QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex]; - return NSAccessibilityUnignoredAncestor(rowElement); - } - } - } - if (parent->role() != QAccessible::Application) { - QAccessible::Id parentId = QAccessible::uniqueId(parent); - return NSAccessibilityUnignoredAncestor([QMacAccessibilityElement elementWithId: parentId]); + 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()) { @@ -419,8 +583,8 @@ 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; QRect rect; @@ -437,10 +601,7 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of int &row = isRow ? cellPos.ry() : cellPos.rx(); int &col = isRow ? cellPos.rx() : cellPos.ry(); - QMacAccessibilityElement *tableElement = - [QMacAccessibilityElement elementWithId:axid]; - NSArray *tracks = isRow ? tableElement->rows : tableElement->columns; - NSUInteger trackIndex = [tracks indexOfObjectIdenticalTo:self]; + NSUInteger trackIndex = self.accessibilityIndex; if (trackIndex != NSNotFound) { row = int(trackIndex); if (QAccessibleInterface *firstCell = table->cellAt(cellPos.y(), cellPos.x())) { @@ -463,71 +624,50 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSString*)accessibilityLabel { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (!iface || !iface->isValid()) { - qWarning() << "Called accessibilityLabel on invalid object: " << axid; - return nil; - } - 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)); -} - -- (id) accessibilityMinValue:(QAccessibleInterface*)iface { - if (QAccessibleValueInterface *val = iface->valueInterface()) - return @(val->minimumValue().toDouble()); + 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; @@ -544,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); @@ -555,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()) @@ -565,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(); @@ -577,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; } @@ -601,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; } @@ -642,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); @@ -670,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]) { @@ -689,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()) @@ -718,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); @@ -733,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; @@ -751,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); } @@ -761,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); } @@ -788,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); } @@ -815,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(); @@ -829,19 +969,17 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of */ - (NSInteger) accessibilityIndex { NSInteger index = 0; - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid()) { + 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()) { - QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithId:axid]; - NSArray *track = synthesizedRole == NSAccessibilityRowRole - ? tableElement->rows : tableElement->columns; - if (track) { - NSUInteger trackIndex = [track indexOfObjectIdenticalTo:self]; - index = (NSInteger)trackIndex; - } + if (m_rowIndex >= 0) + index = NSInteger(m_rowIndex); + else if (m_columnIndex >= 0) + index = NSInteger(m_columnIndex); } } } @@ -849,18 +987,18 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of } - (NSArray *) accessibilityRows { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid() && iface->tableInterface() && !synthesizedRole) { - if (rows) + if (!synthesizedRole && rows) { + QAccessibleInterface *iface = self.qtInterface; + if (iface && iface->tableInterface()) return NSAccessibilityUnignoredChildren(rows); } return nil; } - (NSArray *) accessibilityColumns { - QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); - if (iface && iface->isValid() && iface->tableInterface() && !synthesizedRole) { - if (columns) + if (!synthesizedRole && columns) { + QAccessibleInterface *iface = self.qtInterface; + if (iface && iface->tableInterface()) return NSAccessibilityUnignoredChildren(columns); } return nil; diff --git a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm index bb7b5c3c0c..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(); @@ -314,21 +334,72 @@ 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]; + 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). - const QString qurlString = QString::fromNSString(urlString); 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) @@ -371,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 8f50bc5834..b211b5d02d 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -23,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); } // ---------------------------------------------------------------------------- @@ -72,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) @@ -189,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; } @@ -256,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(); @@ -440,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); } } @@ -475,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(); } // ---------------------------------------------------------------------------- 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 101a13e3c2..a8404889e9 100644 --- a/src/plugins/platforms/cocoa/qcocoadrag.mm +++ b/src/plugins/platforms/cocoa/qcocoadrag.mm @@ -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 <AppKit/AppKit.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> #include "qcocoadrag.h" #include "qmacclipboard.h" @@ -161,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; } @@ -213,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; } @@ -224,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(); + } } @@ -240,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()) { diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 787b23ec4d..96eb70dabc 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -56,9 +56,12 @@ #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); @@ -67,11 +70,11 @@ 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) @@ -86,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(); diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index fd0a4b4717..739fbda4f5 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -115,6 +115,7 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() return; } + using DoubleSeconds = std::chrono::duration<double, std::ratio<1>>; if (!runLoopTimerRef) { // start the CFRunLoopTimer CFAbsoluteTime ttf = CFAbsoluteTimeGetCurrent(); @@ -122,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; @@ -145,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) @@ -170,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()) { @@ -187,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()) { @@ -229,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 @@ -374,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) { @@ -417,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 @@ -537,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() @@ -617,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]; @@ -656,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. @@ -704,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]; } @@ -716,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(). @@ -736,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 @@ -782,7 +802,7 @@ void qt_mac_maybeCancelWaitForMoreEventsForwarder(QAbstractEventDispatcher *even } QCocoaEventDispatcher::QCocoaEventDispatcher(QObject *parent) - : QAbstractEventDispatcher(*new QCocoaEventDispatcherPrivate, parent) + : QAbstractEventDispatcherV2(*new QCocoaEventDispatcherPrivate, parent) { Q_D(QCocoaEventDispatcher); @@ -957,7 +977,7 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher() { Q_D(QCocoaEventDispatcher); - qDeleteAll(d->timerInfoList); + d->timerInfoList.clearTimers(); d->maybeStopCFRunLoopTimer(); CFRunLoopRemoveSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes); CFRelease(d->activateTimersSourceRef); @@ -966,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]; } 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 91d76fa254..044a282686 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -27,6 +27,8 @@ #include <qpa/qplatformtheme.h> #include <qpa/qplatformnativeinterface.h> +#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> + QT_USE_NAMESPACE using namespace Qt::StringLiterals; @@ -55,12 +57,11 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; NSPopUpButton *m_popupButton; NSTextField *m_textField; QPointer<QCocoaFileDialogHelper> m_helper; - NSString *m_currentDirectory; 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 @@ -80,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; @@ -113,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]; @@ -124,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]; @@ -184,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]; @@ -194,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); @@ -215,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]; @@ -290,8 +378,8 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; Q_UNUSED(sender); if (!m_helper) return; - QString selection = m_nameFilterDropDownList->value([m_popupButton indexOfSelectedItem]); - *m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; + const QString selection = m_nameFilterDropDownList.value([m_popupButton indexOfSelectedItem]); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; [m_panel validateVisibleColumns]; [self updateProperties]; @@ -310,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()) }; } } @@ -353,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]; @@ -378,10 +479,18 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; 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)); } } @@ -394,14 +503,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; if (!m_helper) return; - if (!(path && path.length) || [path isEqualToString:m_currentDirectory]) - 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); } /* @@ -409,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 { @@ -421,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; @@ -432,6 +532,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; continue; auto extensions = filter.split('.', Qt::SkipEmptyParts); + if (extensions.count() > 2) + return nil; + fileTypes += extensions.last(); } @@ -468,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; @@ -485,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(); } @@ -528,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/qcocoahelpers.h b/src/plugins/platforms/cocoa/qcocoahelpers.h index 694e57e73d..c6862a9e65 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.h +++ b/src/plugins/platforms/cocoa/qcocoahelpers.h @@ -56,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 @@ -88,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 diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.mm b/src/plugins/platforms/cocoa/qcocoahelpers.mm index ec64c94d0b..1eba88d5e3 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.mm +++ b/src/plugins/platforms/cocoa/qcocoahelpers.mm @@ -336,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 72e5798bd6..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,8 +84,7 @@ public: QCocoaServices *services() const override; QVariant styleHint(StyleHint hint) const override; - Qt::KeyboardModifiers queryKeyboardModifiers() const override; - QList<int> possibleKeys(const QKeyEvent *event) const override; + QPlatformKeyMapper *keyMapper() const override; void setApplicationIcon(const QIcon &icon) const override; void setApplicationBadge(qint64 number) override; diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.mm b/src/plugins/platforms/cocoa/qcocoaintegration.mm index 6dd410ad22..2ce39ff897 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.mm +++ b/src/plugins/platforms/cocoa/qcocoaintegration.mm @@ -33,11 +33,16 @@ #include <QtCore/private/qcore_mac_p.h> #include <QtGui/private/qcoregraphics_p.h> #include <QtGui/private/qmacmimeregistry_p.h> -#include <QtGui/private/qopenglcontext_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() { @@ -120,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; @@ -137,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 @@ -190,6 +185,9 @@ 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 @@ -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,14 +406,9 @@ QVariant QCocoaIntegration::styleHint(StyleHint hint) const return QPlatformIntegration::styleHint(hint); } -Qt::KeyboardModifiers QCocoaIntegration::queryKeyboardModifiers() const -{ - return QAppleKeyMapper::queryKeyboardModifiers(); -} - -QList<int> QCocoaIntegration::possibleKeys(const QKeyEvent *event) const +QPlatformKeyMapper *QCocoaIntegration::keyMapper() const { - return mKeyboardMapper->possibleKeys(event); + return mKeyboardMapper.data(); } void QCocoaIntegration::setApplicationIcon(const QIcon &icon) const @@ -435,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 0f39246a43..fa88a19d45 100644 --- a/src/plugins/platforms/cocoa/qcocoamenu.mm +++ b/src/plugins/platforms/cocoa/qcocoamenu.mm @@ -19,6 +19,7 @@ #include "qcocoaapplicationdelegate.h" #include <QtCore/private/qcore_mac_p.h> +#include <QtCore/qpointer.h> QT_BEGIN_NAMESPACE @@ -42,6 +43,8 @@ QCocoaMenu::~QCocoaMenu() item->setMenuParent(nullptr); } + if (isOpen()) + dismiss(); [m_nativeMenu release]; } @@ -60,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; } @@ -320,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; @@ -404,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()) @@ -483,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 c0f0f9e702..2493d90724 100644 --- a/src/plugins/platforms/cocoa/qcocoamenubar.mm +++ b/src/plugins/platforms/cocoa/qcocoamenubar.mm @@ -36,7 +36,7 @@ QCocoaMenuBar::QCocoaMenuBar() QCocoaMenuBar::~QCocoaMenuBar() { - qCDebug(lcQpaMenus) << "Destructing" << this << "with" << m_nativeMenu;; + qCDebug(lcQpaMenus) << "Destructing" << this << "with" << m_nativeMenu; for (auto menu : std::as_const(m_menus)) { if (!menu) continue; @@ -162,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 @@ -186,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; } } @@ -304,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]; } @@ -325,6 +361,15 @@ 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; @@ -416,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 : std::as_const(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 : std::as_const(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 e438fd2a07..f677ffb7a7 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.h +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.h @@ -8,6 +8,8 @@ #include <qpa/qplatformmenu.h> #include <QtGui/QImage> +#include <QtCore/qpointer.h> + Q_FORWARD_DECLARE_OBJC_CLASS(NSMenuItem); Q_FORWARD_DECLARE_OBJC_CLASS(NSMenu); Q_FORWARD_DECLARE_OBJC_CLASS(NSObject); diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.mm b/src/plugins/platforms/cocoa/qcocoamenuitem.mm index 213326f892..3a0f71bc50 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.mm @@ -180,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() @@ -440,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 index 564dd915c5..b8c273469a 100644 --- a/src/plugins/platforms/cocoa/qcocoamessagedialog.h +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.h @@ -28,6 +28,7 @@ private: Qt::WindowModality modality() const; NSAlert *m_alert = nullptr; QEventLoop *m_eventLoop = nullptr; + NSModalResponse runModal() const; void processResponse(NSModalResponse response); }; diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm index f19146c60c..84525099c9 100644 --- a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm @@ -5,6 +5,7 @@ #include "qcocoawindow.h" #include "qcocoahelpers.h" +#include "qcocoaeventdispatcher.h" #include <QtCore/qmetaobject.h> #include <QtCore/qscopedvaluerollback.h> @@ -90,8 +91,17 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w if (!options()) return false; - if (windowModality == Qt::ApplicationModal && QThread::currentThread()->loopLevel() > 1) { - qCWarning(lcQpaDialogs, "Cannot use native application modal dialog from nested event loop"); + // 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; } @@ -99,10 +109,7 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w m_alert = [NSAlert new]; m_alert.window.title = options()->windowTitle().toNSString(); - QString text = toPlainText(options()->text()); - QString details = toPlainText(options()->detailedText()); - if (!details.isEmpty()) - text += u"\n\n"_s + details; + const QString text = toPlainText(options()->text()); m_alert.messageText = text.toNSString(); m_alert.informativeText = toPlainText(options()->informativeText()).toNSString(); @@ -130,8 +137,8 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w break; } - bool defaultButtonAdded = false; - bool cancelButtonAdded = false; + auto defaultButton = options()->defaultButton(); + auto escapeButton = options()->escapeButton(); const auto addButton = [&](auto title, auto tag, auto role) { title = QPlatformTheme::removeMnemonics(title); @@ -141,17 +148,27 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w // 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). Unfortunately QMessageBox does not currently plumb setDefaultButton/setEscapeButton - // through the dialog options, so we can't forward this information directly. The closest we - // can get right now is to use the role to set the button's key equivalent. + // 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 (role == AcceptRole && !defaultButtonAdded) { + if (tag == defaultButton) button.keyEquivalent = @"\r"; - defaultButtonAdded = true; - } else if (role == RejectRole && !cancelButtonAdded) { + 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"; - cancelButtonAdded = true; - } + else if ([button.keyEquivalent isEqualToString:@"\e"]) + button.keyEquivalent = @""; if (@available(macOS 11, *)) button.hasDestructiveAction = role == DestructiveRole; @@ -171,30 +188,69 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w 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) { + for (int standardButton = FirstButton; standardButton <= LastButton; standardButton <<= 1) { if (standardButtons & standardButton) { auto title = platformTheme->standardButtonText(standardButton); - addButton(title, standardButton, buttonRole(StandardButton(standardButton))); + buttons.push_back({ + title, standardButton, buttonRole(StandardButton(standardButton)) + }); } } } - const auto customButtons = options()->customButtons(); for (auto customButton : customButtons) - addButton(customButton.label, customButton.id, customButton.role); + 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); - // QMessageDialog's logic for adding a fallback OK button if no other buttons - // are added depends on QMessageBox::showEvent(), which is too late when - // native dialogs are in use. To ensure there's always an OK button with a tag - // we recognize we add it explicitly here as a fallback. - if (!m_alert.buttons.count) { - addButton(platformTheme->standardButtonText(StandardButton::Ok), - StandardButton::Ok, ButtonRole::AcceptRole); + ++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(); @@ -222,9 +278,10 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w // 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 && NSApp.modalWindow != m_alert.window) { + if (m_alert && !m_alert.window.visible) { qCDebug(lcQpaDialogs) << "Running deferred modal" << m_alert; - processResponse([m_alert runModal]); + QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); + processResponse(runModal()); } }); } @@ -232,6 +289,20 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w 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); @@ -243,7 +314,8 @@ void QCocoaMessageDialog::exec() m_eventLoop->exec(QEventLoop::DialogExec); } else { qCDebug(lcQpaDialogs) << "Running modal" << m_alert; - processResponse([m_alert runModal]); + QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); + processResponse(runModal()); } } 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 078d8a1c72..be562e5455 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -15,6 +15,7 @@ #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> @@ -199,7 +200,7 @@ QCocoaScreen::~QCocoaScreen() static QString displayName(CGDirectDisplayID displayID) { QIOType<io_iterator_t> iterator; - if (IOServiceGetMatchingServices(kIOMasterPortDefault, + if (IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IODisplayConnect"), &iterator)) return QString(); @@ -247,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())); @@ -271,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); @@ -279,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) @@ -289,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 @@ -361,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 @@ -510,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; } 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 68daa660ef..87212c265c 100644 --- a/src/plugins/platforms/cocoa/qcocoaservices.mm +++ b/src/plugins/platforms/cocoa/qcocoaservices.mm @@ -4,14 +4,23 @@ #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) { + // avoid recursing back into self + if (url == m_handlingUrl) + return false; + return [[NSWorkspace sharedWorkspace] openURL:url.toNSURL()]; } @@ -20,4 +29,45 @@ bool QCocoaServices::openDocument(const QUrl &url) 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); +} + +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.mm b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm index ec366f5483..cec8301cf6 100644 --- a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm +++ b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm @@ -184,7 +184,18 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon) void QCocoaSystemTrayIcon::updateMenu(QPlatformMenu *menu) { - m_statusItem.menu = menu ? static_cast<QCocoaMenu *>(menu)->nsMenu() : nil; + 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 diff --git a/src/plugins/platforms/cocoa/qcocoatheme.h b/src/plugins/platforms/cocoa/qcocoatheme.h index a7c37a685c..c49d83feae 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.h +++ b/src/plugins/platforms/cocoa/qcocoatheme.h @@ -35,6 +35,7 @@ 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; Qt::ColorScheme colorScheme() const override; @@ -54,6 +55,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 594df204e5..f4fbfadbe4 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -22,6 +22,7 @@ #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> @@ -94,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; } @@ -221,6 +225,8 @@ QCocoaTheme::QCocoaTheme() NSSystemColorsDidChangeNotification, [this] { handleSystemThemeChange(); }); + + updateColorScheme(); } QCocoaTheme::~QCocoaTheme() @@ -239,6 +245,9 @@ void QCocoaTheme::reset() void QCocoaTheme::handleSystemThemeChange() { reset(); + + updateColorScheme(); + m_systemPalette = qt_mac_createSystemPalette(); m_palettes = qt_mac_createRolePalettes(); @@ -401,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 @@ -432,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) { @@ -445,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: @@ -472,7 +475,19 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const Qt::ColorScheme QCocoaTheme::colorScheme() const { - return qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; + return m_colorScheme; +} + +/* + 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() +{ + m_colorScheme = qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; } QString QCocoaTheme::standardButtonText(int button) const @@ -490,12 +505,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 @@ -508,7 +527,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.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 2083803c6d..4a245a0f8a 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -117,20 +117,26 @@ 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())); - recreateWindowIfNeeded(); + } else { + // Pick up essential foreign window state + QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect()); + } - setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); + recreateWindowIfNeeded(); m_initialized = true; } @@ -144,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 @@ -168,8 +177,7 @@ QCocoaWindow::~QCocoaWindow() object:m_view]; [m_view release]; - [m_nsWindow close]; - [m_nsWindow release]; + [m_nsWindow closeAndRelease]; } QSurfaceFormat QCocoaWindow::format() const @@ -300,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; @@ -338,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); @@ -375,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()) { @@ -410,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 @@ -551,10 +598,17 @@ 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; @@ -1192,32 +1246,33 @@ 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. @@ -1229,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); } } @@ -1271,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 @@ -1440,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; @@ -1478,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) { @@ -1509,20 +1586,9 @@ 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()); - setOpacity(window()->opacity()); - } else { + if (parentCocoaWindow) { // Child windows have no NSWindow, re-parent to superview instead [parentCocoaWindow->m_view addSubview:m_view]; - [m_view setHidden:!window()->isVisible()]; } } @@ -1532,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(); @@ -1726,8 +1795,9 @@ 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; @@ -1742,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; } @@ -1789,26 +1863,28 @@ void QCocoaWindow::setWindowCursor(NSCursor *cursor) 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]; - if (QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSMonterey) { - // 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. - // We only do this is if the window would normally receive cursor updates. - auto locationInWindow = m_view.window.mouseLocationOutsideOfEventStream; - auto locationInSuperview = [m_view.superview convertPoint:locationInWindow fromView:nil]; - 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 - eventNumber:0 trackingNumber:0 userData:0]]; - } + // 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]; + 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 + eventNumber:0 trackingNumber:0 userData:0]]; } } @@ -1913,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)]; @@ -1940,8 +2016,21 @@ bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder() if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput)) return true; - if (QWindowPrivate::get(window())->blockedByModalWindow) - 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"); diff --git a/src/plugins/platforms/cocoa/qmacclipboard.h b/src/plugins/platforms/cocoa/qmacclipboard.h index 75c112a10b..95267565f2 100644 --- a/src/plugins/platforms/cocoa/qmacclipboard.h +++ b/src/plugins/platforms/cocoa/qmacclipboard.h @@ -7,6 +7,8 @@ #include <QtGui> #include <QtGui/qutimimeconverter.h> +#include <QtCore/qpointer.h> + #include <ApplicationServices/ApplicationServices.h> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qnsview.h b/src/plugins/platforms/cocoa/qnsview.h index e41f5a7296..7f845a5c3b 100644 --- a/src/plugins/platforms/cocoa/qnsview.h +++ b/src/plugins/platforms/cocoa/qnsview.h @@ -32,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 f8c17e179d..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" @@ -36,13 +37,6 @@ #include "qcocoaintegration.h" #include <QtGui/private/qmacmimeregistry_p.h> -// Private interface -@interface QNSView () -- (BOOL)isTransparentForUserInput; -@property (assign) NSView* previousSuperview; -@property (assign) NSWindow* previousWindow; -@end - @interface QNSView (Drawing) <CALayerDelegate> - (void)initDrawing; @end @@ -86,6 +80,20 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); @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 { QPointer<QCocoaWindow> m_platformWindow; @@ -113,11 +121,20 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); 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; @@ -133,6 +150,8 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); m_sendKeyEvent = false; m_currentlyInterpretedKeyEvent = nil; m_lastSeenContext = NSDraggingContextWithinApplication; + + self.menuHelper = [[[QNSViewMenuHelper alloc] initWithView:self] autorelease]; } return self; } @@ -258,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 @@ -299,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 3ccaf8269e..d7f8f4baf0 100644 --- a/src/plugins/platforms/cocoa/qnsview_complextext.mm +++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm @@ -130,8 +130,8 @@ newlineEvent.key = isEnter ? Qt::Key_Enter : Qt::Key_Return; newlineEvent.text = isEnter ? QLatin1Char(kEnterCharCode) : QLatin1Char(kReturnCharCode); - newlineEvent.nativeVirtualKey = isEnter ? kVK_ANSI_KeypadEnter - : kVK_Return; + newlineEvent.nativeVirtualKey = isEnter ? quint32(kVK_ANSI_KeypadEnter) + : quint32(kVK_Return); qCDebug(lcQpaKeys) << "Inserting newline via" << newlineEvent; newlineEvent.sendWindowSystemEvent(m_platformWindow->window()); @@ -505,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 f5bb25c300..4f7d35a0d6 100644 --- a/src/plugins/platforms/cocoa/qnsview_dragging.mm +++ b/src/plugins/platforms/cocoa/qnsview_dragging.mm @@ -16,8 +16,8 @@ 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 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 118678ffa5..abee622e65 100644 --- a/src/plugins/platforms/cocoa/qnsview_keys.mm +++ b/src/plugins/platforms/cocoa/qnsview_keys.mm @@ -30,8 +30,36 @@ static bool isSpecialKey(const QString &text) 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; @@ -52,17 +80,8 @@ static bool isSpecialKey(const QString &text) 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 ? m_platformWindow->window()->focusObject() : nullptr; @@ -94,7 +113,10 @@ static bool isSpecialKey(const QString &text) 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; diff --git a/src/plugins/platforms/cocoa/qnsview_menus.mm b/src/plugins/platforms/cocoa/qnsview_menus.mm index 644f2139de..2840936975 100644 --- a/src/plugins/platforms/cocoa/qnsview_menus.mm +++ b/src/plugins/platforms/cocoa/qnsview_menus.mm @@ -9,23 +9,55 @@ #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; + qCDebug(lcQpaMenus) << "Validating" << item << "for" << self.view; auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item); if (!nativeItem) @@ -53,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; } @@ -62,54 +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]; - qCDebug(lcQpaMenus) << "Forwarding" << invocation.selector << "from" << sender; - - // We claim to respond to standard edit actions such as cut/copy/paste, - // but these might not be exclusively coming from menu items that we - // control. For example, when embedded into a native UI (as a plugin), - // the menu items might be part of the host application, and if we're - // the first responder, we'll be the target of these actions. As we - // don't have a mechanism in Qt to trigger generic actions, we have - // to bail out if we don't have a QCocoaNSMenuItem we can activate(). - // Note that we skip the call to super as well, as that would just - // try to invoke the current action on ourselves again. - if (auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(sender)) - [self qt_itemFired:nativeItem]; - else - qCDebug(lcQpaMenus) << "Ignoring action for menu item we didn't create"; - - 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 f69e809133..8f842eba85 100644 --- a/src/plugins/platforms/cocoa/qnswindow.h +++ b/src/plugins/platforms/cocoa/qnswindow.h @@ -36,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 d7418b8abf..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 @@ -196,6 +206,29 @@ static bool isMouseEvent(NSEvent *ev) 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]]; 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 4cbc7ac986..54e96b09e5 100644 --- a/src/plugins/platforms/direct2d/CMakeLists.txt +++ b/src/plugins/platforms/direct2d/CMakeLists.txt @@ -12,11 +12,11 @@ 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 @@ -47,7 +47,6 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin qwindowsdirect2dwindow.cpp qwindowsdirect2dwindow.h NO_UNITY_BUILD_SOURCES ../windows/qwindowspointerhandler.cpp - ../windows/qwindowsmousehandler.cpp DEFINES QT_NO_CAST_FROM_ASCII QT_NO_FOREACH @@ -69,6 +68,7 @@ qt_internal_add_plugin(QWindowsDirect2DIntegrationPlugin imm32 ole32 oleaut32 + setupapi shell32 shlwapi user32 @@ -108,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 @@ -186,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 @@ -207,11 +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 ) -if (MINGW) - set_source_files_properties(../windows/qwindowspointerhandler.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON) -endif() diff --git a/src/plugins/platforms/directfb/qdirectfbblitter.cpp b/src/plugins/platforms/directfb/qdirectfbblitter.cpp index aac0e3d800..c93e48ead7 100644 --- a/src/plugins/platforms/directfb/qdirectfbblitter.cpp +++ b/src/plugins/platforms/directfb/qdirectfbblitter.cpp @@ -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/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/qdirectfbinput.cpp b/src/plugins/platforms/directfb/qdirectfbinput.cpp index 516dc1e9d8..c5aacad6cc 100644 --- a/src/plugins/platforms/directfb/qdirectfbinput.cpp +++ b/src/plugins/platforms/directfb/qdirectfbinput.cpp @@ -176,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/eglfs/CMakeLists.txt b/src/plugins/platforms/eglfs/CMakeLists.txt index cebb0fe66e..a0a6116a45 100644 --- a/src/plugins/platforms/eglfs/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/CMakeLists.txt @@ -34,6 +34,7 @@ qt_internal_add_module(EglFSDeviceIntegrationPrivate DEFINES QT_BUILD_EGL_DEVICE_LIB QT_EGL_NO_X11 + QT_NO_FOREACH EGLFS_PREFERRED_PLUGIN=${QT_QPA_DEFAULT_EGLFS_INTEGRATION} INCLUDE_DIRECTORIES api @@ -45,6 +46,7 @@ qt_internal_add_module(EglFSDeviceIntegrationPrivate EGL::EGL HEADER_SYNC_SOURCE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/api" + NO_GENERATE_CPP_EXPORTS ) ## Scopes: diff --git a/src/plugins/platforms/eglfs/api/qeglfscursor.cpp b/src/plugins/platforms/eglfs/api/qeglfscursor.cpp index 391442de71..1e1a7d8269 100644 --- a/src/plugins/platforms/eglfs/api/qeglfscursor.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfscursor.cpp @@ -368,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); @@ -391,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); @@ -426,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]; @@ -505,6 +507,8 @@ void QEglFSCursor::draw(const QRectF &r) 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); f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 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 72c4a47dbb..c5108be04a 100644 --- a/src/plugins/platforms/eglfs/api/qeglfsscreen.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfsscreen.cpp @@ -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(); diff --git a/src/plugins/platforms/eglfs/api/qeglfswindow.cpp b/src/plugins/platforms/eglfs/api/qeglfswindow.cpp index 1a31f97d7b..306d121cfb 100644 --- a/src/plugins/platforms/eglfs/api/qeglfswindow.cpp +++ b/src/plugins/platforms/eglfs/api/qeglfswindow.cpp @@ -263,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())); } 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 d6db171637..bb50216f23 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/CMakeLists.txt @@ -14,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) @@ -26,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_kms/CMakeLists.txt b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt index d575ae9d4f..9f9d315202 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/CMakeLists.txt @@ -24,6 +24,7 @@ qt_internal_add_module(EglFsKmsGbmSupportPrivate Qt::GuiPrivate Qt::KmsSupportPrivate gbm::gbm + NO_GENERATE_CPP_EXPORTS ) ##################################################################### ## QEglFSKmsGbmIntegrationPlugin Plugin: diff --git a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp index e6fd627823..8dcfed04db 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen.cpp @@ -73,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) @@ -142,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, @@ -155,16 +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; + m_flipPending = false; // not necessarily true but enough to keep bo_next m_gbm_bo_current = nullptr; - m_gbm_bo_next = 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, @@ -194,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); 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 3660f094d2..aca34fcae2 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen_p.h +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms/qeglfskmsgbmscreen_p.h @@ -83,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/qeglfskmsegldeviceintegration.cpp b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_egldevice/qeglfskmsegldeviceintegration.cpp index 9ada2de2b6..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); 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 d4e5f2f90c..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,11 +13,62 @@ 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() { + 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()) @@ -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 bffacb822e..5fcc0e1b18 100644 --- a/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/CMakeLists.txt +++ b/src/plugins/platforms/eglfs/deviceintegration/eglfs_kms_support/CMakeLists.txt @@ -24,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_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/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 e0473b3ce4..4cc3efc91e 100644 --- a/src/plugins/platforms/ios/CMakeLists.txt +++ b/src/plugins/platforms/ios/CMakeLists.txt @@ -27,7 +27,13 @@ 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} @@ -52,19 +58,31 @@ qt_internal_extend_target(QIOSIntegrationPlugin CONDITION QT_FEATURE_opengl 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 + qiosdocumentpickercontroller.h qiosdocumentpickercontroller.mm + LIBRARIES + ${FWUniformTypeIdentifiers} + ${FWPhotos} +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS + SOURCES qioscolordialog.h qioscolordialog.mm qiosfontdialog.h qiosfontdialog.mm - qiosmenu.h qiosmenu.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} - ${FWUniformTypeIdentifiers} ) add_subdirectory(optional) 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 f545a81bf2..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 { 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/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 f07391a369..de8ab69dff 100644 --- a/src/plugins/platforms/ios/qiosclipboard.mm +++ b/src/plugins/platforms/ios/qiosclipboard.mm @@ -11,18 +11,6 @@ #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 @@ -31,7 +19,6 @@ @implementation QUIClipboard { QIOSClipboard *m_qiosClipboard; NSInteger m_changeCountClipboard; - NSInteger m_changeCountFindBuffer; } - (instancetype)initWithQIOSClipboard:(QIOSClipboard *)qiosClipboard @@ -39,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 @@ -77,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 @@ -99,20 +79,17 @@ 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) { @@ -127,7 +104,7 @@ 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]; const auto converters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All); @@ -164,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]; } @@ -172,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; @@ -193,15 +170,17 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) 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; @@ -213,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.mm b/src/plugins/platforms/ios/qioscolordialog.mm index 92c0f3e46c..6651b1791d 100644 --- a/src/plugins/platforms/ios/qioscolordialog.mm +++ b/src/plugins/platforms/ios/qioscolordialog.mm @@ -8,6 +8,7 @@ #include <QtCore/private/qcore_mac_p.h> +#include "qiosglobal.h" #include "qioscolordialog.h" #include "qiosintegration.h" @@ -117,8 +118,7 @@ bool QIOSColorDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windo if (windowModality == Qt::ApplicationModal || windowModality == Qt::WindowModal) m_viewController.modalInPresentation = YES; - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window - : qt_apple_sharedApplication().keyWindow; + UIWindow *window = presentationWindow(parent); if (!window) return false; diff --git a/src/plugins/platforms/ios/qioscontext.mm b/src/plugins/platforms/ios/qioscontext.mm index 1649089841..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" diff --git a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm index af866077cd..fca0432426 100644 --- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm +++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm @@ -30,14 +30,14 @@ case QFileDialogOptions::AnyFile: case QFileDialogOptions::ExistingFile: case QFileDialogOptions::ExistingFiles: - [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)kUTTypeContent]]; - [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)kUTTypeItem]]; - [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)kUTTypeData]]; + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeContent]]; + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeItem]]; + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeData]]; break; // Showing files is not supported in Directory mode in iOS case QFileDialogOptions::Directory: case QFileDialogOptions::DirectoryOnly: - [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)kUTTypeFolder]]; + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeFolder]]; break; } } 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 526c040335..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() @@ -53,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) { @@ -71,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; } @@ -83,8 +123,7 @@ bool QIOSFileDialog::showNativeDocumentPickerDialog(QWindow *parent) #ifndef Q_OS_TVOS 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.mm b/src/plugins/platforms/ios/qiosfontdialog.mm index 4cea1cb558..25d0197195 100644 --- a/src/plugins/platforms/ios/qiosfontdialog.mm +++ b/src/plugins/platforms/ios/qiosfontdialog.mm @@ -11,6 +11,7 @@ #include <QtGui/private/qfont_p.h> #include <QtGui/private/qfontengine_p.h> +#include "qiosglobal.h" #include "qiosfontdialog.h" #include "qiosintegration.h" @@ -144,8 +145,7 @@ bool QIOSFontDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window if (windowModality == Qt::ApplicationModal || windowModality == Qt::WindowModal) m_viewController.modalInPresentation = YES; - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window - : qt_apple_sharedApplication().keyWindow; + UIWindow *window = presentationWindow(parent); if (!window) return false; 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..25ccf2961b 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -13,6 +13,7 @@ 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() { @@ -29,6 +30,15 @@ bool isQtApplication() return isQt; } +bool isRunningOnVisionOS() +{ + static bool result = []{ + // This class is documented to only be available on visionOS + return NSClassFromString(@"UIWindowSceneGeometryPreferencesVision"); + }(); + return result; +} + #ifndef Q_OS_TVOS Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientation) { @@ -85,6 +95,47 @@ 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 = windowScene.keyWindow; + if (!uiWindow && windowScene.windows.count) + uiWindow = windowScene.windows[0]; + + return uiWindow.rootViewController.view; + } + + return nullptr; +} + QT_END_NAMESPACE // ------------------------------------------------------------------------- @@ -119,7 +170,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 7a76760638..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,7 +121,7 @@ static QUIView *focusView() { [self keyboardWillOrDidChange:notification]; - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; if (![firstResponder isKindOfClass:[QIOSTextInputResponder class]]) return; @@ -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; @@ -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(); diff --git a/src/plugins/platforms/ios/qiosintegration.h b/src/plugins/platforms/ios/qiosintegration.h index 3de1fb4c57..2c7d33cc94 100644 --- a/src/plugins/platforms/ios/qiosintegration.h +++ b/src/plugins/platforms/ios/qiosintegration.h @@ -11,7 +11,8 @@ #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 @@ -31,6 +32,7 @@ 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) @@ -40,9 +42,11 @@ public: 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; @@ -75,7 +79,7 @@ public: private: QPlatformFontDatabase *m_fontDatabase; -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) QPlatformClipboard *m_clipboard; #endif QPlatformInputContext *m_inputContext; @@ -83,7 +87,7 @@ 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 }; diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index 27d0f7f2ba..7cd21f83f6 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -1,13 +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" -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) #include "qiosclipboard.h" #endif #include "qiosinputcontext.h" @@ -49,7 +51,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) @@ -70,6 +72,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]) { @@ -79,6 +85,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; @@ -86,8 +93,10 @@ 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) @@ -105,7 +114,7 @@ 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 @@ -148,6 +157,8 @@ bool QIOSIntegration::hasCapability(Capability cap) const return false; case ApplicationState: return true; + case ForeignWindows: + return true; default: return QPlatformIntegration::hasCapability(cap); } @@ -158,6 +169,11 @@ 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); @@ -199,14 +215,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 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 5c9bbc276f..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); diff --git a/src/plugins/platforms/ios/qiosmessagedialog.mm b/src/plugins/platforms/ios/qiosmessagedialog.mm index b95cfada60..7fbd5d8729 100644 --- a/src/plugins/platforms/ios/qiosmessagedialog.mm +++ b/src/plugins/platforms/ios/qiosmessagedialog.mm @@ -30,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(); @@ -40,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; @@ -116,26 +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; - if (!window) { - qCDebug(lcQpaWindow, "Attempting to exec a dialog without any window/widget visible."); - - auto *primaryScreen = static_cast<QIOSScreen*>(QGuiApplication::primaryScreen()->handle()); - Q_ASSERT(primaryScreen); - - window = primaryScreen->uiWindow(); - 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; - } - } - + 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 e968cafe1d..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,6 +12,7 @@ #include "qiosviewcontroller.h" #include "quiview.h" #include "qiostheme.h" +#include "quiwindow.h" #include <QtCore/private/qcore_mac_p.h> @@ -44,6 +47,7 @@ typedef void (^DisplayLinkBlock)(CADisplayLink *displayLink); // ------------------------------------------------------------------------- +#if !defined(Q_OS_VISIONOS) static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) { foreach (QScreen *screen, QGuiApplication::screens()) { @@ -81,6 +85,9 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) + (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"); @@ -89,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"); @@ -97,104 +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]; - - 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>(); - } - } -} - -@end +#endif // !defined(Q_OS_VISIONOS) // ------------------------------------------------------------------------- @@ -202,6 +115,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; +#if !defined(Q_OS_VISIONOS) /*! Returns the model identifier of the device. */ @@ -221,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(); @@ -260,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 = [existingWindow 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() @@ -305,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() ? @@ -356,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 @@ -440,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; @@ -461,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 @@ -500,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(); @@ -527,15 +401,12 @@ 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; } - -UIWindow *QIOSScreen::uiWindow() const -{ - return m_uiWindow; -} +#endif QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiostextinputoverlay.mm b/src/plugins/platforms/ios/qiostextinputoverlay.mm index 512ab77bd2..01046334a1 100644 --- a/src/plugins/platforms/ios/qiostextinputoverlay.mm +++ b/src/plugins/platforms/ios/qiostextinputoverlay.mm @@ -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 f8bcf7fb25..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>(); diff --git a/src/plugins/platforms/ios/qiostheme.h b/src/plugins/platforms/ios/qiostheme.h index 9aa2ebaf82..f0a404a61a 100644 --- a/src/plugins/platforms/ios/qiostheme.h +++ b/src/plugins/platforms/ios/qiostheme.h @@ -23,13 +23,16 @@ public: Qt::ColorScheme colorScheme() const 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; diff --git a/src/plugins/platforms/ios/qiostheme.mm b/src/plugins/platforms/ios/qiostheme.mm index d1356ab8f7..3853de9cf1 100644 --- a/src/plugins/platforms/ios/qiostheme.mm +++ b/src/plugins/platforms/ios/qiostheme.mm @@ -11,18 +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 "qiosmessagedialog.h" #include "qioscolordialog.h" #include "qiosfontdialog.h" +#include "qiosmessagedialog.h" +#include "qiosscreen.h" +#include "quiwindow.h" #endif QT_BEGIN_NAMESPACE @@ -68,6 +74,9 @@ void QIOSTheme::initializeSystemPalette() 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 @@ -77,23 +86,17 @@ 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 { @@ -144,17 +147,28 @@ QVariant QIOSTheme::themeHint(ThemeHint hint) const Qt::ColorScheme QIOSTheme::colorScheme() const { - UIUserInterfaceStyle appearance = UIUserInterfaceStyleUnspecified; - // Set the appearance based on the UIWindow +#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 + // Set the appearance based on the QUIWindow // Fallback to the UIScreen if no window is created yet - if (UIWindow *window = qt_apple_sharedApplication().windows.lastObject) { - appearance = window.traitCollection.userInterfaceStyle; - } else { - appearance = UIScreen.mainScreen.traitCollection.userInterfaceStyle; + 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 appearance == UIUserInterfaceStyleDark ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; +#endif } const QFont *QIOSTheme::font(Font type) const @@ -164,4 +178,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 90380e4bb0..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,15 +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; @@ -80,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 99f9e38846..6a1080e238 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -11,14 +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> #endif -#ifdef Q_OS_IOS +#if QT_CONFIG(metal) #import <QuartzCore/CAMetalLayer.h> #endif @@ -26,34 +29,48 @@ 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); - setParent(QPlatformWindow::parent()); + if (QPlatformWindow::parent()) + 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) { @@ -75,8 +92,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]; } @@ -115,7 +139,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; @@ -204,7 +228,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); } @@ -228,22 +252,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); } @@ -251,10 +298,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() @@ -265,7 +318,6 @@ void QIOSWindow::requestActivateWindow() if (blockedByModal()) return; - Q_ASSERT(m_view.window); [m_view.window makeKeyWindow]; [m_view becomeFirstResponder]; @@ -275,8 +327,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; @@ -284,17 +334,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() @@ -323,20 +383,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 @@ -346,7 +399,10 @@ qreal QIOSWindow::devicePixelRatio() const void QIOSWindow::clearAccessibleCache() { - [m_view clearAccessibleCache]; + if (isForeignWindow()) + return; + + [quiview_cast(m_view) clearAccessibleCache]; } void QIOSWindow::requestUpdate() @@ -354,6 +410,20 @@ 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 { @@ -375,6 +445,37 @@ QDebug operator<<(QDebug debug, const QIOSWindow *window) } #endif // !QT_NO_DEBUG_STREAM +/*! + 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 979de6a313..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 @@ -53,7 +54,6 @@ inline ulong getTimeStamp(UIEvent *event) @implementation QUIView { QHash<NSUInteger, QWindowSystemInterface::TouchPoint> m_activeTouches; UITouch *m_activePencilTouch; - int m_nextTouchId; NSMutableArray<UIAccessibilityElement *> *m_accessibleElements; UIPanGestureRecognizer *m_scrollGestureRecognizer; CGPoint m_lastScrollCursorPos; @@ -62,7 +62,7 @@ inline ulong getTimeStamp(UIEvent *event) + (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 @@ -198,6 +198,7 @@ inline ulong getTimeStamp(UIEvent *event) 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 @@ -207,6 +208,7 @@ inline ulong getTimeStamp(UIEvent *event) // FIXME: Allow the scale factor to be customized through QSurfaceFormat. } +#endif - (void)didAddSubview:(UIView *)subview { @@ -264,6 +266,9 @@ inline ulong getTimeStamp(UIEvent *event) Q_UNUSED(layer); Q_ASSERT(layer == self.layer); + if (!self.platformWindow) + return; + [self sendUpdatedExposeEvent]; } @@ -305,7 +310,7 @@ inline ulong getTimeStamp(UIEvent *event) // 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"; @@ -316,7 +321,7 @@ inline ulong getTimeStamp(UIEvent *event) } 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"; @@ -344,7 +349,7 @@ inline ulong getTimeStamp(UIEvent *event) - (BOOL)resignFirstResponder { - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; if (![super resignFirstResponder]) return NO; @@ -353,7 +358,7 @@ inline ulong getTimeStamp(UIEvent *event) UIResponder *newResponder = FirstResponderCandidate::currentCandidate(); if ([self responderShouldTriggerWindowDeactivation:newResponder]) - QWindowSystemInterface::handleWindowActivated(nullptr, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason); return YES; } @@ -367,7 +372,7 @@ inline ulong getTimeStamp(UIEvent *event) if ([self isFirstResponder]) return YES; - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; if ([firstResponder isKindOfClass:[QIOSTextInputResponder class]] && [firstResponder nextResponder] == self) return YES; @@ -518,7 +523,10 @@ inline ulong getTimeStamp(UIEvent *event) { 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 @@ -560,9 +568,6 @@ inline ulong getTimeStamp(UIEvent *event) // 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 @@ -595,7 +600,6 @@ inline ulong getTimeStamp(UIEvent *event) qWarning("Subset of active touches cancelled by UIKit"); m_activeTouches.clear(); - m_nextTouchId = 0; m_activePencilTouch = nil; ulong timestamp = event ? getTimeStamp(event) : ([[NSProcessInfo processInfo] systemUptime] * 1000); @@ -699,7 +703,7 @@ inline ulong getTimeStamp(UIEvent *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 @@ -712,7 +716,7 @@ inline ulong getTimeStamp(UIEvent *event) - (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; @@ -828,14 +832,9 @@ inline ulong getTimeStamp(UIEvent *event) return nil; } -- (UIEdgeInsets)qt_safeAreaInsets -{ - return self.safeAreaInsets; -} - @end -#ifdef Q_OS_IOS +#if QT_CONFIG(metal) @implementation QUIMetalView + (Class)layerClass diff --git a/src/plugins/platforms/ios/quiview_accessibility.mm b/src/plugins/platforms/ios/quiview_accessibility.mm index 366141ef81..04e1f8cfb3 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -54,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..7c910b6d9e --- /dev/null +++ b/src/plugins/platforms/ios/quiwindow.mm @@ -0,0 +1,56 @@ +// 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; +} + +- (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/offscreen/qoffscreenwindow.cpp b/src/plugins/platforms/offscreen/qoffscreenwindow.cpp index 20ed0ed91b..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 diff --git a/src/plugins/platforms/qnx/CMakeLists.txt b/src/plugins/platforms/qnx/CMakeLists.txt index 3dfd006382..9fb412d8a4 100644 --- a/src/plugins/platforms/qnx/CMakeLists.txt +++ b/src/plugins/platforms/qnx/CMakeLists.txt @@ -31,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 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 ff6ce05aaa..cc10f6a00e 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" @@ -62,14 +64,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 +96,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 +140,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 +217,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 +274,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 +310,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 +328,14 @@ QPlatformWindow *QQnxIntegration::createPlatformWindow(QWindow *window) const QPlatformBackingStore *QQnxIntegration::createPlatformBackingStore(QWindow *window) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; return new QQnxRasterBackingStore(window); } #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 +394,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 +403,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 +417,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 +434,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 +453,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 +467,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 +475,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 +483,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); @@ -594,7 +592,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 +622,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 +661,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 +713,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 +727,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 525b22242c..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) 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/qvkkhrdisplayvulkaninstance.cpp b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp index 4d58e4154e..2e8d60209e 100644 --- a/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp +++ b/src/plugins/platforms/vkkhrdisplay/qvkkhrdisplayvulkaninstance.cpp @@ -143,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/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index d7c96afdaa..185b921a4f 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -32,9 +32,11 @@ qt_internal_add_plugin(QWasmIntegrationPlugin 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 QT_NO_FOREACH @@ -47,7 +49,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" ) 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 0419509098..8027dd8fa9 100644 --- a/src/plugins/platforms/wasm/qtloader.js +++ b/src/plugins/platforms/wasm/qtloader.js @@ -1,602 +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 = new 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 = new QtLoader(config); -// qtLoader.loadEmscriptenModule("applicationName"); -// -// Config keys -// -// moduleConfig : {} -// Emscripten module configuration -// 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. - - -// Forces the use of constructor on QtLoader instance. -// This passthrough makes both the old-style: -// -// const loader = QtLoader(config); -// -// and the new-style: -// -// const loader = new QtLoader(config); -// -// instantiation types work. -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) { - return new _QtLoader(config); -} - -function _QtLoader(config) -{ - const self = this; - - // The Emscripten module and module configuration object. The module - // object is created in completeLoadEmscriptenModule(). - self.module = undefined; - self.moduleConfig = config.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); - }); - } - - 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 originalOnRuntimeInitialized = config.onRuntimeInitialized; + config.onRuntimeInitialized = () => { + originalOnRuntimeInitialized?.(); + config.qt.onLoaded?.(); } - function loadEmscriptenModule(applicationName) { - - // 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) - }); + 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 completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule) { + let onExitCalled = false; + const originalOnExit = config.onExit; + config.onExit = code => { + originalOnExit?.(); - // 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; - // Print stack trace to console - console.log(exception); - } - 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.qtAddContainerElement(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.qtRemoveContainerElement(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.qtResizeContainerElement(element); - } - - function setFontDpi(dpi) { - self.qtFontDpi = dpi; - if (publicAPI.status == "Running") - self.module.qtUpdateDpi(); - } + 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/qwasmaccessibility.cpp b/src/plugins/platforms/wasm/qwasmaccessibility.cpp index 072f65aa87..4c3cb46ba3 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.cpp +++ b/src/plugins/platforms/wasm/qwasmaccessibility.cpp @@ -5,10 +5,12 @@ #include "qwasmscreen.h" #include "qwasmwindow.h" #include "qwasmintegration.h" -#include <QtGui/private/qaccessiblebridgeutils_p.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 @@ -21,13 +23,6 @@ Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") // events. In addition or alternatively, we could also walk the accessibility tree // from setRootObject(). -namespace { -QWasmWindow *asWasmWindow(QWindow *window) -{ - return static_cast<QWasmWindow*>(window->handle()); -} -} // namespace - QWasmAccessibility::QWasmAccessibility() { @@ -105,7 +100,8 @@ void QWasmAccessibility::enableAccessibility() emscripten::val QWasmAccessibility::getContainer(QWindow *window) { - return window ? asWasmWindow(window)->a11yContainer() : emscripten::val::undefined(); + return window ? static_cast<QWasmWindow *>(window->handle())->a11yContainer() + : emscripten::val::undefined(); } emscripten::val QWasmAccessibility::getContainer(QAccessibleInterface *iface) @@ -244,12 +240,21 @@ emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *ifac case QAccessible::Dialog: { element = document.call<emscripten::val>("createElement", std::string("dialog")); }break; - case QAccessible::ToolBar: - case QAccessible::ButtonMenu: { + 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("widget")); + 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); @@ -258,12 +263,14 @@ emscripten::val QWasmAccessibility::createHtmlElement(QAccessibleInterface *ifac 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("widget")); + 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) { - ensureHtmlElement(iface->child(i)); - setHtmlElementTextName(iface->child(i)); - setHtmlElementGeometry(iface->child(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: { @@ -367,12 +374,21 @@ void QWasmAccessibility::setHtmlElementTextNameLE(QAccessibleInterface *iface) { 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; @@ -391,7 +407,9 @@ void QWasmAccessibility::handleLineEditUpdate(QAccessibleEvent *event) { 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; @@ -402,13 +420,11 @@ 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()); @@ -426,8 +442,11 @@ void QWasmAccessibility::handleEventFromHtmlElement(const emscripten::val event) } else if (eventType == "input") { - if (iface->editableTextInterface()) { - std::string insertText = event["target"]["value"].as<std::string>(); + // 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)); } } @@ -452,6 +471,9 @@ void QWasmAccessibility::handleCheckBoxUpdate(QAccessibleEvent *event) 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; @@ -468,6 +490,9 @@ void QWasmAccessibility::handleToolUpdate(QAccessibleEvent *event) 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; @@ -479,6 +504,7 @@ void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event) 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 @@ -486,6 +512,9 @@ void QWasmAccessibility::handleMenuUpdate(QAccessibleEvent *event) 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; @@ -500,7 +529,9 @@ void QWasmAccessibility::handleDialogUpdate(QAccessibleEvent *event) { 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; @@ -518,6 +549,7 @@ void QWasmAccessibility::populateAccessibilityTree(QAccessibleInterface *iface) setHtmlElementVisibility(iface, visible); setHtmlElementGeometry(iface); setHtmlElementTextName(iface); + setHtmlElementDescription(iface); for (int i = 0; i < iface->childCount(); ++i) populateAccessibilityTree(iface->child(i)); @@ -536,6 +568,9 @@ void QWasmAccessibility::handleRadioButtonUpdate(QAccessibleEvent *event) 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; @@ -555,6 +590,9 @@ void QWasmAccessibility::handleSpinBoxUpdate(QAccessibleEvent *event) 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; @@ -574,6 +612,9 @@ void QWasmAccessibility::handleSliderUpdate(QAccessibleEvent *event) 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; @@ -593,6 +634,9 @@ void QWasmAccessibility::handleScrollBarUpdate(QAccessibleEvent *event) 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; @@ -609,6 +653,9 @@ void QWasmAccessibility::handlePageTabUpdate(QAccessibleEvent *event) case QAccessible::Focus: { setHtmlElementTextName(event->accessibleInterface()); } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; default: qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); break; @@ -624,6 +671,9 @@ void QWasmAccessibility::handlePageTabListUpdate(QAccessibleEvent *event) case QAccessible::Focus: { setHtmlElementTextName(event->accessibleInterface()); } break; + case QAccessible::DescriptionChanged: { + setHtmlElementDescription(event->accessibleInterface()); + } break; default: qDebug() << "TODO: implement handlePageTabUpdate for event" << event->type(); break; @@ -645,12 +695,12 @@ void QWasmAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) // 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; @@ -735,3 +785,5 @@ void QWasmAccessibility::onHtmlEventReceived(emscripten::val 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 index dd9770179a..c4be7f0d72 100644 --- a/src/plugins/platforms/wasm/qwasmaccessibility.h +++ b/src/plugins/platforms/wasm/qwasmaccessibility.h @@ -4,6 +4,11 @@ #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> @@ -46,6 +51,7 @@ private: 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); @@ -81,4 +87,6 @@ private: }; +#endif // QT_CONFIG(accessibility) + #endif diff --git a/src/plugins/platforms/wasm/qwasmbackingstore.cpp b/src/plugins/platforms/wasm/qwasmbackingstore.cpp index e962592862..a3c1ae8a50 100644 --- a/src/plugins/platforms/wasm/qwasmbackingstore.cpp +++ b/src/plugins/platforms/wasm/qwasmbackingstore.cpp @@ -4,6 +4,7 @@ #include "qwasmbackingstore.h" #include "qwasmwindow.h" #include "qwasmcompositor.h" +#include "qwasmdom.h" #include <QtGui/qpainter.h> #include <QtGui/qbackingstore.h> @@ -75,37 +76,8 @@ void QWasmBackingStore::updateTexture(QWasmWindow *window) clippedDpiScaledRegion |= r; } - for (const QRect &dirtyRect : clippedDpiScaledRegion) { - constexpr int BytesPerColor = 4; - if (dirtyRect.width() == imageRect.width()) { - // Copy a contiguous chunk of memory - // ............... - // OOOOOOOOOOOOOOO - // OOOOOOOOOOOOOOO -> image data - // OOOOOOOOOOOOOOO - // ............... - auto imageMemory = emscripten::typed_memory_view(dirtyRect.width() * dirtyRect.height() - * BytesPerColor, - m_image.constScanLine(dirtyRect.y())); - m_webImageDataArray["data"].call<void>("set", imageMemory); - } 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 r = 0; r < dirtyRect.height(); ++r) { - auto scanlineMemory = emscripten::typed_memory_view( - dirtyRect.width() * 4, - m_image.constScanLine(r) + BytesPerColor * dirtyRect.x()); - m_webImageDataArray["data"].call<void>("set", scanlineMemory, - (r * dirtyRect.width() + dirtyRect.x()) - * BytesPerColor); - } - } - } + for (const QRect &dirtyRect : clippedDpiScaledRegion) + dom::drawImageToWebImageDataArray(m_image, m_webImageDataArray, dirtyRect); m_dirty = QRegion(); } diff --git a/src/plugins/platforms/wasm/qwasmbase64iconstore.h b/src/plugins/platforms/wasm/qwasmbase64iconstore.h index 6150ea19da..89704f2d2c 100644 --- a/src/plugins/platforms/wasm/qwasmbase64iconstore.h +++ b/src/plugins/platforms/wasm/qwasmbase64iconstore.h @@ -4,6 +4,7 @@ #ifndef QWASMBASE64IMAGESTORE_H #define QWASMBASE64IMAGESTORE_H +#include <string> #include <string_view> #include <QtCore/qtconfigmacros.h> diff --git a/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index c33f39d470..1aa3ffa5b3 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -27,10 +27,10 @@ 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"), - _mimes->text().toJsString()); + _mimes->text().toEcmaString()); } if (_mimes->hasHtml()) { - event["clipboardData"].call<void>("setData", val("text/html"), _mimes->html().toJsString()); + event["clipboardData"].call<void>("setData", val("text/html"), _mimes->html().toEcmaString()); } for (auto mimetype : _mimes->formats()) { @@ -38,7 +38,7 @@ static void commonCopyEvent(val event) continue; QByteArray ba = _mimes->data(mimetype); if (!ba.isEmpty()) - event["clipboardData"].call<void>("setData", mimetype.toJsString(), + event["clipboardData"].call<void>("setData", mimetype.toEcmaString(), val(ba.constData())); } @@ -70,25 +70,7 @@ static void qClipboardPasteTo(val event) { event.call<void>("preventDefault"); // prevent browser from handling drop event - static std::shared_ptr<qstdweb::CancellationFlag> readDataCancellation = nullptr; - readDataCancellation = qstdweb::readDataTransfer( - event["clipboardData"], - [](QByteArray fileContent) { - QImage image; - image.loadFromData(fileContent, nullptr); - return image; - }, - [event](std::unique_ptr<QMimeData> data) { - if (data->formats().isEmpty()) - return; - - // Persist clipboard data so that the app can read it when handling the CTRL+V - QWasmIntegration::get()->clipboard()->QPlatformClipboard::setMimeData( - data.release(), QClipboard::Clipboard); - - QWindowSystemInterface::handleKeyEvent(0, QEvent::KeyPress, Qt::Key_V, - Qt::ControlModifier, "V"); - }); + QWasmIntegration::get()->getWasmClipboard()->sendClipboardData(event); } EMSCRIPTEN_BINDINGS(qtClipboardModule) { @@ -261,12 +243,12 @@ void QWasmClipboard::writeToClipboardApi() // we have a blob, now create a ClipboardItem emscripten::val type = emscripten::val::array(); - type.set("type", mimetype.toJsString()); + type.set("type", mimetype.toEcmaString()); emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type); emscripten::val clipboardItemObject = emscripten::val::object(); - clipboardItemObject.set(mimetype.toJsString(), contentBlob); + clipboardItemObject.set(mimetype.toEcmaString(), contentBlob); val clipboardItemData = val::global("ClipboardItem").new_(clipboardItemObject); @@ -300,4 +282,23 @@ void QWasmClipboard::writeToClipboard() 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 4a21252c8e..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> @@ -37,6 +38,7 @@ public: ProcessKeyboardResult processKeyboard(const KeyEvent &event); static void installEventHandlers(const emscripten::val &target); bool hasClipboardApi(); + void sendClipboardData(emscripten::val event); private: void initClipboardPermissions(); diff --git a/src/plugins/platforms/wasm/qwasmcompositor.cpp b/src/plugins/platforms/wasm/qwasmcompositor.cpp index 922e802732..ef460f666f 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.cpp +++ b/src/plugins/platforms/wasm/qwasmcompositor.cpp @@ -3,43 +3,18 @@ #include "qwasmcompositor.h" #include "qwasmwindow.h" -#include "qwasmeventdispatcher.h" -#include "qwasmclipboard.h" -#include "qwasmevent.h" -#include <QtGui/private/qwindow_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> - -namespace { -QWasmWindow *asWasmWindow(QWindow *window) -{ - return static_cast<QWasmWindow*>(window->handle()); -} - -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 +#include <emscripten/html5.h> using namespace emscripten; -Q_GUI_EXPORT int qt_defaultDpiX(); +bool QWasmCompositor::m_requestUpdateHoldEnabled = true; -QWasmCompositor::QWasmCompositor(QWasmScreen *screen) - : QObject(screen), m_windowStack(std::bind(&QWasmCompositor::onTopWindowChanged, this)) +QWasmCompositor::QWasmCompositor(QWasmScreen *screen) : QObject(screen) { QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } @@ -49,112 +24,35 @@ QWasmCompositor::~QWasmCompositor() if (m_requestAnimationFrameId != -1) emscripten_cancel_animation_frame(m_requestAnimationFrameId); - destroy(); -} - -void QWasmCompositor::onScreenDeleting() -{ - deregisterEventHandlers(); -} - -void QWasmCompositor::deregisterEventHandlers() -{ - QByteArray screenElementSelector = screen()->eventTargetId().toUtf8(); - - emscripten_set_touchstart_callback(screenElementSelector.constData(), 0, 0, NULL); - emscripten_set_touchend_callback(screenElementSelector.constData(), 0, 0, NULL); - emscripten_set_touchmove_callback(screenElementSelector.constData(), 0, 0, NULL); - - emscripten_set_touchcancel_callback(screenElementSelector.constData(), 0, 0, NULL); -} - -void QWasmCompositor::destroy() -{ // 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::addWindow(QWasmWindow *window) -{ - if (m_windowStack.empty()) - window->window()->setFlag(Qt::WindowStaysOnBottomHint); - m_windowStack.pushWindow(window, positionPreferenceFromWindowFlags(window->window()->flags())); - window->requestActivateWindow(); - setActive(window); - - updateEnabledState(); -} - -void QWasmCompositor::removeWindow(QWasmWindow *window) -{ - m_requestUpdateWindows.remove(window); - m_windowStack.removeWindow(window); - if (m_windowStack.topWindow()) { - m_windowStack.topWindow()->requestActivateWindow(); - setActive(m_windowStack.topWindow()); - } - - updateEnabledState(); -} - -void QWasmCompositor::setActive(QWasmWindow *window) +void QWasmCompositor::onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, + QWasmWindow *window) { - m_activeWindow = window; - - auto it = m_windowStack.begin(); - if (it == m_windowStack.end()) { - return; - } - for (; it != m_windowStack.end(); ++it) { - (*it)->onActivationChanged(*it == m_activeWindow); - } + 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::updateEnabledState() +void QWasmCompositor::setEnabled(bool enabled) { - m_isEnabled = std::any_of(m_windowStack.begin(), m_windowStack.end(), [](QWasmWindow *window) { - return !window->context2d().isUndefined(); - }); + m_isEnabled = enabled; } -void QWasmCompositor::raise(QWasmWindow *window) +// 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() { - m_windowStack.raise(window); -} - -void QWasmCompositor::lower(QWasmWindow *window) -{ - m_windowStack.lower(window); -} - -void QWasmCompositor::windowPositionPreferenceChanged(QWasmWindow *window, Qt::WindowFlags flags) -{ - m_windowStack.windowPositionPreferenceChanged(window, positionPreferenceFromWindowFlags(flags)); -} - -QWindow *QWasmCompositor::windowAt(QPoint targetPointInScreenCoords, int padding) const -{ - const auto found = std::find_if( - m_windowStack.begin(), m_windowStack.end(), - [padding, &targetPointInScreenCoords](const QWasmWindow *window) { - const QRect geometry = window->windowFrameGeometry().adjusted(-padding, -padding, - padding, padding); - - return window->isVisible() && geometry.contains(targetPointInScreenCoords); - }); - return found != m_windowStack.end() ? (*found)->window() : nullptr; -} - -QWindow *QWasmCompositor::keyWindow() const -{ - return m_activeWindow ? m_activeWindow->window() : nullptr; -} - -void QWasmCompositor::requestUpdateAllWindows() -{ - m_requestUpdateAllWindows = true; - requestUpdate(); + const bool wasEnabled = m_requestUpdateHoldEnabled; + m_requestUpdateHoldEnabled = false; + return wasEnabled; } void QWasmCompositor::requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType) @@ -178,6 +76,9 @@ void QWasmCompositor::requestUpdate() if (m_requestAnimationFrameId != -1) return; + if (m_requestUpdateHoldEnabled) + return; + static auto frame = [](double frameTime, void *context) -> int { Q_UNUSED(frameTime); @@ -198,40 +99,40 @@ 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. 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; - frame(requestUpdateAllWindows, requestUpdateWindows.keys()); + 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( - qwindow, QRect(QPoint(0, 0), qwindow->geometry().size())); + QWindowSystemInterface::handleExposeEvent(qwindow, updateRect); } } @@ -240,33 +141,16 @@ 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. if (!m_inDeliverUpdateRequest) - requestUpdateWindow(asWasmWindow(window)); + requestUpdateWindow(static_cast<QWasmWindow *>(window->handle())); } -int dpiScaled(qreal value) +void QWasmCompositor::frame(const QList<QWasmWindow *> &windows) { - return value * (qreal(qt_defaultDpiX()) / 96.0); -} - -void QWasmCompositor::frame(bool all, const QList<QWasmWindow *> &windows) -{ - if (!m_isEnabled || m_windowStack.empty() || !screen()) + if (!m_isEnabled || !screen()) return; - if (all) { - std::for_each(m_windowStack.rbegin(), m_windowStack.rend(), - [](QWasmWindow *window) { window->paint(); }); - } else { - std::for_each(windows.begin(), windows.end(), [](QWasmWindow *window) { window->paint(); }); - } -} - -void QWasmCompositor::onTopWindowChanged() -{ - constexpr int zOrderForElementInFrontOfScreen = 3; - int z = zOrderForElementInFrontOfScreen; - std::for_each(m_windowStack.rbegin(), m_windowStack.rend(), - [&z](QWasmWindow *window) { window->setZOrder(z++); }); + for (QWasmWindow *window : windows) + window->paint(); } QWasmScreen *QWasmCompositor::screen() diff --git a/src/plugins/platforms/wasm/qwasmcompositor.h b/src/plugins/platforms/wasm/qwasmcompositor.h index 617d14eece..4953d65233 100644 --- a/src/plugins/platforms/wasm/qwasmcompositor.h +++ b/src/plugins/platforms/wasm/qwasmcompositor.h @@ -7,21 +7,15 @@ #include "qwasmwindowstack.h" #include <qpa/qplatformwindow.h> -#include <QMap> - -#include <QtGui/qinputdevice.h> -#include <QtCore/private/qstdweb_p.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; + +enum class QWasmWindowTreeNodeChangeType; class QWasmCompositor final : public QObject { @@ -30,54 +24,35 @@ public: QWasmCompositor(QWasmScreen *screen); ~QWasmCompositor() final; - void addWindow(QWasmWindow *window); - void removeWindow(QWasmWindow *window); - void setVisible(QWasmWindow *window, bool visible); - void setActive(QWasmWindow *window); - void raise(QWasmWindow *window); - void lower(QWasmWindow *window); - void windowPositionPreferenceChanged(QWasmWindow *window, Qt::WindowFlags flags); void onScreenDeleting(); - QWindow *windowAt(QPoint globalPoint, int padding = 0) const; - QWindow *keyWindow() const; - QWasmScreen *screen(); + void setEnabled(bool enabled); + + static bool releaseRequestUpdateHold(); + void requestUpdate(); enum UpdateRequestDeliveryType { ExposeEventDelivery, UpdateRequestDelivery }; - void requestUpdateAllWindows(); void requestUpdateWindow(QWasmWindow *window, UpdateRequestDeliveryType updateType = ExposeEventDelivery); void handleBackingStoreFlush(QWindow *window); + void onWindowTreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindow *window); private: - void frame(bool all, const QList<QWasmWindow *> &windows); - - void onTopWindowChanged(); + void frame(const QList<QWasmWindow *> &windows); void deregisterEventHandlers(); - void destroy(); - void requestUpdate(); void deliverUpdateRequests(); void deliverUpdateRequest(QWasmWindow *window, UpdateRequestDeliveryType updateType); - static int touchCallback(int eventType, const EmscriptenTouchEvent *ev, void *userData); - - bool processTouch(int eventType, const EmscriptenTouchEvent *touchEvent); - - void updateEnabledState(); - - QWasmWindowStack m_windowStack; - QWasmWindow *m_activeWindow = nullptr; - bool m_isEnabled = true; QMap<QWasmWindow *, UpdateRequestDeliveryType> m_requestUpdateWindows; - bool m_requestUpdateAllWindows = false; int m_requestAnimationFrameId = -1; bool m_inDeliverUpdateRequest = false; + static bool m_requestUpdateHoldEnabled; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmcssstyle.cpp b/src/plugins/platforms/wasm/qwasmcssstyle.cpp index 6ac4c8d884..e0e1a99f48 100644 --- a/src/plugins/platforms/wasm/qwasmcssstyle.cpp +++ b/src/plugins/platforms/wasm/qwasmcssstyle.cpp @@ -35,6 +35,11 @@ const char *Style = R"css( background-color: lightgray; } +.qt-window-contents { + overflow: hidden; + position: relative; +} + .qt-window.transparent-for-input { pointer-events: none; } @@ -135,7 +140,7 @@ const char *Style = R"css( padding-bottom: 4px; } -.qt-window.has-border .title-bar { +.qt-window.has-border > .title-bar { display: flex; } diff --git a/src/plugins/platforms/wasm/qwasmcursor.cpp b/src/plugins/platforms/wasm/qwasmcursor.cpp index b341a31b0c..c258befa77 100644 --- a/src/plugins/platforms/wasm/qwasmcursor.cpp +++ b/src/plugins/platforms/wasm/qwasmcursor.cpp @@ -5,7 +5,9 @@ #include "qwasmscreen.h" #include "qwasmwindow.h" +#include <QtCore/qbuffer.h> #include <QtCore/qdebug.h> +#include <QtCore/qstring.h> #include <QtGui/qwindow.h> #include <emscripten/emscripten.h> @@ -15,10 +17,9 @@ QT_BEGIN_NAMESPACE using namespace emscripten; namespace { -QByteArray cursorShapeToCss(Qt::CursorShape shape) +QByteArray cursorToCss(const QCursor *cursor) { - QByteArray cursorName; - + auto shape = cursor->shape(); switch (shape) { case Qt::ArrowCursor: return "default"; @@ -64,8 +65,24 @@ QByteArray cursorShapeToCss(Qt::CursorShape shape) return "default"; case Qt::DragLinkCursor: 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: - static_assert(Qt::BitmapCursor == 24 && Qt::CustomCursor == 25, + static_assert(Qt::CustomCursor == 25, "New cursor type added, handle it"); qWarning() << "QWasmCursor: " << shape << " unsupported"; return "default"; @@ -78,7 +95,7 @@ void QWasmCursor::changeCursor(QCursor *windowCursor, QWindow *window) if (!window) return; if (QWasmWindow *wasmWindow = static_cast<QWasmWindow *>(window->handle())) - wasmWindow->setWindowCursor(windowCursor ? cursorShapeToCss(windowCursor->shape()) : "default"); + wasmWindow->setWindowCursor(windowCursor ? cursorToCss(windowCursor) : "default"); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmdom.cpp b/src/plugins/platforms/wasm/qwasmdom.cpp index 9aca102b2e..6b2b3d0933 100644 --- a/src/plugins/platforms/wasm/qwasmdom.cpp +++ b/src/plugins/platforms/wasm/qwasmdom.cpp @@ -3,17 +3,239 @@ #include "qwasmdom.h" -#include <QMimeData> +#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) { @@ -36,6 +258,46 @@ QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF & 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 index 074eea7061..0a520815a3 100644 --- a/src/plugins/platforms/wasm/qwasmdom.h +++ b/src/plugins/platforms/wasm/qwasmdom.h @@ -5,6 +5,9 @@ #define QWASMDOM_H #include <QtCore/qtconfigmacros.h> +#include <QtCore/QPointF> +#include <private/qstdweb_p.h> +#include <QtCore/qnamespace.h> #include <emscripten/val.h> @@ -12,11 +15,37 @@ #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"); @@ -25,6 +54,9 @@ inline emscripten::val 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 diff --git a/src/plugins/platforms/wasm/qwasmdrag.cpp b/src/plugins/platforms/wasm/qwasmdrag.cpp new file mode 100644 index 0000000000..d07a46618f --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdrag.cpp @@ -0,0 +1,291 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "qwasmdrag.h" + +#include "qwasmbase64iconstore.h" +#include "qwasmdom.h" +#include "qwasmevent.h" +#include "qwasmintegration.h" + +#include <qpa/qwindowsysteminterface.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 + +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 + +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() +{ + return static_cast<QWasmDrag *>(QWasmIntegration::get()->drag()); +} + +Qt::DropAction QWasmDrag::drag(QDrag *drag) +{ + 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(); + } + + if (dragResult == Qt::IgnoreAction) + dragResult = QBasicDrag::drag(drag); + + return dragResult; +} + +void QWasmDrag::onNativeDragStarted(DragEvent *event) +{ + 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; + } + + 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()); +} + +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); + } +} + +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); +} + +emscripten::val QWasmDrag::DragState::DragImage::generateDragImage(const QPixmap &pixmap, + const QMimeData *mimeData) +{ + if (!pixmap.isNull()) + return generateDragImageFromPixmap(pixmap); + if (mimeData->hasFormat("text/plain")) + return generateDragImageFromText(mimeData); + return generateDefaultDragImage(); +} + +emscripten::val +QWasmDrag::DragState::DragImage::generateDragImageFromText(const QMimeData *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; +} + +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; +} + +emscripten::val QWasmDrag::DragState::DragImage::generateDragImageFromPixmap(const QPixmap &pixmap) +{ + 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; +} + +emscripten::val QWasmDrag::DragState::DragImage::htmlElement() +{ + return m_imageDomElement; +} + +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 new file mode 100644 index 0000000000..146a69ebe8 --- /dev/null +++ b/src/plugins/platforms/wasm/qwasmdrag.h @@ -0,0 +1,47 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QWINDOWSDRAG_H +#define QWINDOWSDRAG_H + +#include <private/qstdweb_p.h> +#include <private/qsimpledrag_p.h> + +#include <qpa/qplatformdrag.h> +#include <QtGui/qdrag.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +struct DragEvent; + +class QWasmDrag final : public QSimpleDrag +{ +public: + 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 onNativeDragOver(DragEvent *event); + void onNativeDrop(DragEvent *event); + void onNativeDragStarted(DragEvent *event); + void onNativeDragFinished(DragEvent *event); + + // QPlatformDrag: + Qt::DropAction drag(QDrag *drag) final; + +private: + struct DragState; + + std::unique_ptr<DragState> m_dragState; +}; + +QT_END_NAMESPACE + +#endif // QWINDOWSDRAG_H diff --git a/src/plugins/platforms/wasm/qwasmevent.cpp b/src/plugins/platforms/wasm/qwasmevent.cpp index 6353cce48f..c1d6ce3a2a 100644 --- a/src/plugins/platforms/wasm/qwasmevent.cpp +++ b/src/plugins/platforms/wasm/qwasmevent.cpp @@ -86,7 +86,10 @@ QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( } } // namespace KeyboardModifier -Event::Event(EventType type, emscripten::val target) : type(type), target(target) { } +Event::Event(EventType type, emscripten::val webEvent) + : webEvent(webEvent), type(type) +{ +} Event::~Event() = default; @@ -98,7 +101,7 @@ Event &Event::operator=(const Event &other) = default; Event &Event::operator=(Event &&other) = default; -KeyEvent::KeyEvent(EventType type, emscripten::val event) : Event(type, event["target"]) +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>(); @@ -110,6 +113,9 @@ KeyEvent::KeyEvent(EventType type, emscripten::val event) : Event(type, event["t text = QString::fromUtf8(webKey); if (text.size() > 1) text.clear(); + + if (key == Qt::Key_Tab) + text = "\t"; } KeyEvent::~KeyEvent() = default; @@ -143,7 +149,7 @@ std::optional<KeyEvent> KeyEvent::fromWebWithDeadKeyTranslation(emscripten::val return result; } -MouseEvent::MouseEvent(EventType type, emscripten::val event) : Event(type, event["target"]) +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>()); @@ -177,11 +183,17 @@ PointerEvent::PointerEvent(EventType type, emscripten::val event) : MouseEvent(t 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>(); } @@ -218,8 +230,8 @@ std::optional<PointerEvent> PointerEvent::fromWeb(emscripten::val event) return PointerEvent(*eventType, event); } -DragEvent::DragEvent(EventType type, emscripten::val event) - : MouseEvent(type, event), dataTransfer(event["dataTransfer"]) +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>(); @@ -244,18 +256,42 @@ DragEvent &DragEvent::operator=(const DragEvent &other) = default; DragEvent &DragEvent::operator=(DragEvent &&other) = default; -std::optional<DragEvent> DragEvent::fromWeb(emscripten::val event) +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); + 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) diff --git a/src/plugins/platforms/wasm/qwasmevent.h b/src/plugins/platforms/wasm/qwasmevent.h index 012b1b235b..6ada5393e3 100644 --- a/src/plugins/platforms/wasm/qwasmevent.h +++ b/src/plugins/platforms/wasm/qwasmevent.h @@ -5,11 +5,12 @@ #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> @@ -18,8 +19,12 @@ QT_BEGIN_NAMESPACE class QWasmDeadKeySupport; +class QWindow; enum class EventType { + DragEnd, + DragOver, + DragStart, Drop, KeyDown, KeyUp, @@ -35,6 +40,7 @@ enum class EventType { enum class PointerType { Mouse, Touch, + Pen, Other, }; @@ -119,15 +125,16 @@ QFlags<Qt::KeyboardModifier> getForEvent<EmscriptenKeyboardEvent>( struct Event { - Event(EventType type, emscripten::val target); + 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 = emscripten::val::undefined(); + emscripten::val target() const { return webEvent["target"]; } }; struct KeyEvent : public Event @@ -214,6 +221,10 @@ struct PointerEvent : public MouseEvent PointerType pointerType; int pointerId; qreal pressure; + qreal tiltX; + qreal tiltY; + qreal tangentialPressure; + qreal twist; qreal width; qreal height; bool isPrimary; @@ -221,17 +232,22 @@ struct PointerEvent : public MouseEvent struct DragEvent : public MouseEvent { - static std::optional<DragEvent> fromWeb(emscripten::val webEvent); + static std::optional<DragEvent> fromWeb(emscripten::val webEvent, QWindow *targetQWindow); - DragEvent(EventType type, emscripten::val webEvent); + 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; - emscripten::val dataTransfer; + dom::DataTransfer dataTransfer; + QWindow *targetWindow; }; struct WheelEvent : public MouseEvent 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/qwasmfontdatabase.cpp b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp index 5fdaae8f84..3f3dc10f71 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.cpp +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.cpp @@ -6,137 +6,315 @@ #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 { - // Load font file from resources. Currently - // all fonts needs to be bundled with the nexe - // as Qt resources. +public: + FontData(val fontData) + :m_fontData(fontData) {} - const QString fontFileNames[] = { - QStringLiteral(":/fonts/DejaVuSansMono.ttf"), - QStringLiteral(":/fonts/Vera.ttf"), - QStringLiteral(":/fonts/DejaVuSans.ttf"), - }; - for (const QString &fontFileName : fontFileNames) { - QFile theFont(fontFileName); - if (!theFont.open(QIODevice::ReadOnly)) - break; + QString family() const + { + return QString::fromStdString(m_fontData["family"].as<std::string>()); + } - QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1()); + QString fullName() const + { + return QString::fromStdString(m_fontData["fullName"].as<std::string>()); } - // check if local-fonts API is available in the browser - val window = val::global("window"); - val fonts = window["queryLocalFonts"]; + QString postscriptName() const + { + return QString::fromStdString(m_fontData["postscriptName"].as<std::string>()); + } - if (fonts.isUndefined()) - return; + 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(); +} - val permissions = val::global("navigator")["permissions"]; - if (permissions["request"].isUndefined()) +void checkFontAccessPermitted(std::function<void(bool)> callback) +{ + const val permissions = val::global("navigator")["permissions"]; + if (permissions.isUndefined()) { + callback(false); return; + } - val requestLocalFontsPermission = val::object(); - requestLocalFontsPermission.set("name", std::string("local-fonts")); - - qstdweb::PromiseCallbacks permissionRequestCallbacks { - .thenFunc = [window](val status) { - qCDebug(lcQpaFonts) << "onFontPermissionSuccess:" - << QString::fromStdString(status["state"].as<std::string>()); - - // query all available local fonts and call registerFontFamily for each of them - qstdweb::Promise::make(window, "queryLocalFonts", { - .thenFunc = [](val status) { - const int count = status["length"].as<int>(); - for (int i = 0; i < count; ++i) { - val font = status.call<val>("at", i); - const std::string family = font["family"].as<std::string>(); - QFreeTypeFontDatabase::registerFontFamily(QString::fromStdString(family)); - } - QWasmFontDatabase::notifyFontsChanged(); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) - << "Error while trying to query local-fonts API"; - } - }); + qstdweb::Promise::make(permissions, "query", { + .thenFunc = [callback](val status) { + callback(status["state"].as<std::string>() == "granted"); }, - .catchFunc = [](val error) { - qCWarning(lcQpaFonts) - << "Error while requesting local-fonts API permission: " - << QString::fromStdString(error["name"].as<std::string>()); - } - }; + }, makeObject("name", "local-fonts")); +} - // request local fonts permission (currently supported only by Chrome 103+) - qstdweb::Promise::make(permissions, "request", std::move(permissionRequestCallbacks), std::move(requestLocalFontsPermission)); +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 QWasmFontDatabase::populateFamily(const QString &familyName) +void readBlob(val blob, std::function<void(const QByteArray &)> callback) { - val window = val::global("window"); + qstdweb::Promise::make(blob, "arrayBuffer", { + .thenFunc = [callback](emscripten::val fontArrayBuffer) { + QByteArray fontData = qstdweb::Uint8Array(qstdweb::ArrayBuffer(fontArrayBuffer)).copyToQByteArray(); + callback(fontData); + }, + .catchFunc = printError + }); +} - auto queryFontsArgument = val::array(std::vector<val>({ val(familyName.toStdString()) })); - val queryFont = val::object(); - queryFont.set("postscriptNames", std::move(queryFontsArgument)); +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 + }); +} - qstdweb::PromiseCallbacks localFontsQueryCallback { - .thenFunc = [](val status) { - val font = status.call<val>("at", 0); +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; +}; - if (font.isUndefined()) - return; +} // namespace - qstdweb::PromiseCallbacks blobQueryCallback { - .thenFunc = [](val status) { - qCDebug(lcQpaFonts) << "onBlobQuerySuccess"; +QWasmFontDatabase::QWasmFontDatabase() +:QFreeTypeFontDatabase() +{ + m_localFontsApiSupported = val::global("window")["queryLocalFonts"].isUndefined() == false; + if (m_localFontsApiSupported) + beginFontDatabaseStartupTask(); +} - qstdweb::PromiseCallbacks arrayBufferCallback { - .thenFunc = [](val status) { - qCDebug(lcQpaFonts) << "onArrayBuffer" ; +QWasmFontDatabase *QWasmFontDatabase::get() +{ + return static_cast<QWasmFontDatabase *>(QWasmIntegration::get()->fontDatabase()); +} - QByteArray fontByteArray = QByteArray::fromEcmaUint8Array(status); +// 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() +{ + // 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; + } - QFreeTypeFontDatabase::addTTFile(fontByteArray, QByteArray()); + selectedLocalFontFamilies += m_extraLocalFontFamilies; - QWasmFontDatabase::notifyFontsChanged(); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) << "onArrayBufferError"; - } - }; + if (selectedLocalFontFamilies.isEmpty() && !allFamilies) { + endAllFontFileLoading(); + return; + } - qstdweb::Promise::make(status, "arrayBuffer", std::move(arrayBufferCallback)); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) << "onBlobQueryError"; - } - }; + populateLocalFontFamilies(selectedLocalFontFamilies, allFamilies); +} - qstdweb::Promise::make(font, "blob", std::move(blobQueryCallback)); - }, - .catchFunc = [](val) { - qCWarning(lcQpaFonts) << "onLocalFontsQueryError"; +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/DejaVuSans.ttf"), }; + for (const QString &fontFileName : fontFileNames) { + QFile theFont(fontFileName); + if (!theFont.open(QIODevice::ReadOnly)) + break; + + QFreeTypeFontDatabase::addTTFile(theFont.readAll(), fontFileName.toLatin1()); + } - qstdweb::Promise::make(window, "queryLocalFonts", std::move(localFontsQueryCallback), std::move(queryFont)); + // 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, @@ -146,9 +324,9 @@ QStringList QWasmFontDatabase::fallbacksForFamily(const QString &family, QFont:: QStringList fallbacks = QFreeTypeFontDatabase::fallbacksForFamily(family, style, styleHint, script); - // Add the vera.ttf and DejaVuSans.ttf fonts (loaded in populateFontDatabase above) as falback fonts + // Add the DejaVuSans.ttf font (loaded in populateFontDatabase above) as a falback font // to all other fonts (except itself). - static const QString wasmFallbackFonts[] = { "Bitstream Vera Sans", "DejaVu Sans" }; + static const QString wasmFallbackFonts[] = { "DejaVu Sans" }; for (auto wasmFallbackFont : wasmFallbackFonts) { if (family != wasmFallbackFont && !fallbacks.contains(wasmFallbackFont)) fallbacks.append(wasmFallbackFont); @@ -164,13 +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(); + } } -void QWasmFontDatabase::notifyFontsChanged() +// Unconditionally ends local font loading, for instance if there +// are no fonts to load or if there was an unexpected error. +void QWasmFontDatabase::endAllFontFileLoading() { - QFontCache::instance()->clear(); - emit qGuiApp->fontDatabaseChanged(); + 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 22c550f244..a1c8f1ff48 100644 --- a/src/plugins/platforms/wasm/qwasmfontdatabase.h +++ b/src/plugins/platforms/wasm/qwasmfontdatabase.h @@ -6,13 +6,17 @@ #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; - void populateFamily(const QString &familyName) override; QFontEngine *fontEngine(const QFontDef &fontDef, void *handle) override; QStringList fallbacksForFamily(const QString &family, QFont::Style style, QFont::StyleHint styleHint, @@ -20,7 +24,26 @@ public: void releaseHandle(void *handle) override; QFont defaultFont() const override; - static void notifyFontsChanged(); + 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 157f96fe49..ae72e7b7f9 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.cpp +++ b/src/plugins/platforms/wasm/qwasminputcontext.cpp @@ -30,7 +30,7 @@ static void inputCallback(emscripten::val event) QString str = QString::fromStdString(_incomingCharVal.as<std::string>()); QWasmInputContext *wasmInput = reinterpret_cast<QWasmInputContext*>(event["target"]["data-qinputcontext"].as<quintptr>()); - wasmInput->inputStringChanged(str, wasmInput); + wasmInput->inputStringChanged(str, EMSCRIPTEN_EVENT_KEYDOWN, wasmInput); } // this clears the input string, so backspaces do not send a character // but stops suggestions @@ -49,29 +49,24 @@ QWasmInputContext::QWasmInputContext() m_inputElement.set("style", "position:absolute;left:-1000px;top:-1000px"); // offscreen m_inputElement.set("contenteditable","true"); - if (platform() == Platform::Android || platform() == Platform::Windows) { + 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)); + 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); - - // Enter is sent through target window, let's just handle this here - emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, (void *)this, 1, - &androidKeyboardCallback); - - } - - if (platform() == Platform::MacOS || platform() == Platform::iPhone) { - auto callback = [=](emscripten::val) { - m_inputElement["parentElement"].call<void>("removeChild", m_inputElement); - inputPanelIsOpen = false; - }; - m_blurEventHandler.reset(new EventCallback(m_inputElement, "blur", callback)); } QObject::connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, @@ -118,7 +113,7 @@ void QWasmInputContext::showInputPanel() // screen element. if (platform() == Platform::MacOS // keep for compatibility - || platform() == Platform::iPhone + || platform() == Platform::iOS || platform() == Platform::Windows) { emscripten::val inputWrapper = inputHandlerElementForFocusedWindow(); if (inputWrapper.isUndefined()) @@ -138,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); + Qt::Key thisKey = keys[0].key(); + // 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; + } + QWindowSystemInterface::handleKeyEvent( - 0, QEvent::KeyPress,keys[0].key(), keys[0].keyboardModifiers(), inputString); + 0, eventType == EMSCRIPTEN_EVENT_KEYDOWN ? QEvent::KeyPress : QEvent::KeyRelease, + thisKey, keys[0].keyboardModifiers(), + eventType == EMSCRIPTEN_EVENT_KEYDOWN ? inputString : QStringLiteral("")); } -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); - - return true; -} QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasminputcontext.h b/src/plugins/platforms/wasm/qwasminputcontext.h index 086599bfb0..10dd1a0950 100644 --- a/src/plugins/platforms/wasm/qwasminputcontext.h +++ b/src/plugins/platforms/wasm/qwasminputcontext.h @@ -29,7 +29,8 @@ public: bool isValid() const override { return true; } void focusWindowChanged(QWindow *focusWindow); - void inputStringChanged(QString &, QWasmInputContext *context); + void inputStringChanged(QString &, int eventType, QWasmInputContext *context); + emscripten::val m_inputElement = emscripten::val::null(); private: emscripten::val inputHandlerElementForFocusedWindow(); @@ -37,11 +38,8 @@ private: 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 2bdb3b7c69..f5cc3e2eee 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.cpp +++ b/src/plugins/platforms/wasm/qwasmintegration.cpp @@ -14,14 +14,14 @@ #include "qwasmwindow.h" #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> @@ -38,14 +38,19 @@ 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) @@ -64,22 +69,31 @@ 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_accessibility(new QWasmAccessibility) + : m_fontDb(nullptr) + , m_desktopServices(nullptr) + , m_clipboard(new QWasmClipboard) +#if QT_CONFIG(accessibility) + , m_accessibility(new QWasmAccessibility) +#endif { s_instance = this; @@ -87,11 +101,13 @@ QWasmIntegration::QWasmIntegration() 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 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"); if (qtContainerElements.isArray()) { for (int i = 0; i < qtContainerElements["length"].as<int>(); ++i) { @@ -99,13 +115,14 @@ QWasmIntegration::QWasmIntegration() if (element.isNull() || element.isUndefined()) qWarning() << "Skipping null or undefined element in qtContainerElements"; else - addScreen(element); + filtered.call<void>("push", element); } } else { // No screens, which may or may not be intended qWarning() << "The qtContainerElements module property was not set or is invalid. " "Proceeding with no screens."; } + setContainerElements(filtered); // install browser window resize handler emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, @@ -125,7 +142,7 @@ QWasmIntegration::QWasmIntegration() visualViewport.call<void>("addEventListener", val("resize"), val::module_property("qtResizeAllScreens")); } - m_drag = std::make_unique<QSimpleDrag>(); + m_drag = std::make_unique<QWasmDrag>(); } QWasmIntegration::~QWasmIntegration() @@ -142,10 +159,12 @@ QWasmIntegration::~QWasmIntegration() delete m_desktopServices; if (m_platformInputContext) delete m_platformInputContext; +#if QT_CONFIG(accessibility) delete m_accessibility; +#endif for (const auto &elementAndScreen : m_screens) - elementAndScreen.second->deleteScreen(); + elementAndScreen.wasmScreen->deleteScreen(); m_screens.clear(); @@ -187,6 +206,16 @@ 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 { @@ -196,12 +225,12 @@ QPlatformOpenGLContext *QWasmIntegration::createPlatformOpenGLContext(QOpenGLCon void QWasmIntegration::initialize() { - if (qgetenv("QT_IM_MODULE").isEmpty() && touchPoints < 1) + 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()); } @@ -281,37 +310,93 @@ QPlatformAccessibility *QWasmIntegration::accessibility() const } #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(); }); -void QWasmIntegration::addScreen(const emscripten::val &element) + 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)); 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" - << QString::fromJsString(element["id"]); + qWarning() << "Attempt to remove a nonexistent screen."; return; } - it->second->deleteScreen(); - m_screens.erase(it); + + 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" - << QString::fromJsString(element["id"]); + << QString::fromEcmaString(element["id"]); return; } - it->second->updateQScreenAndCanvasRenderSize(); + it->wasmScreen->updateQScreenAndCanvasRenderSize(); } void QWasmIntegration::updateDpi() @@ -321,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() diff --git a/src/plugins/platforms/wasm/qwasmintegration.h b/src/plugins/platforms/wasm/qwasmintegration.h index decf25009e..870bd0d16b 100644 --- a/src/plugins/platforms/wasm/qwasmintegration.h +++ b/src/plugins/platforms/wasm/qwasmintegration.h @@ -14,7 +14,6 @@ #include <QtCore/qhash.h> -#include <private/qsimpledrag_p.h> #include <private/qstdweb_p.h> #include <emscripten.h> @@ -33,6 +32,7 @@ class QWasmBackingStore; class QWasmClipboard; class QWasmAccessibility; class QWasmServices; +class QWasmDrag; class QWasmIntegration : public QObject, public QPlatformIntegration { @@ -70,21 +70,29 @@ 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(); 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; @@ -95,7 +103,7 @@ private: mutable QWasmInputContext *m_platformInputContext = nullptr; #if QT_CONFIG(draganddrop) - std::unique_ptr<QSimpleDrag> m_drag; + std::unique_ptr<QWasmDrag> m_drag; #endif }; diff --git a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp index 0191e0b216..dcfc4433e6 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.cpp @@ -27,4 +27,9 @@ 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 38a991f4ce..1c71310448 100644 --- a/src/plugins/platforms/wasm/qwasmoffscreensurface.h +++ b/src/plugins/platforms/wasm/qwasmoffscreensurface.h @@ -20,6 +20,7 @@ public: ~QWasmOffscreenSurface() final; const std::string &id() const { return m_specialTargetId; } + bool isValid() const override; private: std::string m_specialTargetId; diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp index fe92f34eca..8a4664ec8c 100644 --- a/src/plugins/platforms/wasm/qwasmopenglcontext.cpp +++ b/src/plugins/platforms/wasm/qwasmopenglcontext.cpp @@ -21,16 +21,16 @@ EMSCRIPTEN_BINDINGS(qwasmopenglcontext) QT_BEGIN_NAMESPACE QWasmOpenGLContext::QWasmOpenGLContext(QOpenGLContext *context) - : m_requestedFormat(context->format()), m_qGlContext(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 (m_requestedFormat.depthBufferSize() < 0 && m_requestedFormat.stencilBufferSize() > 0) - m_requestedFormat.setDepthBufferSize(16); + if (m_actualFormat.depthBufferSize() < 0 && m_actualFormat.stencilBufferSize() > 0) + m_actualFormat.setDepthBufferSize(16); - if (m_requestedFormat.stencilBufferSize() < 0 && m_requestedFormat.depthBufferSize() > 0) - m_requestedFormat.setStencilBufferSize(8); + if (m_actualFormat.stencilBufferSize() < 0 && m_actualFormat.depthBufferSize() > 0) + m_actualFormat.setStencilBufferSize(8); } QWasmOpenGLContext::~QWasmOpenGLContext() @@ -58,28 +58,19 @@ QWasmOpenGLContext::obtainEmscriptenContext(QPlatformSurface *surface) return m_ownedWebGLContext.handle; if (surface->surface()->surfaceClass() == QSurface::Offscreen) { - if (const auto *shareContext = m_qGlContext->shareContext()) { - // Since there are no resource sharing capabilities with WebGL whatsoever, we use the - // same actual underlaying WebGL context. This is not perfect, but it works in most - // cases. - return static_cast<QWasmOpenGLContext *>(shareContext->handle()) - ->m_ownedWebGLContext.handle; - } else { - // 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_requestedFormat) }; + // 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); @@ -87,11 +78,23 @@ QWasmOpenGLContext::obtainEmscriptenContext(QPlatformSurface *surface) m_ownedWebGLContext = QOpenGLContextData{ .surface = surface, .handle = createEmscriptenContext(static_cast<QWasmWindow *>(surface)->canvasSelector(), - m_requestedFormat) + m_actualFormat) }; + } - return m_ownedWebGLContext.handle; + 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; } void QWasmOpenGLContext::destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle) @@ -116,9 +119,8 @@ QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector, 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 const bool useDepthStencil = (format.depthBufferSize() > 0 || format.stencilBufferSize() > 0); @@ -127,13 +129,20 @@ QWasmOpenGLContext::createEmscriptenContext(const std::string &canvasSelector, attributes.alpha = format.alphaBufferSize() > 0; attributes.depth = useDepthStencil; attributes.stencil = useDepthStencil; + EMSCRIPTEN_RESULT contextResult = emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); - return emscripten_webgl_create_context(canvasSelector.c_str(), &attributes); + 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 @@ -143,6 +152,15 @@ GLuint QWasmOpenGLContext::defaultFramebufferObject(QPlatformSurface *surface) c bool QWasmOpenGLContext::makeCurrent(QPlatformSurface *surface) { + 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; @@ -170,7 +188,7 @@ bool QWasmOpenGLContext::isSharing() const 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 diff --git a/src/plugins/platforms/wasm/qwasmopenglcontext.h b/src/plugins/platforms/wasm/qwasmopenglcontext.h index 90863abdfe..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> @@ -40,7 +43,7 @@ private: static void destroyWebGLContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE contextHandle); - QSurfaceFormat m_requestedFormat; + QSurfaceFormat m_actualFormat; QOpenGLContext *m_qGlContext; QOpenGLContextData m_ownedWebGLContext; EMSCRIPTEN_WEBGL_CONTEXT_HANDLE m_usedWebGLContextHandle = 0; @@ -48,3 +51,4 @@ private: QT_END_NAMESPACE +#endif // QWASMOPENGLCONTEXT_H diff --git a/src/plugins/platforms/wasm/qwasmplatform.cpp b/src/plugins/platforms/wasm/qwasmplatform.cpp index c641e345e4..e54992be1d 100644 --- a/src/plugins/platforms/wasm/qwasmplatform.cpp +++ b/src/plugins/platforms/wasm/qwasmplatform.cpp @@ -13,8 +13,9 @@ Platform platform() if (rawPlatform.call<bool>("includes", emscripten::val("Mac"))) return Platform::MacOS; - if (rawPlatform.call<bool>("includes", emscripten::val("iPhone"))) - return Platform::iPhone; + 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"))) { diff --git a/src/plugins/platforms/wasm/qwasmplatform.h b/src/plugins/platforms/wasm/qwasmplatform.h index 239efdeae9..5b32e43633 100644 --- a/src/plugins/platforms/wasm/qwasmplatform.h +++ b/src/plugins/platforms/wasm/qwasmplatform.h @@ -19,7 +19,7 @@ enum class Platform { Windows, Linux, Android, - iPhone, + iOS }; Platform platform(); diff --git a/src/plugins/platforms/wasm/qwasmscreen.cpp b/src/plugins/platforms/wasm/qwasmscreen.cpp index 6f8d1509ab..ddf8140c48 100644 --- a/src/plugins/platforms/wasm/qwasmscreen.cpp +++ b/src/plugins/platforms/wasm/qwasmscreen.cpp @@ -28,6 +28,7 @@ const char *QWasmScreen::m_canvasResizeObserverCallbackContextPropertyName = QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) : m_container(containerOrCanvas), + m_intermediateContainer(emscripten::val::undefined()), m_shadowContainer(emscripten::val::undefined()), m_compositor(new QWasmCompositor(this)), m_deadKeySupport(std::make_unique<QWasmDeadKeySupport>()) @@ -43,9 +44,20 @@ QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) m_container["parentNode"].call<void>("replaceChild", container, m_container); m_container = container; } + + // 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_container.call<emscripten::val>("attachShadow", shadowOptions); + auto shadow = m_intermediateContainer.call<emscripten::val>("attachShadow", shadowOptions); m_shadowContainer = document.call<emscripten::val>("createElement", emscripten::val("div")); @@ -80,12 +92,24 @@ QWasmScreen::QWasmScreen(const emscripten::val &containerOrCanvas) 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() { + m_intermediateContainer.call<void>("remove"); + emscripten::val::module_property("specialHTMLTargets") .set(eventTargetId().toStdString(), emscripten::val::undefined()); @@ -95,7 +119,6 @@ QWasmScreen::~QWasmScreen() void QWasmScreen::deleteScreen() { - m_compositor->onScreenDeleting(); // Deletes |this|! QWindowSystemInterface::handleScreenRemoved(this); } @@ -183,7 +206,7 @@ qreal QWasmScreen::devicePixelRatio() const QString QWasmScreen::name() const { - return QString::fromJsString(m_shadowContainer["id"]); + return QString::fromEcmaString(m_shadowContainer["id"]); } QPlatformCursor *QWasmScreen::cursor() const @@ -200,12 +223,18 @@ 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 @@ -233,6 +262,18 @@ void QWasmScreen::setGeometry(const QRect &rect) 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. @@ -261,7 +302,6 @@ void QWasmScreen::updateQScreenAndCanvasRenderSize() }; setGeometry(QRect(getElementBodyPosition(m_shadowContainer), cssSize.toSize())); - m_compositor->requestUpdateAllWindows(); } void QWasmScreen::canvasResizeObserverCallback(emscripten::val entries, emscripten::val) @@ -307,4 +347,27 @@ void QWasmScreen::installCanvasResizeObserver() resizeObserver.call<void>("observe", m_shadowContainer); } +emscripten::val QWasmScreen::containerElement() +{ + return m_shadowContainer; +} + +QWasmWindowTreeNode *QWasmScreen::parentNode() +{ + return nullptr; +} + +QList<QWasmWindow *> QWasmScreen::allWindows() +{ + QList<QWasmWindow *> windows; + for (auto *child : childStack()) { + QWindowList list = child->window()->findChildren<QWindow *>(Qt::FindChildrenRecursively); + std::transform( + list.begin(), list.end(), std::back_inserter(windows), + [](const QWindow *window) { return static_cast<QWasmWindow *>(window->handle()); }); + 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 633cf853f7..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> @@ -23,7 +25,7 @@ class QWasmCompositor; class QWasmDeadKeySupport; class QOpenGLContext; -class QWasmScreen : public QObject, public QPlatformScreen +class QWasmScreen : public QObject, public QPlatformScreen, public QWasmWindowTreeNode { Q_OBJECT public: @@ -37,10 +39,13 @@ public: QString eventTargetId() const; QString outerScreenId() const; QPointingDevice *touchDevice() { return m_touchDevice.get(); } + QPointingDevice *tabletDevice() { return m_tabletDevice.get(); } QWasmCompositor *compositor(); QWasmDeadKeySupport *deadKeySupport() { return m_deadKeySupport.get(); } + QList<QWasmWindow *> allWindows(); + QRect geometry() const override; int depth() const override; QImage::Format format() const override; @@ -53,6 +58,10 @@ 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; @@ -65,10 +74,16 @@ public slots: void setGeometry(const QRect &rect); private: + // QWasmWindowTreeNode: + void onSubtreeChanged(QWasmWindowTreeNodeChangeType changeType, QWasmWindowTreeNode *parent, + QWasmWindow *child) final; + emscripten::val m_container; + emscripten::val m_intermediateContainer; emscripten::val m_shadowContainer; std::unique_ptr<QWasmCompositor> m_compositor; 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; diff --git a/src/plugins/platforms/wasm/qwasmservices.cpp b/src/plugins/platforms/wasm/qwasmservices.cpp index f5fd4e4790..e767295e41 100644 --- a/src/plugins/platforms/wasm/qwasmservices.cpp +++ b/src/plugins/platforms/wasm/qwasmservices.cpp @@ -12,7 +12,7 @@ QT_BEGIN_NAMESPACE bool QWasmServices::openUrl(const QUrl &url) { - emscripten::val::global("window").call<void>("open", url.toString().toJsString(), + emscripten::val::global("window").call<void>("open", url.toString().toEcmaString(), emscripten::val("_blank")); return true; } diff --git a/src/plugins/platforms/wasm/qwasmwindow.cpp b/src/plugins/platforms/wasm/qwasmwindow.cpp index 3772217cdb..b8197c5113 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.cpp +++ b/src/plugins/platforms/wasm/qwasmwindow.cpp @@ -5,6 +5,7 @@ #include <private/qguiapplication_p.h> #include <QtCore/qfile.h> #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <private/qpixmapcache_p.h> #include <QtGui/qopenglfunctions.h> #include <QBuffer> @@ -32,6 +33,17 @@ 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, QWasmDeadKeySupport *deadKeySupport, @@ -56,6 +68,7 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, 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")); @@ -67,9 +80,9 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmClipboard::installEventHandlers(m_canvas); - // set inputmode to none to stop mobile keyboard opening + // set inputMode to none to stop mobile keyboard opening // when user clicks anywhere on the canvas. - m_canvas.set("inputmode", std::string("none")); + 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")); @@ -82,8 +95,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_canvasContainer.call<void>("appendChild", m_a11yContainer); m_a11yContainer["classList"].call<void>("add", emscripten::val("qt-window-a11y-container")); - compositor->screen()->element().call<void>("appendChild", m_qtWindow); - const bool rendersTo2dContext = w->surfaceType() != QSurface::OpenGLSurface; if (rendersTo2dContext) m_context2d = m_canvas.call<emscripten::val>("getContext", emscripten::val("2d")); @@ -92,7 +103,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_qtWindow.set("id", "qt-window-" + std::to_string(m_winId)); emscripten::val::module_property("specialHTMLTargets").set(canvasSelector(), m_canvas); - m_compositor->addWindow(this); m_flags = window()->flags(); const auto pointerCallback = std::function([this](emscripten::val event) { @@ -105,12 +115,6 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, m_pointerLeaveCallback = std::make_unique<qstdweb::EventCallback>(m_qtWindow, "pointerleave", pointerCallback); - m_dropCallback = std::make_unique<qstdweb::EventCallback>( - m_qtWindow, "drop", [this](emscripten::val event) { - if (processDrop(*DragEvent::fromWeb(event))) - event.call<void>("preventDefault"); - }); - m_wheelEventCallback = std::make_unique<qstdweb::EventCallback>( m_qtWindow, "wheel", [this](emscripten::val event) { if (processWheel(*WheelEvent::fromWeb(event))) @@ -120,21 +124,48 @@ QWasmWindow::QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, 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_keyDownCallback = - std::make_unique<qstdweb::EventCallback>(m_qtWindow, "keydown", keyCallback); - m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(m_qtWindow, "keyup", keyCallback); + std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keydown", keyCallback); + m_keyUpCallback = std::make_unique<qstdweb::EventCallback>(keyFocusWindow, "keyup", keyCallback); + + setParent(parent()); } QWasmWindow::~QWasmWindow() { emscripten::val::module_property("specialHTMLTargets").delete_(canvasSelector()); - destroy(); - m_compositor->removeWindow(this); + 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 +} + +QSurfaceFormat QWasmWindow::format() const +{ + return window()->requestedFormat(); +} + +QWasmWindow *QWasmWindow::fromWindow(QWindow *window) +{ + return static_cast<QWasmWindow *>(window->handle()); } void QWasmWindow::onRestoreClicked() @@ -160,15 +191,14 @@ void QWasmWindow::onCloseClicked() void QWasmWindow::onNonClientAreaInteraction() { - if (!isActive()) - requestActivateWindow(); + requestActivateWindow(); QGuiApplicationPrivate::instance()->closeAllPopups(); } bool QWasmWindow::onNonClientEvent(const PointerEvent &event) { QPointF pointInScreen = platformScreen()->mapFromLocal( - dom::mapPoint(event.target, platformScreen()->element(), event.localPoint)); + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); return QWindowSystemInterface::handleMouseEvent( window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), pointInScreen, event.mouseButtons, event.mouseButton, @@ -176,46 +206,27 @@ bool QWasmWindow::onNonClientEvent(const PointerEvent &event) event.modifiers); } -void QWasmWindow::destroy() -{ - m_qtWindow["parentElement"].call<emscripten::val>("removeChild", m_qtWindow); - - m_canvasContainer.call<void>("removeChild", m_canvas); - m_context2d = emscripten::val::undefined(); -} - void QWasmWindow::initialize() { - QRect rect = windowGeometry(); - - const auto windowFlags = window()->flags(); - const bool shouldRestrictMinSize = - !windowFlags.testFlag(Qt::FramelessWindowHint) && !windowIsPopupType(windowFlags); - const bool isMinSizeUninitialized = window()->minimumSize() == QSize(0, 0); - - if (shouldRestrictMinSize && isMinSizeUninitialized) - window()->setMinimumSize(QSize(minSizeForRegularWindows, minSizeForRegularWindows)); - - - const QSize minimumSize = windowMinimumSize(); - const QSize maximumSize = windowMaximumSize(); - const QSize targetSize = !rect.isEmpty() ? rect.size() : minimumSize; - - rect.setWidth(qBound(minimumSize.width(), targetSize.width(), maximumSize.width())); - rect.setHeight(qBound(minimumSize.width(), targetSize.height(), maximumSize.height())); + 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 @@ -254,21 +265,34 @@ void QWasmWindow::setGeometry(const QRect &rect) if (m_state.testFlag(Qt::WindowMaximized)) return platformScreen()->availableGeometry().marginsRemoved(frameMargins()); - const auto screenGeometry = screen()->geometry(); - - QRect result(rect); - result.moveTop(std::max(std::min(rect.y(), screenGeometry.bottom()), - screenGeometry.y() + margins.top())); - result.setSize( - result.size().expandedTo(windowMinimumSize()).boundedTo(windowMaximumSize())); - return result; + 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(-screen()->geometry().topLeft()); + .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"); @@ -306,6 +330,8 @@ void QWasmWindow::setVisible(bool visible) 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(); } @@ -329,13 +355,15 @@ QMargins QWasmWindow::frameMargins() const void QWasmWindow::raise() { - m_compositor->raise(this); + bringToTop(); invalidate(); + if (QWasmIntegration::get()->inputContext()) + m_canvas.call<void>("focus"); } void QWasmWindow::lower() { - m_compositor->lower(this); + sendToBottom(); invalidate(); } @@ -346,12 +374,8 @@ WId QWasmWindow::winId() const 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(); } @@ -372,13 +396,16 @@ void QWasmWindow::onActivationChanged(bool active) void QWasmWindow::setWindowFlags(Qt::WindowFlags flags) { - if (flags.testFlag(Qt::WindowStaysOnTopHint) != m_flags.testFlag(Qt::WindowStaysOnTopHint)) - m_compositor->windowPositionPreferenceChanged(this, 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", flags.testFlag(Qt::WindowTitleHint)); - dom::syncCSSClassWith(m_qtWindow, "frameless", flags.testFlag(Qt::FramelessWindowHint)); + dom::syncCSSClassWith(m_qtWindow, "has-title", hasTitleBar()); dom::syncCSSClassWith(m_qtWindow, "transparent-for-input", flags.testFlag(Qt::WindowTransparentForInput)); @@ -455,6 +482,12 @@ void QWasmWindow::applyWindowState() setGeometry(newGeom); } +void QWasmWindow::commitParent(QWasmWindowTreeNode *parent) +{ + onParentChanged(m_commitedParent, parent, positionPreferenceFromWindowFlags(window()->flags())); + m_commitedParent = parent; +} + bool QWasmWindow::processKey(const KeyEvent &event) { constexpr bool ProceedToNativeEvent = false; @@ -477,13 +510,13 @@ bool QWasmWindow::processKey(const KeyEvent &event) bool QWasmWindow::processPointer(const PointerEvent &event) { - if (event.pointerType != PointerType::Mouse) + if (event.pointerType != PointerType::Mouse && event.pointerType != PointerType::Pen) return false; switch (event.type) { case EventType::PointerEnter: { const auto pointInScreen = platformScreen()->mapFromLocal( - dom::mapPoint(event.target, platformScreen()->element(), event.localPoint)); + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); QWindowSystemInterface::handleEnterEvent( window(), m_window->mapFromGlobal(pointInScreen), pointInScreen); break; @@ -498,30 +531,6 @@ bool QWasmWindow::processPointer(const PointerEvent &event) return false; } -bool QWasmWindow::processDrop(const DragEvent &event) -{ - m_dropDataReadCancellationFlag = qstdweb::readDataTransfer( - event.dataTransfer, - [](QByteArray fileContent) { - QImage image; - image.loadFromData(fileContent, nullptr); - return image; - }, - [this, event](std::unique_ptr<QMimeData> data) { - QWindowSystemInterface::handleDrag(window(), data.get(), - event.pointInPage.toPoint(), event.dropAction, - event.mouseButton, event.modifiers); - - QWindowSystemInterface::handleDrop(window(), data.get(), - event.pointInPage.toPoint(), event.dropAction, - event.mouseButton, event.modifiers); - - QWindowSystemInterface::handleDrag(window(), nullptr, QPoint(), Qt::IgnoreAction, - {}, {}); - }); - return true; -} - bool QWasmWindow::processWheel(const WheelEvent &event) { // Web scroll deltas are inverted from Qt deltas - negate. @@ -537,7 +546,7 @@ bool QWasmWindow::processWheel(const WheelEvent &event) })(); const auto pointInScreen = platformScreen()->mapFromLocal( - dom::mapPoint(event.target, platformScreen()->element(), event.localPoint)); + dom::mapPoint(event.target(), platformScreen()->element(), event.localPoint)); return QWindowSystemInterface::handleWheelEvent( window(), QWasmIntegration::getTimestamp(), window()->mapFromGlobal(pointInScreen), @@ -561,16 +570,25 @@ void QWasmWindow::requestUpdate() m_compositor->requestUpdateWindow(this, QWasmCompositor::UpdateRequestDelivery); } +bool QWasmWindow::hasFrame() const +{ + return !m_flags.testFlag(Qt::FramelessWindowHint); +} + bool QWasmWindow::hasBorder() const { - return !m_state.testFlag(Qt::WindowFullScreen) && !m_flags.testFlag(Qt::FramelessWindowHint) - && !windowIsPopupType(m_flags); + return hasFrame() && !m_state.testFlag(Qt::WindowFullScreen) && !m_flags.testFlag(Qt::SubWindow) + && !windowIsPopupType(m_flags) && !parent(); +} + +bool QWasmWindow::hasTitleBar() const +{ + return hasBorder() && m_flags.testFlag(Qt::WindowTitleHint); } bool QWasmWindow::hasShadow() const { - return !m_flags.testFlag(Qt::NoDropShadowWindowHint) - && !m_flags.testFlag(Qt::FramelessWindowHint); + return hasBorder() && !m_flags.testFlag(Qt::NoDropShadowWindowHint); } bool QWasmWindow::hasMaximizeButton() const @@ -594,10 +612,8 @@ void QWasmWindow::requestActivateWindow() return; } - if (window()->isTopLevel()) { - raise(); - m_compositor->setActive(this); - } + raise(); + setAsActiveNode(); if (!QWasmIntegration::get()->inputContext()) m_canvas.call<void>("focus"); @@ -645,9 +661,41 @@ void QWasmWindow::setMask(const QRegion ®ion) m_qtWindow["style"].set("clipPath", emscripten::val(cssClipPath.str())); } +void QWasmWindow::setParent(const QPlatformWindow *) +{ + commitParent(parentNode()); +} + std::string QWasmWindow::canvasSelector() const { return "!qtwindow" + std::to_string(m_winId); } +emscripten::val QWasmWindow::containerElement() +{ + return m_windowContents; +} + +QWasmWindowTreeNode *QWasmWindow::parentNode() +{ + if (parent()) + return static_cast<QWasmWindow *>(parent()); + return platformScreen(); +} + +QWasmWindow *QWasmWindow::asWasmWindow() +{ + return this; +} + +void QWasmWindow::onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) +{ + 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 661a5160e8..ab0dc68e83 100644 --- a/src/plugins/platforms/wasm/qwasmwindow.h +++ b/src/plugins/platforms/wasm/qwasmwindow.h @@ -6,11 +6,14 @@ #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" @@ -23,28 +26,27 @@ QT_BEGIN_NAMESPACE namespace qstdweb { -struct CancellationFlag; -} - -namespace qstdweb { class EventCallback; } class ClientArea; -struct DragEvent; struct KeyEvent; struct PointerEvent; class QWasmDeadKeySupport; struct WheelEvent; -class QWasmWindow final : public QPlatformWindow +class QWasmWindow final : public QPlatformWindow, + public QWasmWindowTreeNode, + public QNativeInterface::Private::QWasmWindow { public: QWasmWindow(QWindow *w, QWasmDeadKeySupport *deadKeySupport, QWasmCompositor *compositor, QWasmBackingStore *backingStore); ~QWasmWindow() final; - void destroy(); + static QWasmWindow *fromWindow(QWindow *window); + QSurfaceFormat format() const override; + void paint(); void setZOrder(int order); void setWindowCursor(QByteArray cssCursorName); @@ -80,6 +82,7 @@ public: 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; } @@ -87,23 +90,39 @@ public: QWindow *window() const { return m_window; } std::string canvasSelector() const; + 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 minSizeForRegularWindows = 100; + static constexpr auto defaultWindowSize = 160; + + // QWasmWindowTreeNode: + QWasmWindow *asWasmWindow() final; + void onParentChanged(QWasmWindowTreeNode *previous, QWasmWindowTreeNode *current, + QWasmWindowStack::PositionPreference positionPreference) final; void invalidate(); + bool hasFrame() const; + bool hasTitleBar() const; bool hasBorder() const; bool hasShadow() const; bool hasMaximizeButton() const; void applyWindowState(); + void commitParent(QWasmWindowTreeNode *parent); bool processKey(const KeyEvent &event); bool processPointer(const PointerEvent &event); - bool processDrop(const DragEvent &event); bool processWheel(const WheelEvent &event); QWindow *m_window = nullptr; @@ -123,6 +142,8 @@ private: 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; @@ -148,8 +169,6 @@ private: friend class QWasmCompositor; friend class QWasmEventTranslator; bool windowIsPopupType(Qt::WindowFlags flags) const; - - std::shared_ptr<qstdweb::CancellationFlag> m_dropDataReadCancellationFlag; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp index e3b681adb5..6da3e24c05 100644 --- a/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.cpp @@ -7,6 +7,7 @@ #include "qwasmevent.h" #include "qwasmscreen.h" #include "qwasmwindow.h" +#include "qwasmdrag.h" #include <QtGui/private/qguiapplication_p.h> #include <QtGui/qpointingdevice.h> @@ -19,8 +20,9 @@ ClientArea::ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val : m_screen(screen), m_window(window), m_element(element) { const auto callback = std::function([this](emscripten::val event) { - if (processPointer(*PointerEvent::fromWeb(event))) - event.call<void>("preventDefault"); + processPointer(*PointerEvent::fromWeb(event)); + event.call<void>("preventDefault"); + event.call<void>("stopPropagation"); }); m_pointerDownCallback = @@ -30,34 +32,50 @@ ClientArea::ClientArea(QWasmWindow *window, QWasmScreen *screen, emscripten::val 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) { - const auto localScreenPoint = - dom::mapPoint(event.target, m_screen->element(), event.localPoint); - const auto pointInScreen = m_screen->mapFromLocal(localScreenPoint); - - const QPointF pointInTargetWindowCoords = m_window->window()->mapFromGlobal(pointInScreen); switch (event.type) { case EventType::PointerDown: m_element.call<void>("setPointerCapture", event.pointerId); - m_window->window()->requestActivate(); + 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; - case EventType::PointerEnter: - if (event.isPrimary) { - QWindowSystemInterface::handleEnterEvent(m_window->window(), pointInTargetWindowCoords, - pointInScreen); - } - break; - case EventType::PointerLeave: - if (event.isPrimary) - QWindowSystemInterface::handleLeaveEvent(m_window->window()); - break; default: break; }; @@ -71,7 +89,7 @@ bool ClientArea::processPointer(const PointerEvent &event) bool ClientArea::deliverEvent(const PointerEvent &event) { const auto pointInScreen = m_screen->mapFromLocal( - dom::mapPoint(event.target, m_screen->element(), event.localPoint)); + dom::mapPoint(event.target(), m_screen->element(), event.localPoint)); const auto geometryF = m_screen->geometry().toRectF(); const QPointF targetPointClippedToScreen( @@ -90,6 +108,31 @@ bool ClientArea::deliverEvent(const PointerEvent &event) 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 = @@ -98,14 +141,17 @@ bool ClientArea::deliverEvent(const PointerEvent &event) pointInTargetWindowCoords.y() / m_window->window()->height()); const auto tp = m_pointerIdToTouchPoints.find(event.pointerId); - if (tp != m_pointerIdToTouchPoints.end()) { + if (event.pointerType != PointerType::Pen && tp != m_pointerIdToTouchPoints.end()) { touchPoint = &tp.value(); } else { touchPoint = &m_pointerIdToTouchPoints .insert(event.pointerId, QWindowSystemInterface::TouchPoint()) .value(); - touchPoint->id = event.pointerId; + // 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; } diff --git a/src/plugins/platforms/wasm/qwasmwindowclientarea.h b/src/plugins/platforms/wasm/qwasmwindowclientarea.h index 49f515a71a..ba745a59a8 100644 --- a/src/plugins/platforms/wasm/qwasmwindowclientarea.h +++ b/src/plugins/platforms/wasm/qwasmwindowclientarea.h @@ -6,6 +6,7 @@ #include <QtCore/qnamespace.h> #include <qpa/qwindowsysteminterface.h> +#include <QtCore/QMap> #include <emscripten/val.h> @@ -35,6 +36,11 @@ private: 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; diff --git a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp index bc597069e7..00fa8fb236 100644 --- a/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp +++ b/src/plugins/platforms/wasm/qwasmwindownonclientarea.cpp @@ -187,9 +187,11 @@ ResizeConstraints Resizer::getResizeConstraints() { const auto frameRect = QRectF::fromDOMRect(m_windowElement.call<emscripten::val>("getBoundingClientRect")); - const auto screenRect = QRectF::fromDOMRect( - m_window->platformScreen()->element().call<emscripten::val>("getBoundingClientRect")); - const int maxGrowTop = frameRect.top() - screenRect.top(); + auto containerGeometry = + QRectF::fromDOMRect(m_window->parentNode()->containerElement().call<emscripten::val>( + "getBoundingClientRect")); + + const int maxGrowTop = frameRect.top() - containerGeometry.top(); return ResizeConstraints{minShrink, maxGrow, maxGrowTop}; } @@ -206,11 +208,12 @@ void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) m_currentResizeData.reset(new ResizeData{ .edges = resizeEdges, .originInScreenCoords = dom::mapPoint( - event.target, m_window->platformScreen()->element(), event.localPoint), + 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, @@ -222,7 +225,7 @@ void Resizer::startResize(Qt::Edges resizeEdges, const PointerEvent &event) void Resizer::continueResize(const PointerEvent &event) { const auto pointInScreen = - dom::mapPoint(event.target, m_window->platformScreen()->element(), event.localPoint); + 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(), @@ -415,9 +418,15 @@ bool TitleBar::onDoubleClick() QPointF TitleBar::clipPointWithScreen(const QPointF &pointInTitleBarCoords) const { - auto *screen = m_window->platformScreen(); - return screen->clipPoint(screen->mapFromLocal( - dom::mapPoint(m_element, screen->element(), pointInTitleBarCoords))); + 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) 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/wasm_shell.html b/src/plugins/platforms/wasm/wasm_shell.html index aaa121981d..702ea1f59d 100644 --- a/src/plugins/platforms/wasm/wasm_shell.html +++ b/src/plugins/platforms/wasm/wasm_shell.html @@ -27,42 +27,48 @@ </figure> <div id="screen"></div> - <script type='text/javascript'> - let qtLoader = undefined; - function init() { - var spinner = document.querySelector('#qtspinner'); - var canvas = document.querySelector('#screen'); - var status = document.querySelector('#qtstatus') + <script type="text/javascript"> + async function init() + { + const spinner = document.querySelector('#qtspinner'); + const screen = document.querySelector('#screen'); + const status = document.querySelector('#qtstatus'); - qtLoader = new 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 showUi = (ui) => { + [spinner, screen].forEach(element => element.style.display = 'none'); + if (screen === ui) + screen.style.position = 'default'; + ui.style.display = 'block'; + } + + try { + showUi(spinner); + status.innerHTML = 'Loading...'; + + 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 4425e78b30..4b92317978 100644 --- a/src/plugins/platforms/windows/CMakeLists.txt +++ b/src/plugins/platforms/windows/CMakeLists.txt @@ -15,13 +15,13 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin 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 @@ -38,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 @@ -54,6 +56,7 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin imm32 ole32 oleaut32 + setupapi shell32 shlwapi user32 @@ -66,10 +69,6 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin runtimeobject ) -# Duplicated symbols -set_source_files_properties(qwindowspointerhandler.cpp qwindowsmousehandler.cpp - PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON) - # Resources: set_source_files_properties("openglblacklists/default.json" PROPERTIES QT_RESOURCE_ALIAS "default.json" @@ -104,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 @@ -174,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 @@ -195,11 +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 ) - -if (MINGW) - set_source_files_properties(qwindowspointerhandler.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON) -endif() diff --git a/src/plugins/platforms/windows/qtwindowsglobal.h b/src/plugins/platforms/windows/qtwindowsglobal.h index 22787bd63e..96a72600eb 100644 --- a/src/plugins/platforms/windows/qtwindowsglobal.h +++ b/src/plugins/platforms/windows/qtwindowsglobal.h @@ -119,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, @@ -230,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; diff --git a/src/plugins/platforms/windows/qwindowsapplication.cpp b/src/plugins/platforms/windows/qwindowsapplication.cpp index 00477e2890..42e34ac99f 100644 --- a/src/plugins/platforms/windows/qwindowsapplication.cpp +++ b/src/plugins/platforms/windows/qwindowsapplication.cpp @@ -72,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; @@ -143,9 +138,9 @@ QVariant QWindowsApplication::gpuList() const return result; } -void QWindowsApplication::lightSystemPalette(QPalette &result) const +void QWindowsApplication::populateLightSystemPalette(QPalette &result) const { - QWindowsTheme::populateLightSystemBasePalette(result); + 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 8b74b47f3d..0918df91af 100644 --- a/src/plugins/platforms/windows/qwindowsapplication.h +++ b/src/plugins/platforms/windows/qwindowsapplication.h @@ -24,7 +24,6 @@ public: bool isWinTabEnabled() const override; bool setWinTabEnabled(bool enabled) override; - bool isDarkMode() const override; DarkModeHandling darkModeHandling() const override; void setDarkModeHandling(DarkModeHandling handling) override; @@ -43,7 +42,7 @@ public: QVariant gpu() const override; QVariant gpuList() const override; - void lightSystemPalette(QPalette &palette) const override; + void populateLightSystemPalette(QPalette &palette) const override; private: WindowActivationBehavior m_windowActivationBehavior = DefaultActivateWindow; 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/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 5475e9707b..de65a2171d 100644 --- a/src/plugins/platforms/windows/qwindowscontext.cpp +++ b/src/plugins/platforms/windows/qwindowscontext.cpp @@ -45,10 +45,13 @@ #include <QtCore/qscopedpointer.h> #include <QtCore/quuid.h> #include <QtCore/private/qwinregistry_p.h> -#include <QtCore/private/qfactorycacheregistration_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> @@ -151,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() @@ -169,7 +170,6 @@ QWindowsContextPrivate::QWindowsContextPrivate() m_systemInfo |= QWindowsContext::SI_RTL_Extensions; m_keyMapper.setUseRTLExtensions(true); } - m_darkMode = QWindowsTheme::queryDarkMode(); if (FAILED(m_oleInitializeResult)) { qWarning() << "QWindowsContext: OleInitialize() failed: " << QSystemError::windowsComString(m_oleInitializeResult); @@ -461,11 +461,6 @@ bool QWindowsContext::setProcessDpiAwareness(QtWindows::DpiAwareness dpiAwarenes return true; } -bool QWindowsContext::isDarkMode() -{ - return QWindowsContextPrivate::m_darkMode; -} - QWindowsContext *QWindowsContext::instance() { return m_instance; @@ -481,9 +476,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() @@ -535,6 +530,8 @@ QString QWindowsContext::classNamePrefix() # define xstr(s) str(s) # define str(s) #s str << xstr(QT_NAMESPACE); +# undef str +# undef xstr #endif } return result; @@ -1001,9 +998,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); @@ -1087,21 +1085,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(); - const bool darkModeChanged = darkMode != QWindowsContextPrivate::m_darkMode; - QWindowsContextPrivate::m_darkMode = darkMode; - auto integration = QWindowsIntegration::instance(); - integration->updateApplicationBadge(); - if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle)) { - QWindowsTheme::instance()->refresh(); - QWindowSystemInterface::handleThemeChange(); - } - if (darkModeChanged) { - if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeWindowFrames)) { - for (QWindowsWindow *w : d->m_windows) - w->setDarkBorder(QWindowsContextPrivate::m_darkMode); - } - } + QWindowsTheme::handleSettingsChanged(); } return d->m_screenManager.handleScreenChanges(); } @@ -1118,7 +1102,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)); @@ -1161,7 +1145,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(); @@ -1205,21 +1189,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)) @@ -1265,6 +1257,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()); @@ -1403,7 +1396,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); } } @@ -1433,7 +1426,7 @@ bool QWindowsContext::handleContextMenuEvent(QWindow *window, const MSG &msg) } QWindowSystemInterface::handleContextMenuEvent(window, mouseTriggered, pos, globalPos, - QWindowsKeyMapper::queryKeyboardModifiers()); + keyMapper()->queryKeyboardModifiers()); return true; } #endif @@ -1454,7 +1447,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); @@ -1463,8 +1456,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) diff --git a/src/plugins/platforms/windows/qwindowscontext.h b/src/plugins/platforms/windows/qwindowscontext.h index 1a3b47be5e..0539a22afc 100644 --- a/src/plugins/platforms/windows/qwindowscontext.h +++ b/src/plugins/platforms/windows/qwindowscontext.h @@ -33,6 +33,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen) class QWindow; class QPlatformScreen; class QPlatformWindow; +class QPlatformKeyMapper; class QWindowsMenuBar; class QWindowsScreenManager; class QWindowsTabletSupport; @@ -43,7 +44,6 @@ struct QWindowsContextPrivate; class QPoint; class QKeyEvent; class QPointingDevice; - class QWindowsContext { Q_DISABLE_COPY_MOVE(QWindowsContext) @@ -120,15 +120,13 @@ public: static QtWindows::DpiAwareness processDpiAwareness(); static QtWindows::DpiAwareness windowDpiAwareness(HWND hwnd); - static bool isDarkMode(); - void setDetectAltGrModifier(bool a); // Returns a combination of SystemInfoFlags unsigned systemInfo() const; bool useRTLExtensions() const; - QList<int> possibleKeys(const QKeyEvent *e) const; + QPlatformKeyMapper *keyMapper() const; HandleBaseWindowHash &windows(); diff --git a/src/plugins/platforms/windows/qwindowscursor.cpp b/src/plugins/platforms/windows/qwindowscursor.cpp index 77903f6b6f..b416886120 100644 --- a/src/plugins/platforms/windows/qwindowscursor.cpp +++ b/src/plugins/platforms/windows/qwindowscursor.cpp @@ -30,6 +30,8 @@ static bool initResources() QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + /*! \class QWindowsCursorCacheKey \brief Cache key for storing values in a QHash with a QCursor as key. @@ -103,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) @@ -121,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()); @@ -138,8 +147,8 @@ static HCURSOR createBitmapCursor(const QCursor &cursor, qreal scaleFactor = 1) 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); @@ -436,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 51bd9f0c38..0ce0b0e2a7 100644 --- a/src/plugins/platforms/windows/qwindowsdialoghelpers.cpp +++ b/src/plugins/platforms/windows/qwindowsdialoghelpers.cpp @@ -4,7 +4,6 @@ #define QT_NO_URL_CAST_FROM_STRING 1 #include <QtCore/qt_windows.h> -#include "qwindowscombase.h" #include "qwindowsdialoghelpers.h" #include "qwindowscontext.h" @@ -33,6 +32,7 @@ #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> @@ -43,26 +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; -} -#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) { @@ -443,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: @@ -586,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); } @@ -1379,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))) @@ -1571,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 @@ -1605,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 @@ -1713,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) { @@ -1734,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); } @@ -1756,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 48e0bba41f..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; @@ -649,7 +651,8 @@ IDropTargetHelper* QWindowsDrag::dropHelper() { static HRESULT startDoDragDrop(LPDATAOBJECT pDataObj, LPDROPSOURCE pDropSource, DWORD dwOKEffects, LPDWORD pdwEffect) { QWindow *underMouse = QWindowsContext::instance()->windowUnderMouse(); - const HWND hwnd = underMouse ? reinterpret_cast<HWND>(underMouse->winId()) : ::GetFocus(); + const bool hasMouseCapture = underMouse && static_cast<QWindowsWindow *>(underMouse->handle())->hasMouseCapture(); + const HWND hwnd = hasMouseCapture ? reinterpret_cast<HWND>(underMouse->winId()) : ::GetFocus(); bool starting = false; for (;;) { @@ -738,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 = startDoDragDrop(dropDataObject, windowDropSource, allowedEffects, &resultEffect); - QWindowsDrag::m_dragging = false; 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 fbb2b45e21..629291640f 100644 --- a/src/plugins/platforms/windows/qwindowsdropdataobject.cpp +++ b/src/plugins/platforms/windows/qwindowsdropdataobject.cpp @@ -9,6 +9,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + /*! \class QWindowsDropDataObject \brief QWindowsOleDataObject subclass specialized for handling Drag&Drop. @@ -56,8 +58,8 @@ bool QWindowsDropDataObject::shouldIgnore(LPFORMATETC pformatetc) const 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 cfd73519fa..5ca52c2c19 100644 --- a/src/plugins/platforms/windows/qwindowsglcontext.cpp +++ b/src/plugins/platforms/windows/qwindowsglcontext.cpp @@ -1262,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 628b2f3209..aa6be266da 100644 --- a/src/plugins/platforms/windows/qwindowsintegration.cpp +++ b/src/plugins/platforms/windows/qwindowsintegration.cpp @@ -80,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) @@ -167,8 +142,8 @@ static inline unsigned parseOptions(const QStringList ¶mList, 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")) { @@ -281,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 @@ -311,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); } @@ -507,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; } @@ -565,14 +542,9 @@ QVariant QWindowsIntegration::styleHint(QPlatformIntegration::StyleHint hint) co return QPlatformIntegration::styleHint(hint); } -Qt::KeyboardModifiers QWindowsIntegration::queryKeyboardModifiers() const +QPlatformKeyMapper *QWindowsIntegration::keyMapper() const { - return QWindowsKeyMapper::queryKeyboardModifiers(); -} - -QList<int> QWindowsIntegration::possibleKeys(const QKeyEvent *e) const -{ - return d->m_context.possibleKeys(e); + return d->m_context.keyMapper(); } #if QT_CONFIG(clipboard) @@ -653,12 +625,16 @@ void QWindowsIntegration::setApplicationBadge(qint64 number) // 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. - 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_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 @@ -670,7 +646,8 @@ void QWindowsIntegration::setApplicationBadge(qint64 number) return; } - const bool isDarkMode = QWindowsContext::isDarkMode(); + const bool isDarkMode = QWindowsTheme::instance()->colorScheme() + == Qt::ColorScheme::Dark; QColor badgeColor; QColor textColor; @@ -785,7 +762,8 @@ void QWindowsIntegration::updateApplicationBadge() // 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. - setApplicationBadge(m_applicationBadgeNumber); + if (m_applicationBadgeNumber) + setApplicationBadge(m_applicationBadgeNumber); } #if QT_CONFIG(vulkan) diff --git a/src/plugins/platforms/windows/qwindowsintegration.h b/src/plugins/platforms/windows/qwindowsintegration.h index 1f3c71c41b..c271207741 100644 --- a/src/plugins/platforms/windows/qwindowsintegration.h +++ b/src/plugins/platforms/windows/qwindowsintegration.h @@ -45,7 +45,7 @@ public: DontUseWMPointer = 0x400, DetectAltGrModifier = 0x800, RtlEnabled = 0x1000, - FontDatabaseDirectWrite = 0x2000 + FontDatabaseGDI = 0x2000 }; explicit QWindowsIntegration(const QStringList ¶mList); @@ -82,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; } diff --git a/src/plugins/platforms/windows/qwindowskeymapper.cpp b/src/plugins/platforms/windows/qwindowskeymapper.cpp index 3089e745d4..ba76cda40b 100644 --- a/src/plugins/platforms/windows/qwindowskeymapper.cpp +++ b/src/plugins/platforms/windows/qwindowskeymapper.cpp @@ -88,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 { @@ -532,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 /** @@ -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)); } @@ -820,7 +801,7 @@ static void showSystemMenu(QWindow* w) 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, @@ -829,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); } /*! @@ -897,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) @@ -942,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; @@ -977,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; } @@ -1012,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; @@ -1232,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; } @@ -1260,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; @@ -1302,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; @@ -1317,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); @@ -1339,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) @@ -1353,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(); @@ -1369,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 > Qt::KeyboardModifiers(*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.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/qwindowsmimeregistry.cpp b/src/plugins/platforms/windows/qwindowsmimeregistry.cpp index 2d1c42df91..8d147e8fa0 100644 --- a/src/plugins/platforms/windows/qwindowsmimeregistry.cpp +++ b/src/plugins/platforms/windows/qwindowsmimeregistry.cpp @@ -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]; @@ -870,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); @@ -883,7 +883,7 @@ 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); } } diff --git a/src/plugins/platforms/windows/qwindowsmousehandler.cpp b/src/plugins/platforms/windows/qwindowsmousehandler.cpp index ceb33f6141..9af9fba408 100644 --- a/src/plugins/platforms/windows/qwindowsmousehandler.cpp +++ b/src/plugins/platforms/windows/qwindowsmousehandler.cpp @@ -268,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; @@ -286,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); @@ -448,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); } @@ -472,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: @@ -493,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); } @@ -521,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; } @@ -552,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; } @@ -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.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/qwindowspointerhandler.cpp b/src/plugins/platforms/windows/qwindowspointerhandler.cpp index 88f02347b3..71c7217671 100644 --- a/src/plugins/platforms/windows/qwindowspointerhandler.cpp +++ b/src/plugins/platforms/windows/qwindowspointerhandler.cpp @@ -428,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; } @@ -441,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. @@ -537,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. } @@ -635,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. @@ -648,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: @@ -671,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); @@ -724,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; } @@ -737,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()) @@ -760,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) @@ -820,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 } @@ -852,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 5414fc2778..a50f9fd4b0 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.cpp +++ b/src/plugins/platforms/windows/qwindowsscreen.cpp @@ -14,14 +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 @@ -42,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; @@ -58,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); @@ -67,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 @@ -108,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; @@ -120,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; @@ -155,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; @@ -231,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) @@ -390,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(), @@ -406,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 @@ -517,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); diff --git a/src/plugins/platforms/windows/qwindowsscreen.h b/src/plugins/platforms/windows/qwindowsscreen.h index b1c94d2204..0467ab2a0c 100644 --- a/src/plugins/platforms/windows/qwindowsscreen.h +++ b/src/plugins/platforms/windows/qwindowsscreen.h @@ -7,10 +7,9 @@ #include "qtwindowsglobal.h" #include <QtCore/qlist.h> -#include <QtCore/qpair.h> #include <QtCore/qscopedpointer.h> #include <qpa/qplatformscreen.h> -#include <qpa/qplatformscreen_p.h> +#include <QtGui/qscreen_platform.h> QT_BEGIN_NAMESPACE @@ -31,15 +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 - , public QNativeInterface::Private::QWindowsScreen +class QWindowsScreen : public QPlatformScreen, public QNativeInterface::QWindowsScreen { public: #ifndef QT_NO_CURSOR @@ -58,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; diff --git a/src/plugins/platforms/windows/qwindowsservices.cpp b/src/plugins/platforms/windows/qwindowsservices.cpp index d9bb76d10a..89f93fd161 100644 --- a/src/plugins/platforms/windows/qwindowsservices.cpp +++ b/src/plugins/platforms/windows/qwindowsservices.cpp @@ -154,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); diff --git a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp index 3bad237f9e..6f0680ac23 100644 --- a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp +++ b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp @@ -24,6 +24,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + static const UINT q_uNOTIFYICONID = 0; static uint MYWM_TASKBARCREATED = 0; @@ -116,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()), @@ -162,8 +164,6 @@ 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()) @@ -184,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); @@ -217,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 @@ -293,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(); } @@ -310,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; 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 6fba9e55c1..e8a324aedb 100644 --- a/src/plugins/platforms/windows/qwindowstheme.cpp +++ b/src/plugins/platforms/windows/qwindowstheme.cpp @@ -7,26 +7,29 @@ #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 "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/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> @@ -48,10 +51,6 @@ # include <winrt/Windows.UI.ViewManagement.h> #endif // QT_CONFIG(cpp_winrt) -#if defined(__IImageList_INTERFACE_DEFINED__) && defined(__IID_DEFINED__) -# define USE_IIMAGELIST -#endif - QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; @@ -79,11 +78,11 @@ static inline QColor mixColors(const QColor &c1, const QColor &c2) (c1.blue() + c2.blue()) / 2}; } -static inline QColor getSysColor(int index) -{ - COLORREF cr = GetSysColor(index); - return QColor(GetRValue(cr), GetGValue(cr), GetBValue(cr)); -} +enum AccentColorLevel { + AccentColorDarkest, + AccentColorNormal, + AccentColorLightest +}; #if QT_CONFIG(cpp_winrt) static constexpr QColor getSysColor(winrt::Windows::UI::Color &&color) @@ -92,111 +91,146 @@ static constexpr QColor getSysColor(winrt::Windows::UI::Color &&color) } #endif -// 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. +[[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; +} -struct QShGetFileInfoParams +static inline QColor getSysColor(int index) { - 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; -}; + 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. class QShGetFileInfoThread : public QThread { public: - explicit QShGetFileInfoThread() - : QThread(), m_params(nullptr) + struct Task { - connect(this, &QThread::finished, this, &QObject::deleteLater); + 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() + { + start(); + } + + ~QShGetFileInfoThread() + { + cancel(); + wait(); + } + + QSharedPointer<Task> getNextTask() + { + QMutexLocker l(&m_waitForTaskMutex); + while (!isInterruptionRequested()) { + if (!m_taskQueue.isEmpty()) + return m_taskQueue.dequeue(); + m_waitForTaskCondition.wait(&m_waitForTaskMutex); + } + return nullptr; } void run() override { QComHelper comHelper(COINIT_MULTITHREADED); - QMutexLocker readyLocker(&m_readyMutex); - while (!m_cancelled.loadRelaxed()) { - if (!m_params && !m_cancelled.loadRelaxed() - && !m_readyCondition.wait(&m_readyMutex, QDeadlineTimer(1000ll))) - continue; - - if (m_params) { - const QString fileName = m_params->fileName; + 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(); } } } - 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: - 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; -} +Q_APPLICATION_STATIC(QShGetFileInfoThread, s_shGetFileInfoThread) // from QStyle::standardPalette static inline QPalette standardPalette() @@ -227,22 +261,17 @@ static QColor placeHolderColor(QColor textColor) */ void QWindowsTheme::populateLightSystemBasePalette(QPalette &result) { - QColor background = getSysColor(COLOR_BTNFACE); - QColor textColor = getSysColor(COLOR_WINDOWTEXT); - QColor accent = getSysColor(COLOR_HIGHLIGHT); + const QColor background = getSysColor(COLOR_BTNFACE); + const QColor textColor = getSysColor(COLOR_WINDOWTEXT); -#if QT_CONFIG(cpp_winrt) - // respect the Windows 11 accent color - using namespace winrt::Windows::UI::ViewManagement; - const auto settings = UISettings(); - - accent = getSysColor(settings.GetColorValue(UIColorType::Accent)); -#endif + 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, accent); + result.setColor(QPalette::Highlight, getSysColor(COLOR_HIGHLIGHT)); result.setColor(QPalette::WindowText, getSysColor(COLOR_WINDOWTEXT)); result.setColor(QPalette::Button, btnFace); result.setColor(QPalette::Light, btnHighlight); @@ -257,9 +286,10 @@ void QWindowsTheme::populateLightSystemBasePalette(QPalette &result) result.setColor(QPalette::Midlight, getSysColor(COLOR_3DLIGHT)); result.setColor(QPalette::Shadow, getSysColor(COLOR_3DDKSHADOW)); result.setColor(QPalette::HighlightedText, getSysColor(COLOR_HIGHLIGHTTEXT)); + result.setColor(QPalette::Accent, accent); - 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()); @@ -269,7 +299,7 @@ void QWindowsTheme::populateLightSystemBasePalette(QPalette &result) result.setColor(QPalette::Midlight, result.button().color().lighter(110)); } -static void populateDarkSystemBasePalette(QPalette &result) +void QWindowsTheme::populateDarkSystemBasePalette(QPalette &result) { #if QT_CONFIG(cpp_winrt) using namespace winrt::Windows::UI::ViewManagement; @@ -293,19 +323,18 @@ static void populateDarkSystemBasePalette(QPalette &result) const QColor accentLight = getSysColor(settings.GetColorValue(UIColorType::AccentLight1)); const QColor accentLighter = getSysColor(settings.GetColorValue(UIColorType::AccentLight2)); const QColor accentLightest = getSysColor(settings.GetColorValue(UIColorType::AccentLight3)); - const QColor linkColor = accent; #else const QColor foreground = Qt::white; const QColor background = QColor(0x1E, 0x1E, 0x1E); - const QColor accent = QColor(0x00, 0x55, 0xff); + const QColor accent = qt_accentColor(AccentColorNormal); const QColor accentDark = accent.darker(120); const QColor accentDarker = accentDark.darker(120); const QColor accentDarkest = accentDarker.darker(120); const QColor accentLight = accent.lighter(120); const QColor accentLighter = accentLight.lighter(120); const QColor accentLightest = accentLighter.lighter(120); - const QColor linkColor = Qt::blue; #endif + const QColor linkColor = accent; const QColor buttonColor = background.lighter(200); result.setColor(QPalette::All, QPalette::WindowText, foreground); @@ -331,36 +360,7 @@ static void populateDarkSystemBasePalette(QPalette &result) result.setColor(QPalette::All, QPalette::ToolTipBase, buttonColor); result.setColor(QPalette::All, QPalette::ToolTipText, foreground.darker(120)); result.setColor(QPalette::All, QPalette::PlaceholderText, placeHolderColor(foreground)); -} - -static QPalette systemPalette(bool light) -{ - QPalette result = standardPalette(); - if (light) - QWindowsTheme::populateLightSystemBasePalette(result); - else - populateDarkSystemBasePalette(result); - - 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, result.color(QPalette::Highlight)); - result.setColor(QPalette::Disabled, QPalette::HighlightedText, result.color(QPalette::HighlightedText)); - result.setColor(QPalette::Disabled, QPalette::Base, result.window().color()); - return result; + result.setColor(QPalette::All, QPalette::Accent, accent); } static inline QPalette toolTipPalette(const QPalette &systemPalette, bool light) @@ -452,6 +452,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(); @@ -473,7 +474,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() @@ -538,7 +542,31 @@ QVariant QWindowsTheme::themeHint(ThemeHint hint) const Qt::ColorScheme QWindowsTheme::colorScheme() const { - return QWindowsContext::isDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; + return QWindowsTheme::effectiveColorScheme(); +} + +Qt::ColorScheme QWindowsTheme::effectiveColorScheme() +{ + if (queryHighContrast()) + return Qt::ColorScheme::Unknown; + return s_colorScheme; +} + +void QWindowsTheme::handleSettingsChanged() +{ + const auto newColorScheme = QWindowsTheme::queryColorScheme(); + const bool colorSchemeChanged = newColorScheme != QWindowsTheme::s_colorScheme; + 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() @@ -549,41 +577,66 @@ 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); clearPalettes(); - m_palettes[SystemPalette] = new QPalette(systemPalette(light)); + 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) { -#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)); - m_palettes[CheckBoxPalette] = new QPalette(*m_palettes[SystemPalette]); - m_palettes[CheckBoxPalette]->setColor(QPalette::Active, QPalette::Base, accent); - m_palettes[CheckBoxPalette]->setColor(QPalette::Active, QPalette::Button, accentLight); - m_palettes[CheckBoxPalette]->setColor(QPalette::Inactive, QPalette::Base, accentDarkest); -#else - m_palettes[ButtonPalette] = new QPalette(*m_palettes[SystemPalette]); - m_palettes[ButtonPalette]->setColor(QPalette::Button, QColor(0x666666u)); - const QColor checkBoxBlue(0x0078d7u); m_palettes[CheckBoxPalette] = new QPalette(*m_palettes[SystemPalette]); - m_palettes[CheckBoxPalette]->setColor(QPalette::Base, checkBoxBlue); - m_palettes[CheckBoxPalette]->setColor(QPalette::Button, checkBoxBlue); - m_palettes[CheckBoxPalette]->setColor(QPalette::ButtonText, Qt::white); -#endif + 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() { qDeleteAll(m_fonts, m_fonts + NFonts); @@ -695,11 +748,7 @@ 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(lcQpaWindow) << __FUNCTION__ << m_fileIconSizes; } @@ -812,15 +861,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; + } } } @@ -890,10 +942,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}}; @@ -902,16 +953,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; } @@ -963,13 +1010,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); @@ -985,7 +1028,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; @@ -997,43 +1039,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; @@ -1044,6 +1086,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(); @@ -1068,19 +1115,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 7f320da967..6109122944 100644 --- a/src/plugins/platforms/windows/qwindowstheme.h +++ b/src/plugins/platforms/windows/qwindowstheme.h @@ -33,6 +33,8 @@ public: Qt::ColorScheme colorScheme() const override; + static void handleSettingsChanged(); + const QPalette *palette(Palette type = SystemPalette) const override { return m_palettes[type]; } const QFont *font(Font type = SystemFont) const override @@ -41,6 +43,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,15 +56,13 @@ public: void showPlatformMenuBar() override; static bool useNativeMenus(); - static bool queryDarkMode(); - static bool queryHighContrast(); void refreshFonts(); void refresh(); static const char *name; - static void populateLightSystemBasePalette(QPalette &result); + static QPalette systemPalette(Qt::ColorScheme); private: void clearPalettes(); @@ -69,7 +70,15 @@ private: 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; 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 d0107145af..5d96d40af5 100644 --- a/src/plugins/platforms/windows/qwindowswindow.cpp +++ b/src/plugins/platforms/windows/qwindowswindow.cpp @@ -5,6 +5,7 @@ #include "qwindowswindow.h" #include "qwindowscontext.h" +#include "qwindowstheme.h" #if QT_CONFIG(draganddrop) # include "qwindowsdrag.h" #endif @@ -27,11 +28,11 @@ #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> #include <qpa/qwindowsysteminterface.h> -#include <qpa/qplatformtheme.h> #include <QtCore/qdebug.h> #include <QtCore/qlibraryinfo.h> @@ -430,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: @@ -469,14 +466,21 @@ 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_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) { @@ -492,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); @@ -835,6 +839,10 @@ 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; } } @@ -922,7 +930,7 @@ QWindowsWindowData return result; } - if (QWindowsContext::isDarkMode() && shouldApplyDarkFrame(w)) + if (QWindowsTheme::instance()->colorScheme() == Qt::ColorScheme::Dark && shouldApplyDarkFrame(w)) QWindowsWindow::setDarkBorderToWindow(result.hwnd, true); if (mirrorParentWidth != 0) { @@ -933,6 +941,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; @@ -1007,6 +1016,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. @@ -1019,7 +1043,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. @@ -1035,15 +1059,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. @@ -1061,7 +1083,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); @@ -1075,8 +1097,6 @@ 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(GetWindowLongPtr(hwnd, GWL_STYLE)), DWORD(GetWindowLongPtr(hwnd, GWL_EXSTYLE))); } @@ -1085,7 +1105,7 @@ QMargins QWindowsGeometryHint::frame(const QWindow *w, HWND hwnd) 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)) { @@ -1338,6 +1358,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) @@ -1513,6 +1535,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(); @@ -1954,6 +1977,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 @@ -1961,10 +1990,9 @@ 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 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 @@ -1973,7 +2001,8 @@ void QWindowsWindow::handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT * // are currently doing. m_data.customMargins *= scale; } - const QSize windowSize = (geometry().size() * scale).grownBy(margins + customMargins()); + + const QSize windowSize = (geometry().size() * scale).grownBy((margins * scale) + customMargins()); SIZE *size = reinterpret_cast<SIZE *>(lParam); size->cx = windowSize.width(); size->cy = windowSize.height(); @@ -1983,11 +2012,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. @@ -2010,13 +2045,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); @@ -2220,10 +2264,10 @@ 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(lcQpaWindow).noquote().nospace() << __FUNCTION__ << ' ' << window() << " \"" << (currentScreen ? currentScreen->name() : QString()) << "\"->\"" << newScreen->name() << '"'; @@ -2235,6 +2279,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 @@ -2252,6 +2297,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; @@ -2340,23 +2388,9 @@ static inline bool isSoftwareGl() } bool QWindowsWindow::handleWmPaint(HWND hwnd, UINT message, - WPARAM wParam, LPARAM, LRESULT *result) + WPARAM, LPARAM, LRESULT *result) { if (message == WM_ERASEBKGND) { // Backing store - ignored. - if (!m_firstBgDraw) { - // Get window background from the default palette; this will - // usually be the system background color. - const QColor bgColor = QGuiApplication::palette().color(QPalette::Window); - HBRUSH bgBrush = CreateSolidBrush(RGB(bgColor.red(), bgColor.green(), bgColor.blue())); - // Fill rectangle with system background color - RECT rc; - auto hdc = reinterpret_cast<HDC>(wParam); - GetClientRect(hwnd, &rc); - FillRect(hdc, &rc, bgBrush); - DeleteObject(bgBrush); - // Brush the window with system background color only for first time - m_firstBgDraw = true; - } *result = 1; return true; } @@ -2441,6 +2475,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(); @@ -2464,6 +2506,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) { @@ -2531,26 +2578,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 { @@ -2643,7 +2690,7 @@ bool QWindowsWindow::windowEvent(QEvent *event) { switch (event->type()) { case QEvent::ApplicationPaletteChange: - setDarkBorder(QWindowsContext::isDarkMode()); + setDarkBorder(QWindowsTheme::instance()->colorScheme() == Qt::ColorScheme::Dark); break; case QEvent::WindowBlocked: // Blocked by another modal window. setEnabled(false); @@ -2670,10 +2717,17 @@ void QWindowsWindow::propagateSizeHints() 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)) { @@ -2689,9 +2743,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; @@ -2713,7 +2764,7 @@ bool QWindowsWindow::handleGeometryChanging(MSG *message) const void QWindowsWindow::setFullFrameMargins(const QMargins &newMargins) { - if (m_data.flags & Qt::FramelessWindowHint) + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) return; if (m_data.fullFrameMargins != newMargins) { qCDebug(lcQpaWindow) << __FUNCTION__ << window() << m_data.fullFrameMargins << "->" << newMargins; @@ -2732,14 +2783,46 @@ void QWindowsWindow::updateFullFrameMargins() void QWindowsWindow::calculateFullFrameMargins() { - if (m_data.flags & Qt::FramelessWindowHint) + 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 + 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 @@ -2752,7 +2835,7 @@ QMargins QWindowsWindow::frameMargins() const QMargins QWindowsWindow::fullFrameMargins() const { - if (m_data.flags & Qt::FramelessWindowHint) + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) return {}; return m_data.fullFrameMargins; } @@ -3203,17 +3286,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) - qCWarning(lcQpaWindow, "%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; @@ -3229,8 +3301,6 @@ void QWindowsWindow::setDarkBorder(bool d) { // respect explicit opt-out and incompatible palettes or styles d = d && shouldApplyDarkFrame(window()); - if (queryDarkBorder(m_data.hwnd) == d) - return; setDarkBorderToWindow(m_data.hwnd, d); } @@ -3359,24 +3429,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 38e9fb9a5b..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; @@ -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; @@ -377,7 +381,6 @@ private: HICON m_iconBig = nullptr; void *m_surface = nullptr; int m_savedDpi = 96; - bool m_firstBgDraw = false; static bool m_screenForGLInitialized; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp index 001cb8505b..1abb412ccd 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,16 +113,17 @@ 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()) { 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 16f0a3f9a6..95ddbcced6 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> @@ -61,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)) { } @@ -80,7 +79,7 @@ void QWindowsUiaMainProvider::notifyFocusChange(QAccessibleEvent *event) accessible = child; } if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_AutomationFocusChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_AutomationFocusChangedEventId); } } @@ -97,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); } } } @@ -106,13 +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)) - QWindowsUiaWrapper::instance()->raiseAutomationEvent(focusedProvider, UIA_AutomationFocusChangedEventId); + UiaRaiseAutomationEvent(focusedProvider, UIA_AutomationFocusChangedEventId); } } else { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Window_WindowClosedEventId); + UiaRaiseAutomationEvent(provider, UIA_Window_WindowClosedEventId); } } } @@ -141,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)) { @@ -169,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); } } } @@ -185,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); } } @@ -196,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); } } } @@ -208,13 +191,13 @@ 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); } } } @@ -223,33 +206,26 @@ void QWindowsUiaMainProvider::notifyTextChange(QAccessibleEvent *event) HRESULT STDMETHODCALLTYPE QWindowsUiaMainProvider::QueryInterface(REFIID iid, LPVOID *iface) { - if (!iface) - return E_INVALIDARG; - *iface = nullptr; - - QAccessibleInterface *accessible = accessibleInterface(); + HRESULT result = QComObject::QueryInterface(iid, iface); - 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 (SUCCEEDED(result) && iid == __uuidof(IRawElementProviderFragmentRoot)) { + QAccessibleInterface *accessible = accessibleInterface(); + if (accessible && hwndForAccessible(accessible)) { + result = S_OK; + } else { + result = E_NOINTERFACE; + iface = nullptr; + } + } -ULONG QWindowsUiaMainProvider::AddRef() -{ - return ++m_ref; + return result; } ULONG STDMETHODCALLTYPE QWindowsUiaMainProvider::Release() { QMutexLocker locker(&m_mutex); - if (!--m_ref) { - delete this; - return 0; - } - return m_ref; + return QComObject::Release(); } HRESULT QWindowsUiaMainProvider::get_ProviderOptions(ProviderOptions *pRetVal) @@ -304,15 +280,18 @@ 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) + // 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()); @@ -369,6 +348,28 @@ 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; +} + HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pRetVal) { qCDebug(lcQpaUiAutomation) << __FUNCTION__ << idProp; @@ -403,6 +404,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; @@ -416,7 +426,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 @@ -519,7 +529,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; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h index fb5069f620..99db0ed318 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h @@ -20,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); @@ -39,7 +37,6 @@ public: // IUnknown HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id, LPVOID *iface) override; - ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE Release() override; // IRawElementProviderSimple methods @@ -62,7 +59,7 @@ public: private: QString automationIdForAccessible(const QAccessibleInterface *accessible); - ULONG m_ref; + static void fillVariantArrayForRelation(QAccessibleInterface *accessible, QAccessible::Relation relation, VARIANT *pRetVal); static QMutex m_mutex; }; 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 7e829b24a7..95bc2f7570 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionitemprovider.cpp @@ -36,6 +36,14 @@ 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; @@ -73,6 +81,13 @@ 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; @@ -98,6 +113,13 @@ 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; @@ -124,6 +146,14 @@ 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) @@ -146,12 +176,18 @@ 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 (QAccessibleInterface *parent = accessible->parent()) { + if (parent) { if ((accessible->role() == QAccessible::ListItem && parent->role() == QAccessible::List) || (accessible->role() == QAccessible::PageTab && parent->role() == QAccessible::PageTabList)) { *pRetVal = QWindowsUiaMainProvider::providerForAccessible(parent); 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 45f3b20552..37148c655a 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaselectionprovider.cpp @@ -41,17 +41,23 @@ 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 (accessible->role() == QAccessible::PageTabList) { - if (child->role() == QAccessible::PageTab && child->state().focused) { - selectedList.append(child); - } - } else { - 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); + } } } } @@ -102,11 +108,15 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_IsSelectionRequired( // 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 (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; + } } } } @@ -116,6 +126,134 @@ HRESULT STDMETHODCALLTYPE QWindowsUiaSelectionProvider::get_IsSelectionRequired( 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; + } + + return S_OK; +} + QT_END_NAMESPACE #endif // QT_CONFIG(accessibility) 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..1593a07202 --- /dev/null +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.cpp @@ -0,0 +1,74 @@ +// 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); +} + +#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..a192b9b0fb --- /dev/null +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h @@ -0,0 +1,70 @@ +// 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_CaretPositionAttributeId 40038 + +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 17e723d607..e8fb442dd4 100644 --- a/src/plugins/platforms/xcb/CMakeLists.txt +++ b/src/plugins/platforms/xcb/CMakeLists.txt @@ -5,6 +5,11 @@ ## 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 @@ -57,7 +62,6 @@ 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 @@ -68,11 +72,6 @@ qt_disable_apple_app_extension_api_only(XcbQpaPrivate) ## 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 @@ -178,5 +177,5 @@ qt_internal_add_plugin(QXcbIntegrationPlugin 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/xcb_egl/qxcbeglintegration.cpp b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.cpp index 1e93ea6805..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"; } @@ -80,6 +81,7 @@ bool QXcbEglIntegration::initialize(QXcbConnection *connection) m_egl_display = streamFuncs.get_platform_display(EGL_PLATFORM_X11_KHR, m_connection->xlib_display(), nullptr); + m_using_platform_display = true; } #if QT_CONFIG(egl_x11) @@ -92,16 +94,19 @@ bool QXcbEglIntegration::initialize(QXcbConnection *connection) 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())); 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 ff0804678f..7caf4304bc 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.h +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_egl/qxcbeglintegration.h @@ -39,10 +39,13 @@ public: EGLDisplay eglDisplay() const { return m_egl_display; } + 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/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/qxcbatom.cpp b/src/plugins/platforms/xcb/qxcbatom.cpp index 09b1fe8a9d..dd5596653c 100644 --- a/src/plugins/platforms/xcb/qxcbatom.cpp +++ b/src/plugins/platforms/xcb/qxcbatom.cpp @@ -111,6 +111,7 @@ 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" @@ -196,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" @@ -230,8 +232,10 @@ void QXcbAtom::initializeAllAtoms(xcb_connection_t *connection) { 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 07dad31d45..bc677eaf3e 100644 --- a/src/plugins/platforms/xcb/qxcbatom.h +++ b/src/plugins/platforms/xcb/qxcbatom.h @@ -112,6 +112,7 @@ public: 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, @@ -199,6 +200,7 @@ public: Atom_COMPIZ_DECOR_REQUEST, Atom_COMPIZ_DECOR_DELETE_PIXMAP, Atom_COMPIZ_TOOLKIT_ACTION, + Atom_GTK_APPLICATION_ID, Atom_GTK_LOAD_ICONTHEMES, AtomAT_SPI_BUS, diff --git a/src/plugins/platforms/xcb/qxcbbackingstore.cpp b/src/plugins/platforms/xcb/qxcbbackingstore.cpp index 0659cf9fc4..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); diff --git a/src/plugins/platforms/xcb/qxcbconnection.cpp b/src/plugins/platforms/xcb/qxcbconnection.cpp index 2e103b0eb2..1056c6408f 100644 --- a/src/plugins/platforms/xcb/qxcbconnection.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection.cpp @@ -92,9 +92,9 @@ QXcbConnection::QXcbConnection(QXcbNativeInterface *nativeInterface, bool canGra 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(); @@ -1141,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 24e684866d..527744fe81 100644 --- a/src/plugins/platforms/xcb/qxcbconnection.h +++ b/src/plugins/platforms/xcb/qxcbconnection.h @@ -183,7 +183,6 @@ public: QXcbSystemTrayTracker *systemTrayTracker() const; Qt::MouseButtons queryMouseButtons() const; - Qt::KeyboardModifiers queryKeyboardModifiers() const; bool isUserInputEvent(xcb_generic_event_t *event) const; @@ -367,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_xi2.cpp b/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp index efcea4f93d..4f62a1880b 100644 --- a/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection_xi2.cpp @@ -241,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; @@ -453,10 +454,6 @@ void QXcbConnection::xi2SetupSlavePointerDevice(void *info, bool removeExisting, */ 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); @@ -468,14 +465,13 @@ void QXcbConnection::xi2SetupDevices() // 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) { - for (auto it = previousDevices.constBegin(); it != previousDevices.constEnd(); ++it) { - if ((*it)->systemId() == systemId) { - previousDevices.erase(it); // it's a keeper - return false; // not new - } - } - return true; // it's really new + // 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. @@ -540,6 +536,11 @@ void QXcbConnection::xi2SetupDevices() // 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) @@ -765,7 +766,7 @@ void QXcbConnection::xi2HandleEvent(xcb_ge_event_t *event) if (auto device = QPointingDevicePrivate::pointingDeviceById(sourceDeviceId)) xi2HandleScrollEvent(event, device); else - qCWarning(lcQpaXInputEvents) << "scroll event from unregistered device" << Qt::hex << sourceDeviceId; + qCDebug(lcQpaXInputEvents) << "scroll event from unregistered device" << sourceDeviceId; if (xiDeviceEvent) { switch (xiDeviceEvent->event_type) { @@ -812,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; @@ -1274,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; @@ -1525,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, @@ -1533,22 +1547,19 @@ 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 diff --git a/src/plugins/platforms/xcb/qxcbcursor.cpp b/src/plugins/platforms/xcb/qxcbcursor.cpp index 8f087edc9b..dc9ed46956 100644 --- a/src/plugins/platforms/xcb/qxcbcursor.cpp +++ b/src/plugins/platforms/xcb/qxcbcursor.cpp @@ -10,6 +10,7 @@ #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> @@ -288,6 +289,13 @@ QXcbCursor::~QXcbCursor() 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) diff --git a/src/plugins/platforms/xcb/qxcbcursor.h b/src/plugins/platforms/xcb/qxcbcursor.h index 14958f824b..bf26861e8f 100644 --- a/src/plugins/platforms/xcb/qxcbcursor.h +++ b/src/plugins/platforms/xcb/qxcbcursor.h @@ -48,6 +48,8 @@ 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); diff --git a/src/plugins/platforms/xcb/qxcbdrag.cpp b/src/plugins/platforms/xcb/qxcbdrag.cpp index 3ef37e85c5..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) @@ -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) { @@ -766,7 +769,7 @@ void QXcbDrag::handle_xdnd_position(QPlatformWindow *w, const xcb_client_message } 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); @@ -1006,7 +1009,7 @@ void QXcbDrag::handleDrop(QPlatformWindow *, const xcb_client_message_event_t *e return; 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, @@ -1231,7 +1234,7 @@ void QXcbDrag::handleSelectionRequest(const xcb_selection_request_event_t *event bool QXcbDrag::dndEnable(QXcbWindow *w, bool on) { - qCDebug(lcQpaXDnd) << "dndEnable" << w << 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; 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 e32891e814..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(); @@ -337,11 +344,12 @@ 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(); @@ -422,14 +430,9 @@ QPlatformServices *QXcbIntegration::services() const return m_services.data(); } -Qt::KeyboardModifiers QXcbIntegration::queryKeyboardModifiers() const -{ - return m_connection->queryKeyboardModifiers(); -} - -QList<int> QXcbIntegration::possibleKeys(const QKeyEvent *e) const +QPlatformKeyMapper *QXcbIntegration::keyMapper() const { - return m_connection->keyboard()->possibleKeys(e); + return m_connection->keyboard(); } QStringList QXcbIntegration::themeNames() const @@ -472,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(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 @@ -585,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 9d62423071..860d195d13 100644 --- a/src/plugins/platforms/xcb/qxcbmime.cpp +++ b/src/plugins/platforms/xcb/qxcbmime.cpp @@ -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.size() * 2); + data->assign({reinterpret_cast<const char *>(mozUri.data()), mozUri.size() * 2}); } else if (atomName == "application/x-color"_L1) *dataFormat = 16; ret = true; @@ -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(); } } } diff --git a/src/plugins/platforms/xcb/qxcbwindow.cpp b/src/plugins/platforms/xcb/qxcbwindow.cpp index 584f1e5406..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,7 +96,7 @@ 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(); } QXcbScreen *QXcbWindow::initialScreen() const @@ -133,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; } @@ -223,6 +226,7 @@ enum : quint32 { void QXcbWindow::create() { + xcb_window_t old_m_window = m_window; destroy(); m_windowState = Qt::WindowNoState; @@ -231,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); @@ -272,11 +276,11 @@ void QXcbWindow::create() 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); } @@ -294,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()); } } @@ -388,6 +392,31 @@ void QXcbWindow::create() 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); @@ -465,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() @@ -472,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 @@ -489,6 +545,8 @@ 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); @@ -522,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(); @@ -641,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()) { @@ -654,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(); @@ -781,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() @@ -1014,7 +1094,7 @@ void QXcbWindow::setNetWmState(Qt::WindowFlags flags) 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(); @@ -1091,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::Atom_NET_WM_STATE_MAXIMIZED_HORZ), atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT)); - if (m_windowState & Qt::WindowFullScreen) + 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; @@ -1123,13 +1206,8 @@ void QXcbWindow::setWindowState(Qt::WindowStates state) } m_minimized = true; } - if (state & Qt::WindowMaximized) - setNetWmState(true, - atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_HORZ), - atom(QXcbAtom::Atom_NET_WM_STATE_MAXIMIZED_VERT)); - if (state & Qt::WindowFullScreen) - setNetWmState(true, atom(QXcbAtom::Atom_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); @@ -1371,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()); } @@ -1389,14 +1468,22 @@ 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) @@ -1411,7 +1498,7 @@ void QXcbWindow::requestActivateWindow() 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; @@ -1715,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); @@ -1726,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; @@ -1802,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())); @@ -1813,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()); } } @@ -1836,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); @@ -1880,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); } @@ -1901,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); @@ -1924,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); } @@ -1941,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) { @@ -1960,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); @@ -2285,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); @@ -2405,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) @@ -2443,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; } 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/platformthemes/gtk3/CMakeLists.txt b/src/plugins/platformthemes/gtk3/CMakeLists.txt index 89acc84f07..6d3c7bf3a2 100644 --- a/src/plugins/platformthemes/gtk3/CMakeLists.txt +++ b/src/plugins/platformthemes/gtk3/CMakeLists.txt @@ -23,6 +23,8 @@ qt_internal_add_plugin(QGtk3ThemePlugin 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 @@ -33,6 +35,13 @@ qt_internal_add_plugin(QGtk3ThemePlugin Qt::GuiPrivate ) +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 b8ba58d30e..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" diff --git a/src/plugins/platformthemes/gtk3/qgtk3interface.cpp b/src/plugins/platformthemes/gtk3/qgtk3interface.cpp index d802f2d1db..a35e211fbf 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3interface.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3interface.cpp @@ -289,15 +289,17 @@ QImage QGtk3Interface::qt_convert_gdk_pixbuf(GdkPixbuf *buf) const 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_ASSUME(gdk_pixbuf_get_bits_per_sample(buf) == 8); - Q_ASSUME(gdk_pixbuf_get_n_channels(buf) == 4); + 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_ARGB32); - return converted.copy(); // detatch to survive lifetime of 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); } /*! @@ -475,15 +477,18 @@ QBrush QGtk3Interface::brush(QGtkWidget wtype, QGtkColorSource source, GtkStateF \internal \brief Returns the name of the current GTK theme. */ -const QString QGtk3Interface::themeName() const +QString QGtk3Interface::themeName() const { - gchar *theme_name; - GtkSettings *settings = gtk_settings_get_default(); - if (!settings) - return QString(); + QString name; - g_object_get(settings, "gtk-theme-name", &theme_name, nullptr); - return QLatin1StringView(theme_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; } /*! @@ -537,13 +542,13 @@ inline constexpr QGtk3Interface::QGtkWidget QGtk3Interface::toWidgetType(QPlatfo 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_fixed; + 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_fixed; + 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; @@ -604,6 +609,25 @@ QFont QGtk3Interface::font(QPlatformTheme::Font type) const 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(); @@ -630,6 +654,7 @@ QFont QGtk3Interface::font(QPlatformTheme::Font type) const font.setFamily("monospace"_L1); } } + return font; } @@ -643,7 +668,7 @@ QIcon QGtk3Interface::fileIcon(const QFileInfo &fileInfo) const if (!file) return QIcon(); - GFileInfo *info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + 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); @@ -658,12 +683,11 @@ QIcon QGtk3Interface::fileIcon(const QFileInfo &fileInfo) const } GtkIconTheme *theme = gtk_icon_theme_get_default(); - GtkIconInfo *iconInfo = gtk_icon_theme_lookup_by_gicon(theme, icon, GTK_ICON_SIZE_BUTTON, + 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); - g_object_unref(icon); return QIcon(); } @@ -671,7 +695,6 @@ QIcon QGtk3Interface::fileIcon(const QFileInfo &fileInfo) const QImage image = qt_convert_gdk_pixbuf(buf); g_object_unref(file); g_object_unref(info); - g_object_unref(icon); g_object_unref(buf); return QIcon(QPixmap::fromImage(image)); } diff --git a/src/plugins/platformthemes/gtk3/qgtk3interface_p.h b/src/plugins/platformthemes/gtk3/qgtk3interface_p.h index b92adcd720..c43932a4fa 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3interface_p.h +++ b/src/plugins/platformthemes/gtk3/qgtk3interface_p.h @@ -134,7 +134,7 @@ public: QIcon fileIcon(const QFileInfo &fileInfo) const; // Return current GTK theme name - const QString themeName() const; + QString themeName() const; // Derive color scheme from default colors Qt::ColorScheme colorSchemeByColors() const; 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 index 8d6f3a4c14..2877b28590 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3storage.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3storage.cpp @@ -21,6 +21,9 @@ QT_BEGIN_NAMESPACE QGtk3Storage::QGtk3Storage() { m_interface.reset(new QGtk3Interface(this)); +#if QT_CONFIG(dbus) + m_portalInterface.reset(new QGtk3PortalInterface(this)); +#endif populateMap(); } @@ -273,7 +276,6 @@ void QGtk3Storage::clear() */ void QGtk3Storage::handleThemeChange() { - clear(); populateMap(); QWindowSystemInterface::handleThemeChange(); } @@ -331,21 +333,32 @@ 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 (m_themeName == newThemeName) + +#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(); - // Derive color scheme from theme name - m_colorScheme = newThemeName.contains("dark"_L1, Qt::CaseInsensitive) - ? Qt::ColorScheme::Dark : m_interface->colorSchemeByColors(); - if (m_themeName.isEmpty()) { - qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << m_colorScheme; + qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << newColorScheme; } else { - qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << m_colorScheme; + qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << newColorScheme; } + m_colorScheme = newColorScheme; m_themeName = newThemeName; // create standard mapping or load from Json file? @@ -524,90 +537,140 @@ void QGtk3Storage::createMapping() // System palette - // background color and calculate derivates - GTK(Default, Background, INSENSITIVE); - ADD(Normal, Window); - ADD(Normal, Button); - ADD(Normal, Base); - ADD(Inactive, Base); - ADD(Inactive, Window); - LIGHTER(Normal, Window, 125); - ADD(Normal, Light); - LIGHTER(Normal, Window, 70); - ADD(Normal, Shadow); - LIGHTER(Normal, Window, 80); - ADD(Normal, Dark); - GTK(button, Foreground, ACTIVE); - ADD(Normal, WindowText); - ADD(Inactive, WindowText); - LIGHTER(Normal, WindowText, 50); - ADD(Disabled, Text); - ADD(Disabled, WindowText); - ADD(Inactive, ButtonText); - GTK(button, Text, NORMAL); - ADD(Disabled, ButtonText); - // special background colors - GTK(Default, Background, SELECTED); - ADD(Disabled, Highlight); - ADD(Normal, Highlight); - GTK(entry, Foreground, SELECTED); - ADD(Normal, HighlightedText); - GTK(entry, Background, ACTIVE); - ADD(Inactive, HighlightedText); - // text color and friends - GTK(entry, Text, NORMAL); - ADD(Normal, ButtonText); - ADD(Normal, WindowText); - ADD(Disabled, 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); - ADD(All, ToolTipText); - MODIFY(Normal, Text, 100, 100, 100); - ADD(All, PlaceholderText, Light); - MODIFY(Normal, Text, -100, -100, -100); - ADD(All, PlaceholderText, Dark); - SAVE(SystemPalette); - CLEAR; - - // Checkbox and Radio Button - GTK(button, Text, ACTIVE); - ADD(Normal, Base, Dark); - GTK(Default, Background, NORMAL); - ADD(All, Base); - GTK(button, Text, NORMAL); - ADD(Normal, Base, Light); - SAVE(CheckBoxPalette); - SAVE(RadioButtonPalette); - CLEAR; - - // ComboBox, GroupBox, Frame - GTK(combo_box, Text, NORMAL); - ADD(Normal, ButtonText, Dark); - ADD(Normal, Text, Dark); - GTK(combo_box, Text, ACTIVE); - ADD(Normal, ButtonText, Light); - ADD(Normal, Text, Light); - SAVE(ComboBoxPalette); - SAVE(GroupBoxPalette); - CLEAR; - - // Menu bar - GTK(Default, Text, ACTIVE); - ADD(Normal, ButtonText); - SAVE(MenuPalette); - CLEAR; - - // LineEdit - GTK(Default, Background, NORMAL); - ADD(All, Base); - SAVE(TextLineEditPalette); - CLEAR; + { + // 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 diff --git a/src/plugins/platformthemes/gtk3/qgtk3storage_p.h b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h index 37c5bf57ff..45192263a9 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3storage_p.h +++ b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h @@ -16,6 +16,9 @@ // #include "qgtk3interface_p.h" +#if QT_CONFIG(dbus) +#include "qgtk3portalinterface_p.h" +#endif #include <QtCore/QJsonDocument> #include <QtCore/QCache> @@ -205,7 +208,9 @@ private: 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; diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp index 1df4cf94a3..9d23ba7e48 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp @@ -104,20 +104,6 @@ QGtk3Theme::QGtk3Theme() SETTING_CONNECT("gtk-cursor-theme-size"); #undef SETTING_CONNECT - /* Set XCURSOR_SIZE and XCURSOR_THEME for Wayland sessions */ - if (QGuiApplication::platformName().startsWith("wayland"_L1)) { - if (qEnvironmentVariableIsEmpty("XCURSOR_SIZE")) { - const int cursorSize = gtkSetting<gint>("gtk-cursor-theme-size"); - if (cursorSize > 0) - qputenv("XCURSOR_SIZE", QByteArray::number(cursorSize)); - } - if (qEnvironmentVariableIsEmpty("XCURSOR_THEME")) { - const QString cursorTheme = gtkSetting("gtk-cursor-theme-name"); - if (!cursorTheme.isEmpty()) - qputenv("XCURSOR_THEME", cursorTheme.toUtf8()); - } - } - m_storage.reset(new QGtk3Storage); } diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp index ec3872f174..1c162be8fc 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp @@ -169,19 +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 and requires the file to exist - // while current_name accepts just file name - QFileInfo selectedFileInfo(d->selectedFiles.first()); - if (selectedFileInfo.exists()) - options.insert("current_file"_L1, QFile::encodeName(d->selectedFiles.first()).append('\0')); - options.insert("current_name"_L1, selectedFileInfo.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 @@ -214,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) diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp index 2f46b53297..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() @@ -62,6 +63,17 @@ public: } } +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; Qt::ColorScheme colorScheme = Qt::ColorScheme::Unknown; @@ -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(); @@ -126,6 +138,11 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() const QXdgDesktopPortalThemePrivate::XdgColorschemePref xdgPref = static_cast<QXdgDesktopPortalThemePrivate::XdgColorschemePref>(dbusVariant.variant().toUInt()); 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 @@ -208,6 +225,8 @@ QVariant QXdgDesktopPortalTheme::themeHint(ThemeHint hint) const Qt::ColorScheme QXdgDesktopPortalTheme::colorScheme() const { Q_D(const QXdgDesktopPortalTheme); + if (d->colorScheme == Qt::ColorScheme::Unknown) + return d->baseTheme->colorScheme(); return d->colorScheme; } @@ -245,3 +264,5 @@ QString QXdgDesktopPortalTheme::standardButtonText(int button) const } QT_END_NAMESPACE + +#include "qxdgdesktopportaltheme.moc" 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/sqldrivers/.cmake.conf b/src/plugins/sqldrivers/.cmake.conf index 146dd2bd5a..10bc1fd407 100644 --- a/src/plugins/sqldrivers/.cmake.conf +++ b/src/plugins/sqldrivers/.cmake.conf @@ -1 +1 @@ -set(QT_REPO_MODULE_VERSION "6.6.0") +set(QT_REPO_MODULE_VERSION "6.8.0") diff --git a/src/plugins/sqldrivers/CMakeLists.txt b/src/plugins/sqldrivers/CMakeLists.txt index 7f44072da0..43abb00ad1 100644 --- a/src/plugins/sqldrivers/CMakeLists.txt +++ b/src/plugins/sqldrivers/CMakeLists.txt @@ -4,6 +4,14 @@ 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" diff --git a/src/plugins/sqldrivers/db2/qsql_db2.cpp b/src/plugins/sqldrivers/db2/qsql_db2.cpp index a18d7a84fa..9b6a06c378 100644 --- a/src/plugins/sqldrivers/db2/qsql_db2.cpp +++ b/src/plugins/sqldrivers/db2/qsql_db2.cpp @@ -305,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, @@ -505,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; } @@ -1706,3 +1704,5 @@ QString QDB2Driver::escapeIdentifier(const QString &identifier, IdentifierType) } 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 a21be56b07..b8f2d2561f 100644 --- a/src/plugins/sqldrivers/ibase/CMakeLists.txt +++ b/src/plugins/sqldrivers/ibase/CMakeLists.txt @@ -10,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 4284f9b811..f6eed5e227 100644 --- a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp +++ b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp @@ -8,6 +8,7 @@ #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> @@ -25,6 +26,8 @@ QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcIbase, "qt.sql.ibase") + using namespace Qt::StringLiterals; #define FBVERSION SQL_DIALECT_V6 @@ -115,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) { @@ -171,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; } @@ -205,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) @@ -400,6 +406,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 @@ -941,7 +983,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(); @@ -950,13 +992,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; } @@ -980,7 +1022,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; } @@ -994,7 +1036,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; } @@ -1030,9 +1072,9 @@ bool QIBaseResult::exec() if (d->inda) { const QList<QVariant> &values = boundValues(); 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; } for (qsizetype para = 0; para < values.count(); ++para) { @@ -1051,6 +1093,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: @@ -1114,8 +1162,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; } } @@ -1227,27 +1275,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; @@ -1282,30 +1330,11 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx) #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; @@ -1386,7 +1415,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; } @@ -1452,7 +1481,6 @@ QSqlRecord QIBaseResult::record() const f.setRequiredStatus(q.value(3).toBool() ? QSqlField::Required : QSqlField::Optional); } } - f.setSqlType(v.sqltype); rec.append(f); } return rec; @@ -1700,13 +1728,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 " @@ -1726,7 +1750,6 @@ QSqlRecord QIBaseDriver::record(const QString& tablename) const f.setPrecision(0); } f.setRequired(q.value(5).toInt() > 0); - f.setSqlType(type); rec.append(f); } @@ -1739,12 +1762,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 " @@ -1839,13 +1857,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; } @@ -1886,13 +1904,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; } @@ -1947,8 +1965,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; @@ -1974,3 +1992,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 index fd160fe477..303af7120c 100644 --- a/src/plugins/sqldrivers/mimer/CMakeLists.txt +++ b/src/plugins/sqldrivers/mimer/CMakeLists.txt @@ -13,6 +13,7 @@ qt_internal_add_plugin(QMimerSQLDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES MimerSQL::MimerSQL Qt::Core diff --git a/src/plugins/sqldrivers/mimer/qsql_mimer.cpp b/src/plugins/sqldrivers/mimer/qsql_mimer.cpp index 3037f10473..7f89e0a0d5 100644 --- a/src/plugins/sqldrivers/mimer/qsql_mimer.cpp +++ b/src/plugins/sqldrivers/mimer/qsql_mimer.cpp @@ -5,6 +5,7 @@ #include <qvariant.h> #include <qmetatype.h> #include <qdatetime.h> +#include <qloggingcategory.h> #include <qsqlerror.h> #include <qsqlfield.h> #include <qsqlindex.h> @@ -30,12 +31,15 @@ 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, @@ -157,6 +161,20 @@ static QSqlError qMakeError(const QString &err, const int errCode, QSqlError::Er 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) { } @@ -207,7 +225,6 @@ static MimerColumnTypes mimerMapColumnTypes(int32_t t) case MIMER_TIMESTAMP: return MimerColumnTypes::Timestamp; 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: @@ -227,6 +244,10 @@ static MimerColumnTypes mimerMapColumnTypes(int32_t t) 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: @@ -234,19 +255,17 @@ static MimerColumnTypes mimerMapColumnTypes(int32_t t) case MIMER_NATIVE_BIGINT_NULLABLE: case MIMER_NATIVE_BIGINT: return MimerColumnTypes::Long; - case MIMER_T_FLOAT: - case MIMER_FLOAT: - return MimerColumnTypes::Float; 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_INTEGER: case MIMER_NATIVE_SMALLINT_NULLABLE: case MIMER_NATIVE_SMALLINT: case MIMER_T_INTEGER: @@ -255,7 +274,7 @@ static MimerColumnTypes mimerMapColumnTypes(int32_t t) case MIMER_UUID: return MimerColumnTypes::Uuid; default: - qWarning() << "QMimerSQLDriver::mimerMapColumnTypes: Unknown data type: " << t; + qCWarning(lcMimer) << "QMimerSQLDriver::mimerMapColumnTypes: Unknown data type:" << t; } return MimerColumnTypes::Unknown; } @@ -292,6 +311,8 @@ static QMetaType::Type qDecodeMSQLType(int32_t t) 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; @@ -300,19 +321,18 @@ static QMetaType::Type qDecodeMSQLType(int32_t t) case MIMER_NATIVE_BIGINT_NULLABLE: case MIMER_NATIVE_BIGINT: return QMetaType::LongLong; - case MIMER_T_FLOAT: - case MIMER_FLOAT: - return QMetaType::Float; 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_INTEGER: + case MIMER_NATIVE_INTEGER: return QMetaType::Int; case MIMER_NATIVE_SMALLINT_NULLABLE: case MIMER_T_SMALLINT: @@ -327,7 +347,7 @@ static QMetaType::Type qDecodeMSQLType(int32_t t) case MIMER_UUID: return QMetaType::QUuid; default: - qWarning() << "QMimerSQLDriver::qDecodeMSQLType: Unknown data type: " << t; + qCWarning(lcMimer) << "QMimerSQLDriver::qDecodeMSQLType: Unknown data type:" << t; return QMetaType::UnknownType; } } @@ -393,7 +413,7 @@ static int32_t qLookupMimDataType(QStringView s) if (s == u"DOUBLE PRECISION") return MIMER_T_DOUBLE; if (s == u"INTEGER") - return MIMER_INTEGER; + return MIMER_T_INTEGER; if (s == u"SMALLINT") return MIMER_T_SMALLINT; if (s == u"DATE") @@ -406,7 +426,7 @@ static int32_t qLookupMimDataType(QStringView s) return MIMER_UUID; if (s == u"USER-DEFINED") return MIMER_DEFAULT_DATATYPE; - qWarning() << "QMimerSQLDriver::qLookupMimDataType: Unhandled data type: " << s; + qCWarning(lcMimer) << "QMimerSQLDriver::qLookupMimDataType: Unhandled data type:" << s; return MIMER_DEFAULT_DATATYPE; } @@ -590,7 +610,7 @@ QVariant QMimerSQLResult::data(int i) genericError, QSqlError::StatementError, nullptr)); return QVariant(); } - mType = MimerParameterType(d->statementhandle, static_cast<std::int16_t>(i) + 1); + mType = MimerParameterType(d->statementhandle, static_cast<std::int16_t>(i + 1)); } else { if (i >= MimerColumnCount(d->statementhandle)) { setLastError(qMakeError( @@ -599,23 +619,21 @@ QVariant QMimerSQLResult::data(int i) genericError, QSqlError::StatementError, nullptr)); return QVariant(); } - mType = MimerColumnType(d->statementhandle, static_cast<std::int16_t>(i) + 1); + 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); + 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, + 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(QCoreApplication::translate("QMimerSQLResult", - "Could not get date, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("date", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -623,12 +641,10 @@ QVariant QMimerSQLResult::data(int i) } case MimerColumnTypes::Time: { wchar_t timeString_w[maxTimeStringSize + 1]; - err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i) + 1, timeString_w, + 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(QCoreApplication::translate("QMimerSQLResult", - "Could not get time, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("time", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -642,14 +658,12 @@ QVariant QMimerSQLResult::data(int i) } case MimerColumnTypes::Timestamp: { wchar_t dateTimeString_w[maxTimestampStringSize + 1]; - err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i) + 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(QCoreApplication::translate("QMimerSQLResult", - "Could not get date time, column %1") - .arg(i), + qMakeError(msgCouldNotGet("date time", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -663,11 +677,9 @@ QVariant QMimerSQLResult::data(int i) } case MimerColumnTypes::Int: { int resInt; - err = MimerGetInt32(d->statementhandle, static_cast<std::int16_t>(i) + 1, &resInt); + err = MimerGetInt32(d->statementhandle, static_cast<std::int16_t>(i + 1), &resInt); if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError(QCoreApplication::translate( - "QMimerSQLResult", "Could not get int32, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("int32", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -675,23 +687,19 @@ QVariant QMimerSQLResult::data(int i) } case MimerColumnTypes::Long: { int64_t resLongLong; - err = MimerGetInt64(d->statementhandle, static_cast<std::int16_t>(i) + 1, &resLongLong); + err = MimerGetInt64(d->statementhandle, static_cast<std::int16_t>(i + 1), &resLongLong); if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError(QCoreApplication::translate( - "QMimerSQLResult", "Could not get int64, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("int64", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } - return QString::number(resLongLong).toLongLong(); + return (qlonglong)resLongLong; } case MimerColumnTypes::Boolean: { - err = MimerGetBoolean(d->statementhandle, static_cast<std::int16_t>(i) + 1); + err = MimerGetBoolean(d->statementhandle, static_cast<std::int16_t>(i + 1)); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not get boolean, column %1") - .arg(i), + qMakeError(msgCouldNotGet("boolean", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -699,11 +707,9 @@ QVariant QMimerSQLResult::data(int i) } case MimerColumnTypes::Float: { float resFloat; - err = MimerGetFloat(d->statementhandle, static_cast<std::int16_t>(i) + 1, &resFloat); + err = MimerGetFloat(d->statementhandle, static_cast<std::int16_t>(i + 1), &resFloat); if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError(QCoreApplication::translate( - "QMimerSQLResult", "Could not get float, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("float", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -711,12 +717,10 @@ QVariant QMimerSQLResult::data(int i) } case MimerColumnTypes::Double: { double resDouble; - err = MimerGetDouble(d->statementhandle, static_cast<std::int16_t>(i) + 1, &resDouble); + err = MimerGetDouble(d->statementhandle, static_cast<std::int16_t>(i + 1), &resDouble); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not get double, column %1") - .arg(i), + qMakeError(msgCouldNotGet("double", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -726,7 +730,7 @@ QVariant QMimerSQLResult::data(int i) case QSql::LowPrecisionInt64: return static_cast<qint64>(resDouble); case QSql::LowPrecisionDouble: - return resDouble; + return static_cast<qreal>(resDouble); case QSql::HighPrecision: return QString::number(resDouble, 'g', 17); } @@ -735,17 +739,15 @@ QVariant QMimerSQLResult::data(int i) case MimerColumnTypes::Binary: { QByteArray byteArray; // Get size - err = MimerGetBinary(d->statementhandle, static_cast<std::int16_t>(i) + 1, NULL, 0); + 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, + err = MimerGetBinary(d->statementhandle, static_cast<std::int16_t>(i + 1), byteArray.data(), err); } if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not get binary, column %1") - .arg(i), + qMakeError(msgCouldNotGet("binary", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -754,7 +756,7 @@ QVariant QMimerSQLResult::data(int i) case MimerColumnTypes::Blob: { QByteArray byteArray; size_t size; - err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i) + 1, &size, + err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i + 1), &size, &d->lobhandle); if (MIMER_SUCCEEDED(err)) { constexpr size_t maxSize = lobChunkMaxSizeFetch; @@ -768,53 +770,48 @@ QVariant QMimerSQLResult::data(int i) byteArray.append(QByteArray::fromRawData(blobchar.data(), bytesToReceive)); left_to_return -= bytesToReceive; if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError( - QCoreApplication::translate("QMimerSQLResult", - "Could not get blob, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("BLOB", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } } } else { - setLastError(qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not get blob, column %1") - .arg(i), + 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, + 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, + 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, + 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(QCoreApplication::translate("QMimerSQLResult", - "Could not get string, column %1") - .arg(i), - err, QSqlError::StatementError, d->drv_d_func())); + 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, + err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i + 1), &size, &d->lobhandle); if (MIMER_SUCCEEDED(err)) { constexpr size_t maxSize = lobChunkMaxSizeFetch; @@ -829,29 +826,22 @@ QVariant QMimerSQLResult::data(int i) returnString.append(QString::fromWCharArray(clobstring_w.data())); left_to_return -= bytesToReceive; if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError( - QCoreApplication::translate("QMimerSQLResult", - "Could not get clob, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("CLOB", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } } return returnString; } - setLastError(qMakeError( - QCoreApplication::translate("QMimerSQLResult", "Could not get clob, column %1") - .arg(i), + 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); + err = MimerGetUUID(d->statementhandle, static_cast<std::int16_t>(i + 1), uuidChar); if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not get uuid, column %1") - .arg(i), + setLastError(qMakeError(msgCouldNotGet("UUID", i), err, QSqlError::StatementError, d->drv_d_func())); return QVariant(QMetaType(type), nullptr); } @@ -871,7 +861,7 @@ QVariant QMimerSQLResult::data(int i) bool QMimerSQLResult::isNull(int index) { Q_D(const QMimerSQLResult); - const int32_t rc = MimerIsNull(d->statementhandle, static_cast<std::int16_t>(index) + 1); + 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") @@ -924,12 +914,11 @@ QSqlRecord QMimerSQLResult::record() const 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, + 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 int32_t mType = MimerColumnType(d->statementhandle, static_cast<std::int16_t>(i + 1)); const QMetaType::Type type = qDecodeMSQLType(mType); - field.setSqlType(mType); field.setMetaType(QMetaType(type)); field.setValue(QVariant(field.metaType())); // field.setPrecision(); Should be implemented once the Mimer API can give this @@ -1014,9 +1003,7 @@ bool QMimerSQLResult::exec() err = MimerSetNull(d->statementhandle, i + 1); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set null, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("null", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1031,9 +1018,7 @@ bool QMimerSQLResult::exec() err = MimerSetInt32(d->statementhandle, i + 1, val.toInt(&convertOk)); if (!convertOk || !MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set int32, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("int32", i), convertOk ? err : genericError, QSqlError::StatementError, convertOk ? d->drv_d_func() : nullptr)); return false; @@ -1045,9 +1030,7 @@ bool QMimerSQLResult::exec() err = MimerSetInt64(d->statementhandle, i + 1, val.toLongLong(&convertOk)); if (!convertOk || !MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set int64, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("int64", i), convertOk ? err : genericError, QSqlError::StatementError, convertOk ? d->drv_d_func() : nullptr)); return false; @@ -1059,9 +1042,7 @@ bool QMimerSQLResult::exec() err = MimerSetFloat(d->statementhandle, i + 1, val.toFloat(&convertOk)); if (!convertOk || !MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set float, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("float", i), convertOk ? err : genericError, QSqlError::StatementError, convertOk ? d->drv_d_func() : nullptr)); return false; @@ -1073,9 +1054,7 @@ bool QMimerSQLResult::exec() err = MimerSetDouble(d->statementhandle, i + 1, val.toDouble(&convertOk)); if (!convertOk || !MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set double, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("double", i), convertOk ? err : genericError, QSqlError::StatementError, convertOk ? d->drv_d_func() : nullptr)); return false; @@ -1088,9 +1067,7 @@ bool QMimerSQLResult::exec() err = MimerSetBinary(d->statementhandle, i + 1, binArr.data(), size); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set binary, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("binary", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1100,9 +1077,7 @@ bool QMimerSQLResult::exec() err = MimerSetBoolean(d->statementhandle, i + 1, val.toBool() == true ? 1 : 0); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate( - "QMimerSQLResult", "Could not set boolean, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("boolean", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1116,24 +1091,22 @@ bool QMimerSQLResult::exec() err = MimerSetUUID(d->statementhandle, i + 1, uuid); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set UUID, parameter %1") - .arg(i), + 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(QCoreApplication::translate("QMimerSQLResult", - "Could not set string, parameter %1") - .arg(i), - err, QSqlError::StatementError, d->drv_d_func())); + qMakeError(msgCouldNotSet( + mimDataType == MimerColumnTypes::Numeric ? "numeric" : "string", i), + err, QSqlError::StatementError, d->drv_d_func())); return false; } break; @@ -1142,9 +1115,7 @@ bool QMimerSQLResult::exec() err = MimerSetString8(d->statementhandle, i + 1, val.toString().toUtf8().constData()); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set date, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("date", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1159,9 +1130,7 @@ bool QMimerSQLResult::exec() timeVal.toString(timeFormatString).toUtf8().constData()); if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set time, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("time", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1176,10 +1145,7 @@ bool QMimerSQLResult::exec() d->statementhandle, i + 1, val.toDateTime().toString(dateTimeFormatString).toUtf8().constData()); if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError( - QCoreApplication::translate("QMimerSQLResult", - "Could not set datetime, parameter %1") - .arg(i), + setLastError(qMakeError(msgCouldNotSet("datetime", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1206,10 +1172,7 @@ bool QMimerSQLResult::exec() } if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate( - "QMimerSQLResult", - "Could not set BLOB byte array, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("BLOB byte array", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1218,10 +1181,7 @@ bool QMimerSQLResult::exec() } } if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError( - QCoreApplication::translate("QMimerSQLResult", - "Could not set BLOB byte array, parameter %1") - .arg(i), + setLastError(qMakeError(msgCouldNotSet("BLOB byte array", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1258,10 +1218,7 @@ bool QMimerSQLResult::exec() pos += maxSize - step_back; } if (!MIMER_SUCCEEDED(err)) { - setLastError(qMakeError( - QCoreApplication::translate("QMimerSQLResult", - "Could not set CLOB, parameter %1") - .arg(i), + setLastError(qMakeError(msgCouldNotSet("CLOB", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1272,9 +1229,7 @@ bool QMimerSQLResult::exec() } if (!MIMER_SUCCEEDED(err)) { setLastError( - qMakeError(QCoreApplication::translate("QMimerSQLResult", - "Could not set CLOB, parameter %1") - .arg(i), + qMakeError(msgCouldNotSet("CLOB", i), err, QSqlError::StatementError, d->drv_d_func())); return false; } @@ -1335,7 +1290,7 @@ bool QMimerSQLResult::execBatch(bool arrayBind) if (bindValueType(i) == QSql::Out || bindValueType(i) == QSql::InOut) { setLastError(qMakeError(QCoreApplication::translate( "QMimerSQLResult", - "Only input parameter can be used in batch operations"), + "Only input parameters can be used in batch operations"), genericError, QSqlError::StatementError, nullptr)); d->execBatch = false; return false; @@ -1349,6 +1304,7 @@ bool QMimerSQLResult::execBatch(bool arrayBind) 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())); @@ -1650,3 +1606,5 @@ void QMimerSQLDriverPrivate::splitTableQualifier(const QString &qualifiedName, Q } QT_END_NAMESPACE + +#include "moc_qsql_mimer.cpp" diff --git a/src/plugins/sqldrivers/mysql/CMakeLists.txt b/src/plugins/sqldrivers/mysql/CMakeLists.txt index fb28abd91a..2e3d028584 100644 --- a/src/plugins/sqldrivers/mysql/CMakeLists.txt +++ b/src/plugins/sqldrivers/mysql/CMakeLists.txt @@ -14,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 diff --git a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp index 1cf6798cd9..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> @@ -51,6 +53,8 @@ struct QT_MYSQL_TIME QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcMysql, "qt.sql.mysql") + using namespace Qt::StringLiterals; class QMYSQLDriverPrivate : public QSqlDriverPrivate @@ -97,9 +101,15 @@ static inline QVariant qDateTimeFromString(QString &val) #else if (val.isEmpty()) return QVariant(QDateTime()); + + // 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) - // TIMESTAMPS have the format yyyyMMddhhmmss 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 } @@ -118,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 @@ -266,7 +288,6 @@ static QSqlField qToField(MYSQL_FIELD *field) 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; } @@ -407,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; } @@ -543,7 +564,7 @@ QVariant QMYSQLResult::data(int field) { Q_D(QMYSQLResult); if (!isSelect() || field >= d->fields.size()) { - qWarning("QMYSQLResult::data: column %d out of range", field); + qCWarning(lcMysql, "QMYSQLResult::data: column %d out of range", field); return QVariant(); } @@ -575,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 @@ -832,28 +853,6 @@ void QMYSQLResult::virtual_hook(int id, void *data) QSqlResult::virtual_hook(id, data); } -static QT_MYSQL_TIME *toMySqlDate(QDate date, QTime time, int type) -{ - Q_ASSERT(type == QMetaType::QTime || type == QMetaType::QDate - || type == QMetaType::QDateTime); - - auto myTime = new QT_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); @@ -886,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; @@ -919,9 +918,8 @@ bool QMYSQLResult::exec() return false; } - if (mysql_stmt_param_count(d->stmt) > 0 && - mysql_stmt_param_count(d->stmt) == (uint)values.size()) { - + 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); @@ -944,25 +942,39 @@ bool QMYSQLResult::exec() case QMetaType::QTime: case QMetaType::QDate: case QMetaType::QDateTime: { - QT_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; + } + + 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; @@ -1003,7 +1015,11 @@ bool QMYSQLResult::exec() } } +#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)); @@ -1074,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 @@ -1181,9 +1197,11 @@ static void setOptionFlag(uint &optionFlags, QStringView opt) else if (opt == "CLIENT_ODBC"_L1) optionFlags |= CLIENT_ODBC; else if (opt == "CLIENT_SSL"_L1) - qWarning("QMYSQLDriver: MYSQL_OPT_SSL_KEY, MYSQL_OPT_SSL_CERT and MYSQL_OPT_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) @@ -1204,6 +1222,28 @@ static bool setOptionBool(MYSQL *mysql, mysql_option option, QStringView v) 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; @@ -1218,7 +1258,8 @@ static bool setOptionProtocol(MYSQL *mysql, mysql_option option, QStringView v) else if (v == "DEFAULT"_L1 || v == "MYSQL_PROTOCOL_DEFAULT"_L1) proto = MYSQL_PROTOCOL_DEFAULT; else - qWarning() << "Unknown protocol '" << v << "' - using MYSQL_PROTOCOL_DEFAULT"; + qCWarning(lcMysql, "Unknown protocol '%ls' - using MYSQL_PROTOCOL_DEFAULT", + qUtf16Printable(QString(v))); return mysql_options(mysql, option, &proto) == 0; } @@ -1259,6 +1300,12 @@ bool QMYSQLDriver::open(const QString &db, {"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}, @@ -1271,8 +1318,9 @@ bool QMYSQLDriver::open(const QString &db, for (const mysqloptions &opt : options) { if (key == opt.key) { if (!opt.func(d->mysql, opt.option, value)) { - qWarning("QMYSQLDriver::open: Could not set connect option value '%s' to '%s'", - key.toLocal8Bit().constData(), value.toLocal8Bit().constData()); + qCWarning(lcMysql, "QMYSQLDriver::open: Could not set connect option value " + "'%ls' to '%ls'", + qUtf16Printable(QString(key)), qUtf16Printable(QString(value))); } return true; } @@ -1303,8 +1351,8 @@ bool QMYSQLDriver::open(const QString &db, else if (val == "TRUE"_L1 || val == "1"_L1) setOptionFlag(optionFlags, key); else - qWarning("QMYSQLDriver::open: Illegal connect option value '%s'", - sv.toLocal8Bit().constData()); + qCWarning(lcMysql, "QMYSQLDriver::open: Illegal connect option value '%ls'", + qUtf16Printable(QString(sv))); } else { setOptionFlag(optionFlags, sv); } @@ -1356,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())) { @@ -1371,6 +1420,9 @@ bool QMYSQLDriver::open(const QString &db, d->preparedQuerysEnabled = checkPreparedQueries(d->mysql); d->dbName = db; + if (d->preparedQuerysEnabled) + setUtcTimeZone(d->mysql); + #if QT_CONFIG(thread) mysql_thread_init(); #endif @@ -1446,20 +1498,38 @@ QSqlIndex QMYSQLDriver::primaryIndex(const QString &tablename) const QSqlRecord QMYSQLDriver::record(const QString &tablename) const { Q_D(const QMYSQLDriver); - const QString table = stripDelimiters(tablename, QSqlDriver::TableName); - - QSqlRecord info; if (!isOpen()) - return info; - MYSQL_RES *r = mysql_list_fields(d->mysql, table.toUtf8().constData(), nullptr); - if (!r) - return info; - - MYSQL_FIELD *field; - while ((field = mysql_fetch_field(r))) - info.append(qToField(field)); - mysql_free_result(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); + } + } + } + return r; } QVariant QMYSQLDriver::handle() const @@ -1472,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")) { @@ -1487,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")) { @@ -1502,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")) { @@ -1539,7 +1609,7 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons 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: @@ -1548,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' + diff --git a/src/plugins/sqldrivers/oci/qsql_oci.cpp b/src/plugins/sqldrivers/oci/qsql_oci.cpp index b670efc15b..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> @@ -30,14 +31,7 @@ #define _int64 __int64 #endif - #include <oci.h> -#ifdef max -#undef max -#endif -#ifdef min -#undef min -#endif #include <stdlib.h> @@ -58,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 @@ -280,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 @@ -440,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 { @@ -556,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 @@ -592,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) @@ -660,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); } @@ -728,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); @@ -745,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; } @@ -844,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()) { @@ -853,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: @@ -908,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, @@ -1079,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; @@ -1317,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; @@ -1436,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; @@ -1514,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 @@ -1540,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) { @@ -1800,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; } } @@ -1821,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() @@ -1829,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"); } @@ -1889,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; } @@ -2003,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; } @@ -2018,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; } @@ -2037,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; } @@ -2129,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; @@ -2160,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 @@ -2198,8 +2188,8 @@ static void qParseOpts(const QString &options, QOCIDriverPrivate *d) 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 QStringView opt = tmp.left(idx); @@ -2219,12 +2209,12 @@ static void qParseOpts(const QString &options, QOCIDriverPrivate *d) } else if (val == "OCI_SYSOPER"_L1) { d->authMode = OCI_SYSOPER; } else if (val != "OCI_DEFAULT"_L1) { - qWarning("QOCIDriver::parseArgs: Unsupported value for OCI_AUTH_MODE: '%s'", - val.toLocal8Bit().constData()); + 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())); } } } @@ -2321,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)); @@ -2370,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, @@ -2391,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, @@ -2411,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, @@ -2691,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; @@ -2767,3 +2757,5 @@ int QOCIDriver::maximumIdentifierLength(IdentifierType type) const } QT_END_NAMESPACE + +#include "moc_qsql_oci_p.cpp" diff --git a/src/plugins/sqldrivers/odbc/CMakeLists.txt b/src/plugins/sqldrivers/odbc/CMakeLists.txt index df091f9687..7812aced2c 100644 --- a/src/plugins/sqldrivers/odbc/CMakeLists.txt +++ b/src/plugins/sqldrivers/odbc/CMakeLists.txt @@ -16,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 diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index 1a57e8fcc6..976911d458 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -10,6 +10,7 @@ #include <qcoreapplication.h> #include <qdatetime.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qmath.h> #include <qsqlerror.h> #include <qsqlfield.h> @@ -26,6 +27,8 @@ 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 @@ -41,6 +44,29 @@ static constexpr SQLSMALLINT TABLENAMESIZE = 128; //Map Qt parameter types to ODBC types static constexpr SQLSMALLINT qParamType[4] = { SQL_PARAM_INPUT, SQL_PARAM_INPUT, SQL_PARAM_OUTPUT, SQL_PARAM_INPUT_OUTPUT }; +class SqlStmtHandle +{ +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) { @@ -73,7 +99,7 @@ QStringConverter::Encoding encodingForSqlTChar() "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; QStringEncoder enc(encodingForSqlTChar()); @@ -88,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; @@ -109,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) const; - DefaultCase defaultCase() 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; @@ -228,7 +257,7 @@ static QList<DiagRecord> qWarnODBCHandle(int handleType, SQLHANDLE handle) description.resize(msgLen + 1); // incl. \0 termination continue; } - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { + if (SQL_SUCCEEDED(r)) { result.push_back({fromSQLTCHAR(description, msgLen), fromSQLTCHAR(state), QString::number(nativeCode)}); @@ -268,6 +297,8 @@ static DiagRecord combineRecords(const QList<DiagRecord> &records) 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); } @@ -291,7 +322,11 @@ static QString errorStringFromDiagRecords(const QList<DiagRecord>& records) template<class T> static void qSqlWarning(const QString &message, T &&val) { - qWarning() << message << "\tError:" << errorStringFromDiagRecords(qODBCWarn(val)); + const auto addMsg = errorStringFromDiagRecords(qODBCWarn(val)); + if (addMsg.isEmpty()) + qCWarning(lcOdbc) << message; + else + qCWarning(lcOdbc) << message << "\tError:" << addMsg; } static QSqlError qMakeError(const QString &err, @@ -385,7 +420,7 @@ static QVariant getStringDataImpl(SQLHANDLE hStmt, SQLUSMALLINT column, qsizetyp targetType, SQLPOINTER(buf.data()), SQLINTEGER(buf.size() * sizeof(CT)), &lengthIndicator); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { + if (SQL_SUCCEEDED(r)) { if (lengthIndicator == SQL_NULL_DATA) { return {}; } @@ -415,7 +450,7 @@ static QVariant getStringDataImpl(SQLHANDLE hStmt, SQLUSMALLINT column, qsizetyp } else if (r == SQL_NO_DATA) { break; } else { - qSqlWarning("qGetStringData: Error while fetching data:"_L1, hStmt); + qSqlWarning("QODBC::getStringData: Error while fetching data"_L1, hStmt); return {}; } } @@ -457,7 +492,8 @@ static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) &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; @@ -472,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)); @@ -501,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>()); @@ -521,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) @@ -541,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>()); @@ -557,16 +593,14 @@ 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) @@ -578,7 +612,6 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, const QODBCDriverPrivate* 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) @@ -589,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; @@ -607,8 +631,7 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess SQLSMALLINT nullable; SQLRETURN r = SQL_ERROR; QVarLengthArray<SQLTCHAR, COLNAMESIZE> colName(COLNAMESIZE); - errorMessage->clear(); - r = SQLDescribeCol(hStmt, + r = SQLDescribeCol(p->hStmt, i+1, colName.data(), SQLSMALLINT(colName.size()), &colNameLen, @@ -618,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, @@ -631,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) @@ -647,10 +670,10 @@ 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)); + f.setAutoValue(isAutoValue(p->hStmt, i)); QVarLengthArray<SQLTCHAR, TABLENAMESIZE> tableName(TABLENAMESIZE); SQLSMALLINT tableNameLen; - r = SQLColAttribute(hStmt, + r = SQLColAttribute(p->hStmt, i + 1, SQL_DESC_BASE_TABLE_NAME, tableName.data(), @@ -679,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'"'; @@ -688,7 +711,19 @@ QChar QODBCDriverPrivate::quoteChar() return quote; } -static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, const QString &val) +SQLRETURN QODBCDriverPrivate::sqlFetchNext(const SqlStmtHandle &hStmt) const +{ + return sqlFetchNext(hStmt.handle()); +} + +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, @@ -697,101 +732,106 @@ static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, co } -bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) +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) { + } else if (opt == "SQL_ATTR_CURRENT_CATALOG"_L1) { r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_CURRENT_CATALOG, val); - } else if (opt.toUpper() == "SQL_ATTR_METADATA_ID"_L1) { - if (val.toUpper() == "SQL_TRUE"_L1) { + } 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) { + } else if (opt == "SQL_ATTR_TRACEFILE"_L1) { r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_TRACEFILE, val); - } else if (opt.toUpper() == "SQL_ATTR_TRACE"_L1) { - if (val.toUpper() == "SQL_OPT_TRACE_OFF"_L1) { + } 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; } @@ -799,60 +839,65 @@ bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) 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; } const QList<QStringView> l = QStringView(qualifier).split(u'.'); switch (l.count()) { case 1: - table = qualifier; + table = adjustName(qualifier); break; case 2: - schema = l.at(0).toString(); - table = l.at(1).toString(); + schema = adjustName(l.at(0).toString()); + table = adjustName(l.at(1).toString()); break; case 3: - catalog = l.at(0).toString(); - schema = l.at(1).toString(); - table = l.at(2).toString(); + catalog = adjustName(l.at(0).toString()); + schema = adjustName(l.at(1).toString()); + table = adjustName(l.at(2).toString()); break; default: - qSqlWarning(QString::fromLatin1("QODBCDriver::splitTableQualifier: Unable to split table qualifier '%1'") + 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; } /* @@ -861,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; } //////////////////////////////////////////////////////////////////////////// @@ -890,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); } } @@ -935,7 +975,7 @@ 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)); @@ -948,7 +988,7 @@ bool QODBCResult::reset (const QString& query) encoded.data(), SQLINTEGER(encoded.size())); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r!= SQL_NO_DATA) { + if (!SQL_SUCCEEDED(r) && r!= SQL_NO_DATA) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to execute statement"), QSqlError::StatementError, d)); return false; @@ -956,7 +996,7 @@ 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; @@ -1025,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)); @@ -1122,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) @@ -1158,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>()); @@ -1171,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>()); @@ -1184,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 @@ -1248,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; } @@ -1289,7 +1329,7 @@ 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)); @@ -1621,7 +1661,7 @@ bool QODBCResult::exec() } } r = SQLExecute(d->hStmt); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r != SQL_NO_DATA) { + 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)); @@ -1630,7 +1670,7 @@ 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; @@ -1762,8 +1802,7 @@ bool QODBCResult::nextResult() SQLRETURN r = SQLMoreResults(d->hStmt); if (r != SQL_SUCCESS) { if (r == SQL_SUCCESS_WITH_INFO) { - QString message = errorStringFromDiagRecords(qODBCWarn(d)); - qWarning() << "QODBCResult::nextResult():" << message; + qSqlWarning("QODBCResult::nextResult:"_L1, d); } else { if (r != SQL_NO_DATA) setLastError(qMakeError(QCoreApplication::translate("QODBCResult", @@ -1882,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(); @@ -1889,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; @@ -1901,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(); @@ -1917,17 +1968,17 @@ 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, 1024> connOut(1024); @@ -1941,7 +1992,7 @@ bool QODBCDriver::open(const QString & db, /*SQL_DRIVER_NOPROMPT*/0); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(tr("Unable to connect"), QSqlError::ConnectionError, d)); setOpenError(true); cleanup(); @@ -1962,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) { @@ -2020,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; } @@ -2030,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; } @@ -2040,39 +2092,36 @@ 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); + 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 auto statements = { - "select 'test'"_L1, - "values('test')"_L1, - "select 'test' from dual"_L1, + 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, encoded.data(), SQLINTEGER(encoded.size())); + 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, 10> buffer(10); - r = SQLGetData(hStmt, 1, SQL_C_WCHAR, buffer.data(), buffer.size() * sizeof(SQLWCHAR), NULL); + 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 @@ -2102,9 +2151,10 @@ bool QODBCDriverPrivate::checkDriver() const return false; } if (sup == SQL_FALSE) { - qWarning () << "QODBCDriver::open: Warning - Driver doesn't support all needed functionality (" - << func - << ").\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; } } @@ -2119,8 +2169,9 @@ bool QODBCDriverPrivate::checkDriver() const return false; } if (sup == SQL_FALSE) { - qWarning() << "QODBCDriver::checkDriver: Warning - Driver doesn't support some non-critical functions (" - << func << ')'; + qSqlWarning(("QODBCDriver::checkDriver: Driver doesn't support some " + "non-critical functions (func id %1)."_L1) + .arg(QString::number(func)), this); return true; } } @@ -2139,7 +2190,7 @@ void QODBCDriverPrivate::checkSchemaUsage() (SQLPOINTER) &val, sizeof(val), NULL); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) useSchema = (val != 0); } @@ -2154,7 +2205,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)); if (serverType.contains("PostgreSQL"_L1, Qt::CaseInsensitive)) dbmsType = QSqlDriver::PostgreSQL; @@ -2172,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; @@ -2183,9 +2234,10 @@ 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); } } @@ -2198,31 +2250,26 @@ void QODBCDriverPrivate::checkHasMultiResults() driverResponse.data(), SQLSMALLINT(driverResponse.size() * sizeof(SQLTCHAR)), &length); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + 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 @@ -2234,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); @@ -2254,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, @@ -2272,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, @@ -2307,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; @@ -2333,7 +2377,7 @@ QStringList QODBCDriver::tables(QSql::TableType type) const { auto joinedTableTypeString = toSQLTCHAR(tableType.join(u',')); - r = SQLTables(hStmt, + r = SQLTables(hStmt.handle(), nullptr, 0, nullptr, 0, nullptr, 0, @@ -2341,36 +2385,21 @@ QStringList QODBCDriver::tables(QSql::TableType type) const } if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver::tables Unable to execute table list"_L1, d); - - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + qSqlWarning("QODBCDriver::tables Unable to execute table list"_L1, + hStmt.handle()); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r != SQL_NO_DATA) { - qSqlWarning("QODBCDriver::tables failed to retrieve table/view list: ("_L1 - + QString::number(r) + u':', - 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) { - tl.append(qGetStringData(hStmt, 2, -1, d->unicode).toString()); - - 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; } @@ -2383,41 +2412,23 @@ 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; 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); + 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, + r = SQLPrimaryKeys(hStmt.handle(), catalog.isEmpty() ? nullptr : c.data(), c.size(), schema.isEmpty() ? nullptr : s.data(), s.size(), t.data(), t.size()); @@ -2430,7 +2441,7 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const auto c = toSQLTCHAR(catalog); auto s = toSQLTCHAR(schema); auto t = toSQLTCHAR(table); - r = SQLSpecialColumns(hStmt, + r = SQLSpecialColumns(hStmt.handle(), SQL_BEST_ROWID, catalog.isEmpty() ? nullptr : c.data(), c.size(), schema.isEmpty() ? nullptr : s.data(), s.size(), @@ -2439,44 +2450,31 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const 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).toString(); // 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).toString(); // column name - idxName = qGetStringData(hStmt, 5, -1, d->unicode).toString(); // 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; } @@ -2487,41 +2485,24 @@ QSqlRecord QODBCDriver::record(const QString& tablename) const if (!isOpen()) return fil; - SQLHANDLE hStmt; - QString 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); - - 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); + + 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, + r = SQLColumns(hStmt.handle(), catalog.isEmpty() ? nullptr : c.data(), c.size(), schema.isEmpty() ? nullptr : s.data(), s.size(), t.data(), t.size(), @@ -2529,32 +2510,14 @@ QSqlRecord QODBCDriver::record(const QString& tablename) const 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); + 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; } @@ -2566,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'-' + diff --git a/src/plugins/sqldrivers/psql/CMakeLists.txt b/src/plugins/sqldrivers/psql/CMakeLists.txt index d392f4dfa5..2f55ab4950 100644 --- a/src/plugins/sqldrivers/psql/CMakeLists.txt +++ b/src/plugins/sqldrivers/psql/CMakeLists.txt @@ -16,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 diff --git a/src/plugins/sqldrivers/psql/qsql_psql.cpp b/src/plugins/sqldrivers/psql/qsql_psql.cpp index 81d7e14444..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(qsizetype 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; @@ -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) @@ -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 945de0e63b..2bb1fc64eb 100644 --- a/src/plugins/sqldrivers/qt_cmdline.cmake +++ b/src/plugins/sqldrivers/qt_cmdline.cmake @@ -3,7 +3,7 @@ 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) diff --git a/src/plugins/sqldrivers/sqlite/CMakeLists.txt b/src/plugins/sqldrivers/sqlite/CMakeLists.txt index add9dff9fd..4203a5c437 100644 --- a/src/plugins/sqldrivers/sqlite/CMakeLists.txt +++ b/src/plugins/sqldrivers/sqlite/CMakeLists.txt @@ -10,10 +10,12 @@ 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 @@ -37,6 +39,11 @@ if(QT_FEATURE_system_sqlite) qt_internal_force_macos_intel_arch(QSQLiteDriverPlugin) endif() +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 ../../../3rdparty/sqlite/sqlite3.c diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp index 2933c68a9d..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); - if (type == QSqlDriver::TableName) - res.replace(u'.', "\".\""_L1); - res = u'"' + res + u'"'; - } - 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 { @@ -210,7 +250,6 @@ void QSQLiteResultPrivate::initColumns(bool emptyResultset) } QSqlField fld(colName, QMetaType(fieldType), tableName); - fld.setSqlType(stp); rInf.append(fld); } } @@ -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"), @@ -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,12 +1058,13 @@ 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; } @@ -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 f84a256bc8..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) diff --git a/src/plugins/styles/CMakeLists.txt b/src/plugins/styles/CMakeLists.txt index a16c7c4c9a..b809042c14 100644 --- a/src/plugins/styles/CMakeLists.txt +++ b/src/plugins/styles/CMakeLists.txt @@ -8,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/qandroidstyle.cpp b/src/plugins/styles/android/qandroidstyle.cpp index ef625c6eb5..64940ed4d8 100644 --- a/src/plugins/styles/android/qandroidstyle.cpp +++ b/src/plugins/styles/android/qandroidstyle.cpp @@ -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"); diff --git a/src/plugins/styles/mac/qmacstyle_mac.mm b/src/plugins/styles/mac/qmacstyle_mac.mm index 890dddbd1b..3f57f284e6 100644 --- a/src/plugins/styles/mac/qmacstyle_mac.mm +++ b/src/plugins/styles/mac/qmacstyle_mac.mm @@ -162,45 +162,6 @@ QVector<QPointer<QObject> > QMacStylePrivate::scrollBars; bool isDarkMode() { return QGuiApplicationPrivate::platformTheme()->colorScheme() == Qt::ColorScheme::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; -} - #if QT_CONFIG(tabwidget) /* Since macOS 10.14 AppKit is using transparency more extensively, especially for the @@ -429,12 +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. - const auto controlSize = slider.controlSize; - [slider initWithFrame:sl->rect.toCGRect()]; - slider.controlSize = controlSize; + 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; } @@ -2208,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; @@ -3424,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)) @@ -3915,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) @@ -4115,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()); @@ -4135,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(); @@ -5385,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]; } @@ -5489,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) @@ -5552,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); @@ -5625,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 @@ -5981,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; } diff --git a/src/plugins/styles/mac/qmacstyle_mac_p_p.h b/src/plugins/styles/mac/qmacstyle_mac_p_p.h index e3f1e39d19..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> diff --git a/src/plugins/styles/windowsvista/CMakeLists.txt b/src/plugins/styles/modernwindows/CMakeLists.txt index 372a944638..985bce3a2d 100644 --- a/src/plugins/styles/windowsvista/CMakeLists.txt +++ b/src/plugins/styles/modernwindows/CMakeLists.txt @@ -5,12 +5,13 @@ ## 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 qwindowsvistaanimation.cpp qwindowsvistaanimation_p.h qwindowsthemedata.cpp qwindowsthemedata_p.h @@ -21,5 +22,6 @@ qt_internal_add_plugin(QWindowsVistaStylePlugin uxtheme Qt::Core Qt::Gui + Qt::GuiPrivate Qt::WidgetsPrivate ) 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..784f491681 --- /dev/null +++ b/src/plugins/styles/modernwindows/qwindows11style.cpp @@ -0,0 +1,2140 @@ +// 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> +#include <QtWidgets/qmdiarea.h> +#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) +{ +} + +/*! + \internal + Constructs a QWindows11Style object. +*/ +QWindows11Style::QWindows11Style(QWindows11StylePrivate &dd) : QWindowsVistaStyle(dd) +{ +} + +/*! + 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; + 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 && !qobject_cast<QMdiArea *>(widget)) { + 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 && !qobject_cast<QMdiArea *>(widget)) { + 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/windowsvista/qwindowsthemedata.cpp b/src/plugins/styles/modernwindows/qwindowsthemedata.cpp index 44569e054d..44569e054d 100644 --- a/src/plugins/styles/windowsvista/qwindowsthemedata.cpp +++ b/src/plugins/styles/modernwindows/qwindowsthemedata.cpp diff --git a/src/plugins/styles/windowsvista/qwindowsthemedata_p.h b/src/plugins/styles/modernwindows/qwindowsthemedata_p.h index 5de2bcbdea..5de2bcbdea 100644 --- a/src/plugins/styles/windowsvista/qwindowsthemedata_p.h +++ b/src/plugins/styles/modernwindows/qwindowsthemedata_p.h diff --git a/src/plugins/styles/windowsvista/qwindowsvistaanimation.cpp b/src/plugins/styles/modernwindows/qwindowsvistaanimation.cpp index ac9a5ad8c0..ac9a5ad8c0 100644 --- a/src/plugins/styles/windowsvista/qwindowsvistaanimation.cpp +++ b/src/plugins/styles/modernwindows/qwindowsvistaanimation.cpp diff --git a/src/plugins/styles/windowsvista/qwindowsvistaanimation_p.h b/src/plugins/styles/modernwindows/qwindowsvistaanimation_p.h index 817fa5f4b5..817fa5f4b5 100644 --- a/src/plugins/styles/windowsvista/qwindowsvistaanimation_p.h +++ b/src/plugins/styles/modernwindows/qwindowsvistaanimation_p.h diff --git a/src/plugins/styles/windowsvista/qwindowsvistastyle.cpp b/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp index 4ae2adde35..beee1d6f31 100644 --- a/src/plugins/styles/windowsvista/qwindowsvistastyle.cpp +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle.cpp @@ -12,6 +12,8 @@ #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> @@ -19,6 +21,8 @@ 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 @@ -48,23 +52,9 @@ static const int windowsRightBorder = 15; // right border on windows # define CMDLGS_DISABLED 4 #endif -/* \internal - Checks if we should use Vista style , or if we should - fall back to Windows style. -*/ -// Theme names matching the QWindowsVistaStylePrivate::Theme enumeration. -static const wchar_t *themeNames[QWindowsVistaStylePrivate::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"WINDOW", L"STATUS", L"TREEVIEW" -}; - // QWindowsVistaStylePrivate ------------------------------------------------------------------------- // Static initializations HWND QWindowsVistaStylePrivate::m_vistaTreeViewHelper = nullptr; -HTHEME QWindowsVistaStylePrivate::m_themes[NThemes]; bool QWindowsVistaStylePrivate::useVistaTheme = false; Q_CONSTINIT QBasicAtomicInt QWindowsVistaStylePrivate::ref = Q_BASIC_ATOMIC_INITIALIZER(-1); // -1 based refcounting @@ -181,7 +171,6 @@ void QWindowsVistaStylePrivate::init(bool force) ref.ref(); useVista(true); - std::fill(m_themes, m_themes + NThemes, nullptr); } /* \internal @@ -237,11 +226,8 @@ int QWindowsVistaStylePrivate::pixelMetricFromSystemDp(QStyle::PixelMetric pm, c : 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 widget && (widget->windowType() == Qt::Tool) - ? GetSystemMetrics(SM_CYSMCAPTION) + GetSystemMetrics(SM_CXSIZEFRAME) - : GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CXSIZEFRAME); - } + 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: @@ -304,31 +290,15 @@ void QWindowsVistaStylePrivate::cleanupVistaTreeViewTheming() */ void QWindowsVistaStylePrivate::cleanupHandleMap() { - for (auto &theme : m_themes) { - if (theme) { - CloseThemeData(theme); - theme = nullptr; - } - } + QWindowsThemeCache::clearAllThemeCaches(); QWindowsVistaStylePrivate::cleanupVistaTreeViewTheming(); } HTHEME QWindowsVistaStylePrivate::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 && QWindowsVistaStylePrivate::initVistaTreeViewTheming()) - hwnd = QWindowsVistaStylePrivate::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]; + if (theme == VistaTreeViewTheme && QWindowsVistaStylePrivate::initVistaTreeViewTheming()) + hwnd = QWindowsVistaStylePrivate::m_vistaTreeViewHelper; + return QWindowsThemeCache::createTheme(theme, hwnd); } QBackingStore *QWindowsVistaStylePrivate::backingStoreForWidget(const QWidget *widget) @@ -353,8 +323,7 @@ HDC QWindowsVistaStylePrivate::hdcForWidgetBackingStore(const QWidget *widget) QString QWindowsVistaStylePrivate::themeName(int theme) { - return theme >= 0 && theme < NThemes - ? QString::fromWCharArray(themeNames[theme]) : QString(); + return QWindowsThemeCache::themeName(theme); } bool QWindowsVistaStylePrivate::isItemViewDelegateLineEdit(const QWidget *widget) @@ -1305,6 +1274,14 @@ QWindowsVistaStyle::QWindowsVistaStyle() : QWindowsStyle(*new QWindowsVistaStyle } /*! + \internal + Constructs a QWindowsStyle object. +*/ +QWindowsVistaStyle::QWindowsVistaStyle(QWindowsVistaStylePrivate &dd) : QWindowsStyle(dd) +{ +} + +/*! Destructor. */ QWindowsVistaStyle::~QWindowsVistaStyle() = default; @@ -1422,17 +1399,17 @@ void QWindowsVistaStyle::drawPrimitive(PrimitiveElement element, const QStyleOpt theme = OpenThemeData(nullptr, L"Edit"); partId = EP_EDITBORDER_NOSCROLL; - if (oldState & State_MouseOver) + if (oldState & State_HasFocus) + fromState = ETS_SELECTED; + else if (oldState & State_MouseOver) fromState = ETS_HOT; - else if (oldState & State_HasFocus) - fromState = ETS_FOCUSED; else fromState = ETS_NORMAL; - if (state & State_MouseOver) + if (state & State_HasFocus) + toState = ETS_SELECTED; + else if (state & State_MouseOver) toState = ETS_HOT; - else if (state & State_HasFocus) - toState = ETS_FOCUSED; else toState = ETS_NORMAL; @@ -1937,10 +1914,10 @@ void QWindowsVistaStyle::drawPrimitive(PrimitiveElement element, const QStyleOpt 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; + else if (state & State_MouseOver) + stateId = ETS_HOT; QWindowsThemeData theme(widget, painter, QWindowsVistaStylePrivate::EditTheme, EP_EDITBORDER_NOSCROLL, stateId, option->rect); @@ -2931,9 +2908,6 @@ void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption else theme.stateId = bullet ? MC_BULLETNORMAL: MC_CHECKMARKNORMAL; d->drawBackground(theme); - } else if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11 - && !act) { - painter->fillRect(checkRect, menuitem->palette.highlight().color().lighter(200)); } } @@ -3132,7 +3106,7 @@ void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption QColor textShadow = qRgb(GetRValue(textShadowRef), GetGValue(textShadowRef), GetBValue(textShadowRef)); painter->setPen(textShadow); drawItemText(painter, titleRect.adjusted(1, 1, 1, 1), - Qt::AlignLeft | Qt::AlignBottom, dwOpt->palette, + Qt::AlignLeft | Qt::AlignBottom | Qt::TextHideMnemonic, dwOpt->palette, dwOpt->state & State_Enabled, titleText); } @@ -3140,7 +3114,7 @@ void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption QColor textColor = qRgb(GetRValue(captionText), GetGValue(captionText), GetBValue(captionText)); painter->setPen(textColor); drawItemText(painter, titleRect, - Qt::AlignLeft | Qt::AlignBottom, dwOpt->palette, + Qt::AlignLeft | Qt::AlignBottom | Qt::TextHideMnemonic, dwOpt->palette, dwOpt->state & State_Enabled, titleText); painter->setFont(oldFont); painter->setPen(oldPen); @@ -3155,7 +3129,7 @@ void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption 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, + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextHideMnemonic, dwOpt->palette, dwOpt->state & State_Enabled, titleText, QPalette::WindowText); @@ -4488,8 +4462,7 @@ QRect QWindowsVistaStyle::subControlRect(ComplexControl control, const QStyleOpt #endif // QT_CONFIG(mdiarea) default: - return visualRect(option->direction, option->rect, - QWindowsStyle::subControlRect(control, option, subControl, widget)); + return QWindowsStyle::subControlRect(control, option, subControl, widget); } return visualRect(option->direction, option->rect, rect); @@ -4642,8 +4615,8 @@ void QWindowsVistaStyle::polish(QWidget *widget) buttonFont.setFamilies(QStringList{QLatin1String("Segoe UI")}); widget->setFont(buttonFont); QPalette pal = widget->palette(); - pal.setColor(QPalette::ButtonText, QColor(21, 28, 85)); - pal.setColor(QPalette::BrightText, QColor(7, 64, 229)); + 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) @@ -4657,6 +4630,7 @@ void QWindowsVistaStyle::polish(QWidget *widget) 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)) { @@ -4781,12 +4755,12 @@ void QWindowsVistaStyle::polish(QPalette &pal) { Q_D(QWindowsVistaStyle); - if (qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark) { + 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->lightSystemPalette(pal); + nativeWindowsApp->populateLightSystemPalette(pal); } QPixmapCache::clear(); diff --git a/src/plugins/styles/windowsvista/qwindowsvistastyle_p.h b/src/plugins/styles/modernwindows/qwindowsvistastyle_p.h index c437b3e434..ff5300beca 100644 --- a/src/plugins/styles/windowsvista/qwindowsvistastyle_p.h +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle_p.h @@ -59,7 +59,8 @@ public: 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/windowsvista/qwindowsvistastyle_p_p.h b/src/plugins/styles/modernwindows/qwindowsvistastyle_p_p.h index 2c38ff4b3b..053e98b68d 100644 --- a/src/plugins/styles/windowsvista/qwindowsvistastyle_p_p.h +++ b/src/plugins/styles/modernwindows/qwindowsvistastyle_p_p.h @@ -77,6 +77,7 @@ #endif #include <qlabel.h> #include <qheaderview.h> +#include <uxtheme.h> QT_BEGIN_NAMESPACE @@ -115,7 +116,6 @@ public: 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 int pixelMetricFromSystemDp(QStyle::PixelMetric pm, const QStyleOption *option = nullptr, const QWidget *widget = nullptr); static int fixedPixelMetric(QStyle::PixelMetric pm); @@ -169,7 +169,6 @@ private: int bufferH = 0; static HWND m_vistaTreeViewHelper; - static HTHEME m_themes[NThemes]; }; QT_END_NAMESPACE 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/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/openssl/qdtls_openssl.cpp b/src/plugins/tls/openssl/qdtls_openssl.cpp index 78288d8dd1..fc07a29ec8 100644 --- a/src/plugins/tls/openssl/qdtls_openssl.cpp +++ b/src/plugins/tls/openssl/qdtls_openssl.cpp @@ -1,10 +1,6 @@ // 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_p.h> #include "qsslsocket_openssl_symbols_p.h" diff --git a/src/plugins/tls/openssl/qsslcontext_openssl.cpp b/src/plugins/tls/openssl/qsslcontext_openssl.cpp index ef0e63911a..75c192bd01 100644 --- a/src/plugins/tls/openssl/qsslcontext_openssl.cpp +++ b/src/plugins/tls/openssl/qsslcontext_openssl.cpp @@ -697,7 +697,9 @@ 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(); 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 746a620d67..4aa9ca6fb1 100644 --- a/src/plugins/tls/openssl/qsslsocket_openssl_symbols.cpp +++ b/src/plugins/tls/openssl/qsslsocket_openssl_symbols.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2017 The Qt Company Ltd. +// Copyright (C) 2017 The Qt Company Ltd. // Copyright (C) 2014 BlackBerry Limited. All rights reserved. // Copyright (C) 2016 Richard J. Moore <rich@kde.org> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -159,7 +159,6 @@ DEFINEFUNC3(int, CRYPTO_memcmp, const void * in_a, in_a, const void * in_b, in_b 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) @@ -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 @@ -856,7 +874,6 @@ bool q_resolveOpenSslSymbols() 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) @@ -1015,7 +1032,6 @@ bool q_resolveOpenSslSymbols() 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) @@ -1066,6 +1082,16 @@ bool q_resolveOpenSslSymbols() #endif // OPENSSL_VERSION_MAJOR >= 3 #ifndef OPENSSL_NO_DEPRECATED_3_0 + RESOLVEFUNC(DH_new) + RESOLVEFUNC(DH_free) + RESOLVEFUNC(DH_check) + RESOLVEFUNC(DH_get0_pqg) + + RESOLVEFUNC(d2i_DHparams) + RESOLVEFUNC(i2d_DHparams) + + RESOLVEFUNC(PEM_read_bio_DHparams) + RESOLVEFUNC(EVP_PKEY_assign) RESOLVEFUNC(EVP_PKEY_cmp) @@ -1210,11 +1236,6 @@ bool q_resolveOpenSslSymbols() #endif // dtls RESOLVEFUNC(CRYPTO_malloc) - RESOLVEFUNC(DH_new) - RESOLVEFUNC(DH_free) - RESOLVEFUNC(d2i_DHparams) - RESOLVEFUNC(i2d_DHparams) - RESOLVEFUNC(DH_check) RESOLVEFUNC(BN_bin2bn) #ifndef OPENSSL_NO_EC diff --git a/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h b/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h index d020527476..a93c110b3f 100644 --- a/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h +++ b/src/plugins/tls/openssl/qsslsocket_openssl_symbols_p.h @@ -233,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) @@ -391,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, @@ -504,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 diff --git a/src/plugins/tls/openssl/qtls_openssl.cpp b/src/plugins/tls/openssl/qtls_openssl.cpp index 031ccd9d15..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 diff --git a/src/plugins/tls/openssl/qtlsbackend_openssl.cpp b/src/plugins/tls/openssl/qtlsbackend_openssl.cpp index 5ce5f45a5b..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> @@ -204,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; } @@ -363,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) { @@ -392,10 +394,9 @@ 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 : std::as_const(certFiles)) diff --git a/src/plugins/tls/openssl/qtlskey_openssl.cpp b/src/plugins/tls/openssl/qtlskey_openssl.cpp index 8f54fda7fa..294fc2ffcd 100644 --- a/src/plugins/tls/openssl/qtlskey_openssl.cpp +++ b/src/plugins/tls/openssl/qtlskey_openssl.cpp @@ -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/schannel/CMakeLists.txt b/src/plugins/tls/schannel/CMakeLists.txt index 9a53bf9ee6..a7f7fcd99f 100644 --- a/src/plugins/tls/schannel/CMakeLists.txt +++ b/src/plugins/tls/schannel/CMakeLists.txt @@ -29,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 b230f2f787..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,39 +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; - 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) @@ -298,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, @@ -319,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) @@ -383,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. @@ -391,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) @@ -459,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 @@ -507,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(); @@ -722,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)) { @@ -784,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")); @@ -838,78 +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 | defaultCredsFlag(), - 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 | defaultCredsFlag(), // 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; @@ -1114,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; @@ -1283,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); @@ -1435,6 +1718,7 @@ void TlsCryptographSchannel::reset() deallocateContext(); freeCredentialsHandle(); // in case we already had one (@future: session resumption requires re-use) + cipherInfo = {}; connectionInfo = {}; streamSizes = {}; @@ -1484,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(); @@ -1637,8 +1923,12 @@ void TlsCryptographSchannel::transmit() 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(?) + // 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 @@ -1809,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 @@ -1994,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!"); @@ -2108,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 @@ -2259,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/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 507a3a727a..97be46866d 100644 --- a/src/plugins/tls/shared/qasn1element.cpp +++ b/src/plugins/tls/shared/qasn1element.cpp @@ -6,6 +6,7 @@ #include <QtCore/qdatastream.h> #include <QtCore/qdatetime.h> +#include <QtCore/qtimezone.h> #include <QtCore/qlist.h> #include <QDebug> #include <private/qtools_p.h> @@ -224,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(date.year() >= rfc2459CenturyStart); + Q_ASSERT(date.year() < 100 + rfc2459CenturyStart); - Q_ASSERT(result.date().year() >= 1950); - Q_ASSERT(result.date().year() <= 2049); + 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/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 e700552c43..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; diff --git a/src/plugins/tracing/CMakeLists.txt b/src/plugins/tracing/CMakeLists.txt index 9840b59ecd..823e11c174 100644 --- a/src/plugins/tracing/CMakeLists.txt +++ b/src/plugins/tracing/CMakeLists.txt @@ -12,12 +12,21 @@ endfunction(make_includable) make_includable(metadata_template.txt metadata_template.h) qt_internal_add_plugin(QCtfTracePlugin - SHARED 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 - Qt6::Core Qt6::CorePrivate + 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/qctflib.cpp b/src/plugins/tracing/qctflib.cpp index e0f4db489a..fe3946d27c 100644 --- a/src/plugins/tracing/qctflib.cpp +++ b/src/plugins/tracing/qctflib.cpp @@ -12,14 +12,18 @@ #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"); +Q_LOGGING_CATEGORY(lcDebugTrace, "qt.core.ctf", QtWarningMsg) static const size_t packetHeaderSize = 24 + 6 * 8 + 4; static const size_t packetSize = 4096; @@ -29,14 +33,27 @@ static const char traceMetadataTemplate[] = ; static const size_t traceMetadataSize = sizeof(traceMetadataTemplate); +static inline QString allLiteral() { return QStringLiteral("all"); } +static inline QString defaultLiteral() { return QStringLiteral("default"); } + + template <typename T> -QByteArray &operator<<(QByteArray &arr, T val) +static QByteArray &operator<<(QByteArray &arr, T val) { static_assert(std::is_arithmetic_v<T>); - arr.append((char *)&val, sizeof(val)); + 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() @@ -48,111 +65,182 @@ QCtfLib *QCtfLibImpl::instance() void QCtfLibImpl::cleanup() { - if (s_instance) - delete s_instance; + delete s_instance; + s_instance = nullptr; } -QCtfLibImpl::QCtfLibImpl() +void QCtfLibImpl::handleSessionChange() { - QString location = QString::fromUtf8(qgetenv("QTRACE_LOCATION")); - if (location.isEmpty()) { - qCWarning (lcDebugTrace) << "QTRACE_LOCATION not set"; - return; - } - - // Check if the location is writable - FILE *file = nullptr; - file = fopen(qPrintable(location + "/metadata"_L1), "w+b"); - if (!file) { - qCWarning (lcDebugTrace) << "Unable to write to location"; - return; - } - fclose(file); - - const QString filename = location + QStringLiteral("/session.json"); - file = fopen(qPrintable(filename), "rb"); - if (!file) { - qCWarning (lcDebugTrace) << "unable to open session file: " << filename; - m_location = location; - m_session.tracepoints.append(QStringLiteral("all")); - m_session.name = QStringLiteral("default"); - } else { - fseek(file, 0, SEEK_END); - long pos = ftell(file); - fseek(file, 0, SEEK_SET); - QByteArray data(pos, Qt::Uninitialized); - long size = (long)fread(data.data(), pos, 1, file); - fclose(file); - if (size != 1) - return; - QJsonDocument json(QJsonDocument::fromJson(data)); - - QJsonObject obj = json.object(); - QJsonValue value = *obj.begin(); - if (value.isNull() || !value.isArray()) - return; - m_session.name = obj.begin().key(); - QJsonArray arr = value.toArray(); - for (auto var : arr) - m_session.tracepoints.append(var.toString()); + m_sessionChanged = true; +} - m_location = location + QStringLiteral("/ust"); - std::filesystem::create_directory(qPrintable(m_location), qPrintable(location)); +void QCtfLibImpl::handleStatusChange(QCtfServer::ServerStatus status) +{ + switch (status) { + case QCtfServer::Error: { + m_serverClosed = true; + } break; + default: + break; } - m_session.all = m_session.tracepoints.contains(QStringLiteral("all")); +} - auto datetime = QDateTime::currentDateTime(); - QString mhn = QSysInfo::machineHostName(); +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"), datetime.toString()); + metadata.replace(QStringLiteral("$CREATION_TIME"), m_datetime.toString(Qt::ISODate)); metadata.replace(QStringLiteral("$HOST_NAME"), mhn); - metadata.replace(QStringLiteral("$CLOCK_FREQUENCY"), m_timer.isMonotonic() ? QStringLiteral("1000000000") : QStringLiteral("1000")); - metadata.replace(QStringLiteral("$CLOCK_NAME"), m_timer.isMonotonic() ? QStringLiteral("monotonic") : QStringLiteral("system")); - metadata.replace(QStringLiteral("$CLOCK_TYPE"), m_timer.isMonotonic() ? QStringLiteral("Monotonic clock") : QStringLiteral("System clock")); - metadata.replace(QStringLiteral("$CLOCK_OFFSET"), QString::number(datetime.toMSecsSinceEpoch() * 1000000)); -#if Q_BYTE_ORDER == Q_BIG_ENDIAN - metadata.replace(QStringLiteral("$ENDIANNESS"), QStringLiteral("be")); -#else - metadata.replace(QStringLiteral("$ENDIANNESS"), QStringLiteral("le")); -#endif + 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) { - FILE *file = nullptr; - file = fopen(qPrintable(m_location + "/metadata"_L1), overwrite ? "w+b": "ab"); - if (!file) - return; + 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); + 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); + // 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; - file = fopen(ch.channelName, "ab"); - if (file) { + 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; - /* Uuid is array of bytes hence implicitely big endian. */ - packet << qToBigEndian(s_TraceUuid.data1); - packet << qToBigEndian(s_TraceUuid.data2); - packet << qToBigEndian(s_TraceUuid.data3); - for (int i = 0; i < 8; i++) - packet << s_TraceUuid.data4[i]; + packet.append(QByteArrayView(s_TraceUuid.toBytes())); packet << quint32(0); packet << ch.minTimestamp; @@ -168,27 +256,61 @@ void QCtfLibImpl::writeCtfPacket(QCtfLibImpl::Channel &ch) Q_ASSERT(ch.data.size() + packetHeaderSize + ch.threadNameLength <= packetSize); Q_ASSERT(packet.size() == qsizetype(packetHeaderSize + ch.threadNameLength)); - 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); + 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); } -bool QCtfLibImpl::tracepointEnabled(const QCtfTracePointEvent &point) +void QCtfLibImpl::removeChannel(Channel *ch) { - return m_session.all || m_session.tracepoints.contains(point.provider.provider); + const QMutexLocker lock(&m_mutex); + m_channels.removeOne(ch); } -QCtfLibImpl::Channel::~Channel() +bool QCtfLibImpl::tracepointEnabled(const QCtfTracePointEvent &point) { - if (data.size()) - QCtfLibImpl::writeCtfPacket(*this); + 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) @@ -205,12 +327,10 @@ event { }; }; */ - QString ret; - ret = QStringLiteral("event {\n name = \"") + provider + QLatin1Char(':') + name + QStringLiteral("\";\n"); - ret += QStringLiteral(" id = ") + QString::number(eventId) + QStringLiteral(";\n"); - ret += QStringLiteral(" stream_id = 0;\n loglevel = 13;\n fields := struct {\n "); - ret += metadata + QStringLiteral("\n };\n};\n"); - return ret; + 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) @@ -235,6 +355,8 @@ void QCtfLibImpl::doTracepoint(const QCtfTracePointEvent &point, const QByteArra QCtfTracePointPrivate *priv = point.d; quint64 timestamp = 0; QThread *thread = nullptr; + if (m_streaming && m_serverClosed) + return; { QMutexLocker lock(&m_mutex); if (!priv->metadataWritten) { @@ -267,12 +389,10 @@ void QCtfLibImpl::doTracepoint(const QCtfTracePointEvent &point, const QByteArra 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]); - FILE *f = nullptr; - f = fopen(ch.channelName, "wb"); - if (f) - fclose(f); ch.minTimestamp = ch.maxTimestamp = timestamp; ch.thread = thread; ch.threadIndex = m_threadIndices[thread]; diff --git a/src/plugins/tracing/qctflib_p.h b/src/plugins/tracing/qctflib_p.h index 081dda1d04..297d38ee50 100644 --- a/src/plugins/tracing/qctflib_p.h +++ b/src/plugins/tracing/qctflib_p.h @@ -25,8 +25,8 @@ #include <qset.h> #include <qthreadstorage.h> #include <qthread.h> -#include <qmutex.h> #include <qloggingcategory.h> +#include "qctfserver_p.h" QT_BEGIN_NAMESPACE @@ -40,7 +40,7 @@ struct QCtfTracePointPrivate bool metadataWritten = false; }; -class QCtfLibImpl : public QCtfLib +class QCtfLibImpl : public QCtfLib, public QCtfServer::ServerCallback { struct Session { @@ -60,6 +60,7 @@ class QCtfLibImpl : public QCtfLib QByteArray threadName; quint32 threadNameLength = 0; bool locked = false; + QCtfLibImpl *impl = nullptr; Channel() { memset(channelName, 0, sizeof(channelName)); @@ -78,15 +79,24 @@ public: 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); - static void writeCtfPacket(Channel &ch); + 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; @@ -98,9 +108,16 @@ private: 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 diff --git a/src/plugins/tracing/qctfplugin.cpp b/src/plugins/tracing/qctfplugin.cpp index 8f2245bb28..93e508e199 100644 --- a/src/plugins/tracing/qctfplugin.cpp +++ b/src/plugins/tracing/qctfplugin.cpp @@ -22,9 +22,13 @@ public: ~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) @@ -51,8 +55,9 @@ public: } private: bool m_cleanup = false; + bool *m_shutdown = nullptr; }; -#include "qctfplugin.moc" - QT_END_NAMESPACE + +#include "qctfplugin.moc" 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 |