diff options
Diffstat (limited to 'src/plugins/platforms')
344 files changed, 10552 insertions, 5537 deletions
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..aaf62dd7e7 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 ##################################################################### @@ -9,27 +9,25 @@ qt_find_package(EGL) qt_internal_add_plugin(QAndroidIntegrationPlugin OUTPUT_NAME qtforandroid PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES android + DEFAULT_IF "android" IN_LIST QT_QPA_PLATFORMS SOURCES androidcontentfileengine.cpp androidcontentfileengine.h androiddeadlockprotector.h androidjniaccessibility.cpp androidjniaccessibility.h - androidjniclipboard.cpp androidjniclipboard.h androidjniinput.cpp androidjniinput.h androidjnimain.cpp androidjnimain.h androidjnimenu.cpp androidjnimenu.h - androidsurfaceclient.h main.cpp qandroidassetsfileenginehandler.cpp qandroidassetsfileenginehandler.h qandroideventdispatcher.cpp qandroideventdispatcher.h qandroidinputcontext.cpp qandroidinputcontext.h qandroidplatformaccessibility.cpp qandroidplatformaccessibility.h - qandroidplatformbackingstore.cpp qandroidplatformbackingstore.h qandroidplatformclipboard.cpp qandroidplatformclipboard.h qandroidplatformdialoghelpers.cpp qandroidplatformdialoghelpers.h qandroidplatformfiledialoghelper.cpp qandroidplatformfiledialoghelper.h qandroidplatformfontdatabase.cpp qandroidplatformfontdatabase.h qandroidplatformforeignwindow.cpp qandroidplatformforeignwindow.h + qandroidplatformiconengine.cpp qandroidplatformiconengine.h qandroidplatformintegration.cpp qandroidplatformintegration.h qandroidplatformmenu.cpp qandroidplatformmenu.h qandroidplatformmenubar.cpp qandroidplatformmenubar.h @@ -42,8 +40,20 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin qandroidplatformtheme.cpp qandroidplatformtheme.h qandroidplatformwindow.cpp qandroidplatformwindow.h qandroidsystemlocale.cpp qandroidsystemlocale.h - DEFINES - QT_USE_QSTRINGBUILDER + androidwindowembedding.cpp androidwindowembedding.h + androidbackendregister.cpp androidbackendregister.h + NO_UNITY_BUILD_SOURCES + # Conflicting symbols and macros with androidjnimain.cpp + # TODO: Unify the usage of FIND_AND_CHECK_CLASS, and similar + # macros. Q_JNI_FIND_AND_CHECK_CLASS in `qjnihelpers_p.h` + # seems to be doing most of the work already. + androidjnimenu.cpp + qandroidinputcontext.cpp + androidjniaccessibility.cpp + qandroidplatformdialoghelpers.cpp + # Conflicting JNI classes, and types + androidcontentfileengine.cpp + qandroidplatformintegration.cpp INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR} ${QtBase_SOURCE_DIR}/src/3rdparty/android @@ -75,4 +85,7 @@ qt_internal_extend_target(QAndroidIntegrationPlugin CONDITION QT_FEATURE_vulkan SOURCES qandroidplatformvulkaninstance.cpp qandroidplatformvulkaninstance.h qandroidplatformvulkanwindow.cpp qandroidplatformvulkanwindow.h + NO_UNITY_BUILD_SOURCES + # To avoid undefined symbols due to missing VK_USE_PLATFORM_ANDROID_KHR + qandroidplatformvulkaninstance.cpp qandroidplatformvulkanwindow.cpp ) diff --git a/src/plugins/platforms/android/androidbackendregister.cpp b/src/plugins/platforms/android/androidbackendregister.cpp new file mode 100644 index 0000000000..bfd86138aa --- /dev/null +++ b/src/plugins/platforms/android/androidbackendregister.cpp @@ -0,0 +1,52 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "androidbackendregister.h" + +#include "androidjnimain.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcAndroidBackendRegister, "qt.qpa.androidbackendregister") + +Q_DECLARE_JNI_CLASS(BackendRegister, "org/qtproject/qt/android/BackendRegister"); + +bool AndroidBackendRegister::registerNatives() +{ + return QtJniTypes::BackendRegister::registerNativeMethods( + { Q_JNI_NATIVE_SCOPED_METHOD(registerBackend, AndroidBackendRegister), + Q_JNI_NATIVE_SCOPED_METHOD(unregisterBackend, AndroidBackendRegister) }); +} + +void AndroidBackendRegister::registerBackend(JNIEnv *, jclass, jclass interfaceClass, + jobject interface) +{ + if (AndroidBackendRegister *reg = QtAndroid::backendRegister()) { + const QJniObject classObject(static_cast<jobject>(interfaceClass)); + QString name = classObject.callMethod<jstring>("getName").toString(); + name.replace('.', '/'); + + QMutexLocker lock(®->m_registerMutex); + reg->m_register[name] = QJniObject(interface); + } else { + qCWarning(lcAndroidBackendRegister) + << "AndroidBackendRegister pointer is null, cannot register functionality"; + } +} + +void AndroidBackendRegister::unregisterBackend(JNIEnv *, jclass, jclass interfaceClass) +{ + if (AndroidBackendRegister *reg = QtAndroid::backendRegister()) { + const QJniObject classObject(static_cast<jobject>(interfaceClass)); + QString name = classObject.callMethod<jstring>("getName").toString(); + name.replace('.', '/'); + + QMutexLocker lock(®->m_registerMutex); + reg->m_register.remove(name); + } else { + qCWarning(lcAndroidBackendRegister) + << "AndroidBackendRegister pointer is null, cannot unregister functionality"; + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidbackendregister.h b/src/plugins/platforms/android/androidbackendregister.h new file mode 100644 index 0000000000..c186f7e107 --- /dev/null +++ b/src/plugins/platforms/android/androidbackendregister.h @@ -0,0 +1,67 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef ANDROIDBACKENDREGISTER_H +#define ANDROIDBACKENDREGISTER_H + +#include <type_traits> + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> +#include <QtCore/qjniobject.h> + +#include <QtCore/qstring.h> +#include <QtCore/qmap.h> +#include <QtCore/qmutex.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcAndroidBackendRegister) + +template <typename T> +using ValidInterfaceType = std::enable_if_t<std::is_base_of_v<QtJniTypes::JObjectBase, T>, bool>; + +class AndroidBackendRegister +{ +public: + static bool registerNatives(); + + template <typename T, ValidInterfaceType<T> = true> + [[nodiscard]] T getInterface() + { + QMutexLocker lock(&m_registerMutex); + return m_register.value(QString(QtJniTypes::Traits<T>::className().data())); + } + + template <typename Object> + using IsObjectType = + typename std::disjunction<std::is_base_of<QJniObject, Object>, + std::is_base_of<QtJniTypes::JObjectBase, Object>>; + + template <typename Interface, typename Ret, typename... Args, + ValidInterfaceType<Interface> = true> + auto callInterface(const char *func, Args... args) + { + if (const auto obj = getInterface<Interface>(); obj.isValid()) + return obj.template callMethod<Ret, Args...>(func, std::forward<Args>(args)...); + + if constexpr (IsObjectType<Ret>::value) + return Ret(QJniObject()); + if constexpr (!std::is_same_v<Ret, void>) + return Ret{}; + } + +private: + QMutex m_registerMutex; + QMap<QString, QJniObject> m_register; + + static void registerBackend(JNIEnv *, jclass, jclass interfaceClass, jobject interface); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(registerBackend) + static void unregisterBackend(JNIEnv *, jclass, jclass interfaceClass); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(unregisterBackend) +}; + +QT_END_NAMESPACE + +#endif // ANDROIDBACKENDREGISTER_H diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 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..805616e481 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::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..266d027b3c 100644 --- a/src/plugins/platforms/android/androidjniinput.cpp +++ b/src/plugins/platforms/android/androidjniinput.cpp @@ -1,3 +1,4 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -8,6 +9,7 @@ #include "androidjnimain.h" #include "qandroidplatformintegration.h" +#include <qpa/qplatformwindow.h> #include <qpa/qwindowsysteminterface.h> #include <QTouchEvent> #include <QPointer> @@ -17,66 +19,129 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); + using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtLayout, "org/qtproject/qt/android/QtLayout") +Q_DECLARE_JNI_CLASS(QtLayoutInterface, "org/qtproject/qt/android/QtLayoutInterface") +Q_DECLARE_JNI_CLASS(QtInputInterface, "org/qtproject/qt/android/QtInputInterface") + namespace QtAndroidInput { static bool m_ignoreMouseEvents = false; + static Qt::MouseButtons m_buttons = Qt::NoButton; + static QRect m_softwareKeyboardRect; static QList<QWindowSystemInterface::TouchPoint> m_touchPoints; static QPointer<QWindow> m_mouseGrabber; + GenericMotionEventListener::~GenericMotionEventListener() {} + namespace { + struct GenericMotionEventListeners { + QMutex mutex; + QList<QtAndroidInput::GenericMotionEventListener *> listeners; + }; + } + Q_GLOBAL_STATIC(GenericMotionEventListeners, g_genericMotionEventListeners) + + static jboolean dispatchGenericMotionEvent(JNIEnv *, jclass, jobject event) + { + jboolean ret = JNI_FALSE; + QMutexLocker locker(&g_genericMotionEventListeners()->mutex); + for (auto *listener : std::as_const(g_genericMotionEventListeners()->listeners)) + ret |= listener->handleGenericMotionEvent(event); + return ret; + } + + KeyEventListener::~KeyEventListener() {} + namespace { + struct KeyEventListeners { + QMutex mutex; + QList<QtAndroidInput::KeyEventListener *> listeners; + }; + } + Q_GLOBAL_STATIC(KeyEventListeners, g_keyEventListeners) + + static jboolean dispatchKeyEvent(JNIEnv *, jclass, jobject event) + { + jboolean ret = JNI_FALSE; + QMutexLocker locker(&g_keyEventListeners()->mutex); + for (auto *listener : std::as_const(g_keyEventListeners()->listeners)) + ret |= listener->handleKeyEvent(event); + return ret; + } + + void registerGenericMotionEventListener(QtAndroidInput::GenericMotionEventListener *listener) + { + QMutexLocker locker(&g_genericMotionEventListeners()->mutex); + g_genericMotionEventListeners()->listeners.push_back(listener); + } + + void unregisterGenericMotionEventListener(QtAndroidInput::GenericMotionEventListener *listener) + { + QMutexLocker locker(&g_genericMotionEventListeners()->mutex); + g_genericMotionEventListeners()->listeners.removeOne(listener); + } + + void registerKeyEventListener(QtAndroidInput::KeyEventListener *listener) + { + QMutexLocker locker(&g_keyEventListeners()->mutex); + g_keyEventListeners()->listeners.push_back(listener); + } + + void unregisterKeyEventListener(QtAndroidInput::KeyEventListener *listener) + { + QMutexLocker locker(&g_keyEventListeners()->mutex); + g_keyEventListeners()->listeners.removeOne(listener); + } + + QJniObject qtLayout() + { + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtLayoutInterface, QtJniTypes::QtLayout>( + "getQtLayout"); + } + void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; -#endif - QJniObject::callStaticMethod<void>(applicationClass(), - "updateSelection", - "(IIII)V", - selStart, - selEnd, - candidatesStart, - candidatesEnd); + qCDebug(lcQpaInputMethods) << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("updateSelection", selStart, selEnd, + candidatesStart, candidatesEnd); } void showSoftwareKeyboard(int left, int top, int width, int height, int inputHints, int enterKeyType) { - QJniObject::callStaticMethod<void>(applicationClass(), - "showSoftwareKeyboard", - "(IIIIII)V", - left, - top, - width, - height, - inputHints, - enterKeyType); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>( + "showSoftwareKeyboard", QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), left, top, width, height, inputHints, + enterKeyType); + qCDebug(lcQpaInputMethods) << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; } void resetSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ RESETSOFTWAREKEYBOARD"); -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("resetSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ RESETSOFTWAREKEYBOARD"; } void hideSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "hideSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ HIDESOFTWAREKEYBOARD"); -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("hideSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ HIDESOFTWAREKEYBOARD"; } bool isSoftwareKeyboardVisible() { - return QJniObject::callStaticMethod<jboolean>(applicationClass(), "isSoftwareKeyboardVisible"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtInputInterface, jboolean>( + "isSoftwareKeyboardVisible"); } QRect softwareKeyboardRect() @@ -86,81 +151,150 @@ namespace QtAndroidInput int getSelectHandleWidth() { - return QJniObject::callStaticMethod<jint>(applicationClass(), "getSelectHandleWidth"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtInputInterface, jint>("getSelectHandleWidth"); } void updateHandles(int mode, QPoint editMenuPos, uint32_t editButtons, QPoint cursor, QPoint anchor, bool rtl) { - QJniObject::callStaticMethod<void>(applicationClass(), "updateHandles", "(IIIIIIIIZ)V", - mode, editMenuPos.x(), editMenuPos.y(), editButtons, - cursor.x(), cursor.y(), - anchor.x(), anchor.y(), rtl); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>( + "updateHandles", QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), mode, editMenuPos.x(), editMenuPos.y(), + editButtons, cursor.x(), cursor.y(), anchor.x(), anchor.y(), rtl); + } + + // from https://developer.android.com/reference/android/view/MotionEvent#getButtonState() + enum AndroidMouseButton { + BUTTON_PRIMARY = 0x00000001, + BUTTON_SECONDARY = 0x00000002, + BUTTON_TERTIARY = 0x00000004, + BUTTON_BACK = 0x00000008, + BUTTON_FORWARD = 0x00000010, + BUTTON_STYLUS_PRIMARY = 0x00000020, + BUTTON_STYLUS_SECONDARY = 0x00000040, + }; + Q_DECLARE_FLAGS(AndroidMouseButtons, AndroidMouseButton) + + static Qt::MouseButtons toMouseButtons(jint j_buttons) + { + const auto buttons = static_cast<AndroidMouseButtons>(j_buttons); + Qt::MouseButtons mouseButtons; + if (buttons.testFlag(BUTTON_PRIMARY)) + mouseButtons.setFlag(Qt::LeftButton); + + if (buttons.testFlag(BUTTON_SECONDARY)) + mouseButtons.setFlag(Qt::RightButton); + + if (buttons.testFlag(BUTTON_TERTIARY)) + mouseButtons.setFlag(Qt::MiddleButton); + + if (buttons.testFlag(BUTTON_BACK)) + mouseButtons.setFlag(Qt::BackButton); + + if (buttons.testFlag(BUTTON_FORWARD)) + mouseButtons.setFlag(Qt::ForwardButton); + + if (buttons.testFlag(BUTTON_STYLUS_PRIMARY)) + mouseButtons.setFlag(Qt::LeftButton); + + if (buttons.testFlag(BUTTON_STYLUS_SECONDARY)) + mouseButtons.setFlag(Qt::RightButton); + + // Fall back to left button + if (Q_UNLIKELY(buttons != 0 && mouseButtons == Qt::NoButton)) { + qWarning() << "Unhandled button value:" << buttons << "Falling back to Qt::LeftButton"; + mouseButtons = Qt::LeftButton; + } + return mouseButtons; } - static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void sendMouseButtonEvents(QWindow *topLevel, QPoint localPos, QPoint globalPos, + jint mouseButtonState, QEvent::Type type) + { + const Qt::MouseButtons mouseButtons = toMouseButtons(mouseButtonState); + const Qt::MouseButtons changedButtons = mouseButtons & ~m_buttons; + + if (changedButtons == Qt::NoButton) + return; + + static_assert (sizeof(changedButtons) <= sizeof(uint), "Qt::MouseButtons size changed. Adapt code."); + + for (uint buttonInt = 0x1; static_cast<uint>(changedButtons) >= buttonInt; buttonInt <<= 1) { + const auto button = static_cast<Qt::MouseButton>(buttonInt); + if (changedButtons.testFlag(button)) { + QWindowSystemInterface::handleMouseEvent(topLevel, localPos, globalPos, + mouseButtons, button, type); + } + } + } + + static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jint mouseButtonState) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = topLevelWindowAt(globalPos); - m_mouseGrabber = tlw; - QPoint localPos = tlw ? (globalPos - tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, - Qt::MouseButtons(Qt::LeftButton), - Qt::LeftButton, QEvent::MouseButtonPress); - } - - static void mouseUp(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) - { - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos -tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, - Qt::MouseButtons(Qt::NoButton), - Qt::LeftButton, QEvent::MouseButtonRelease); + const QPoint globalPos(x,y); + QWindow *window = windowFromId(winId); + m_mouseGrabber = window; + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + sendMouseButtonEvents(window, localPos, globalPos, mouseButtonState, QEvent::MouseButtonPress); + } + + static void mouseUp(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jint mouseButtonState) + { + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + + sendMouseButtonEvents(window, localPos, globalPos, mouseButtonState, QEvent::MouseButtonRelease); m_ignoreMouseEvents = false; - m_mouseGrabber = 0; + m_mouseGrabber.clear(); } - static void mouseMove(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void mouseMove(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(m_mouseGrabber ? Qt::LeftButton : Qt::NoButton), Qt::NoButton, QEvent::MouseMove); } - static void mouseWheel(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y, jfloat hdelta, jfloat vdelta) + static void mouseWheel(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jfloat hdelta, jfloat vdelta) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; - QPoint angleDelta(hdelta * 120, vdelta * 120); + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + const QPoint angleDelta(hdelta * 120, vdelta * 120); - QWindowSystemInterface::handleWheelEvent(tlw, + QWindowSystemInterface::handleWheelEvent(window, localPos, globalPos, QPoint(), angleDelta); } - static void longPress(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void longPress(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y) { QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) @@ -171,16 +305,17 @@ namespace QtAndroidInput if (!rightMouseFromLongPress) return; m_ignoreMouseEvents = true; - QPoint globalPos(x,y); - QWindow *tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; + const QPoint globalPos(x,y); + QWindow *window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; // Click right button if no other button is already pressed. if (!m_mouseGrabber) { - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(Qt::RightButton), Qt::RightButton, QEvent::MouseButtonPress); - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(Qt::NoButton), Qt::RightButton, QEvent::MouseButtonRelease); } @@ -218,12 +353,11 @@ namespace QtAndroidInput touchPoint.rotation = qRadiansToDegrees(rotation); touchPoint.normalPosition = QPointF(double(x / dw), double(y / dh)); touchPoint.state = state; - touchPoint.area = QRectF(x - double(minor), - y - double(major), - double(minor * 2), - double(major * 2)); + touchPoint.area = QRectF(x - double(minor * 0.5f), + y - double(major * 0.5f), + double(minor), + double(major)); m_touchPoints.push_back(touchPoint); - if (state == QEventPoint::State::Pressed) { QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) @@ -254,31 +388,35 @@ namespace QtAndroidInput return touchDevice; } - static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/, jint /*action*/) + static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint winId, jint /*action*/) { if (m_touchPoints.isEmpty()) return; QMutexLocker lock(QtAndroid::platformInterfaceMutex()); - QPointingDevice *touchDevice = getTouchDevice(); + const QPointingDevice *touchDevice = getTouchDevice(); if (!touchDevice) return; - QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) + return; QWindowSystemInterface::handleTouchEvent(window, touchDevice, m_touchPoints); } - static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/) + static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint winId) { if (m_touchPoints.isEmpty()) return; QMutexLocker lock(QtAndroid::platformInterfaceMutex()); - QPointingDevice *touchDevice = getTouchDevice(); + const QPointingDevice *touchDevice = getTouchDevice(); if (!touchDevice) return; - QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) + return; QWindowSystemInterface::handleTouchCancelEvent(window, touchDevice); } @@ -291,14 +429,14 @@ namespace QtAndroidInput #endif // QT_CONFIG(tabletevent) } - static void tabletEvent(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint deviceId, jlong time, jint action, + static void tabletEvent(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint deviceId, jlong time, jint action, jint pointerType, jint buttonState, jfloat x, jfloat y, jfloat pressure) { #if QT_CONFIG(tabletevent) - QPointF globalPosF(x, y); - QPoint globalPos((int)x, (int)y); - QWindow *tlw = topLevelWindowAt(globalPos); - QPointF localPos = tlw ? (globalPosF - tlw->position()) : globalPosF; + const QPointF globalPosF(x, y); + QWindow *window = windowFromId(winId); + const QPointF localPos = window && window->handle() ? + window->handle()->mapFromGlobalF(globalPosF) : globalPosF; // Galaxy Note with plain Android: // 0 1 0 stylus press @@ -318,6 +456,7 @@ namespace QtAndroidInput Qt::MouseButtons buttons = Qt::NoButton; switch (action) { case 1: // ACTION_UP + case 6: // ACTION_POINTER_UP, happens if stylus is not the primary pointer case 212: // stylus release while side-button held on Galaxy Note 4 buttons = Qt::NoButton; break; @@ -329,11 +468,9 @@ namespace QtAndroidInput break; } -#ifdef QT_DEBUG_ANDROID_STYLUS - qDebug() << action << pointerType << buttonState << '@' << x << y << "pressure" << pressure << ": buttons" << buttons; -#endif + qCDebug(lcQpaInputMethods) << action << pointerType << buttonState << '@' << x << y << "pressure" << pressure << ": buttons" << buttons; - QWindowSystemInterface::handleTabletEvent(tlw, ulong(time), + QWindowSystemInterface::handleTabletEvent(window, ulong(time), localPos, globalPosF, int(QInputDevice::DeviceType::Stylus), pointerType, buttons, pressure, 0, 0, 0., 0., 0, deviceId, Qt::NoModifier); #endif // QT_CONFIG(tabletevent) @@ -797,9 +934,7 @@ namespace QtAndroidInput QMetaObject::invokeMethod(inputContext, "hideSelectionHandles", Qt::QueuedConnection); } } -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ KEYBOARDVISIBILITYCHANGED" << inputContext; -#endif + qCDebug(lcQpaInputMethods) << "@@@ KEYBOARDVISIBILITYCHANGED" << inputContext; } static void keyboardGeometryChanged(JNIEnv */*env*/, jobject /*thiz*/, jint x, jint y, jint w, jint h) @@ -812,16 +947,12 @@ namespace QtAndroidInput if (inputContext && qGuiApp) inputContext->emitKeyboardRectChanged(); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ KEYBOARDRECTCHANGED" << m_softwareKeyboardRect; -#endif + qCDebug(lcQpaInputMethods) << "@@@ KEYBOARDRECTCHANGED" << m_softwareKeyboardRect; } static void handleLocationChanged(JNIEnv */*env*/, jobject /*thiz*/, int id, int x, int y) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ handleLocationChanged" << id << x << y; -#endif + qCDebug(lcQpaInputMethods) << "@@@ handleLocationChanged" << id << x << y; QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) QMetaObject::invokeMethod(inputContext, "handleLocationChanged", Qt::BlockingQueuedConnection, @@ -829,13 +960,14 @@ namespace QtAndroidInput } - static JNINativeMethod methods[] = { + + static const JNINativeMethod methods[] = { {"touchBegin","(I)V",(void*)touchBegin}, {"touchAdd","(IIIZIIFFFF)V",(void*)touchAdd}, {"touchEnd","(II)V",(void*)touchEnd}, {"touchCancel", "(I)V", (void *)touchCancel}, - {"mouseDown", "(III)V", (void *)mouseDown}, - {"mouseUp", "(III)V", (void *)mouseUp}, + {"mouseDown", "(IIII)V", (void *)mouseDown}, + {"mouseUp", "(IIII)V", (void *)mouseUp}, {"mouseMove", "(III)V", (void *)mouseMove}, {"mouseWheel", "(IIIFF)V", (void *)mouseWheel}, {"longPress", "(III)V", (void *)longPress}, @@ -845,14 +977,15 @@ namespace QtAndroidInput {"keyUp", "(IIIZ)V", (void *)keyUp}, {"keyboardVisibilityChanged", "(Z)V", (void *)keyboardVisibilityChanged}, {"keyboardGeometryChanged", "(IIII)V", (void *)keyboardGeometryChanged}, - {"handleLocationChanged", "(III)V", (void *)handleLocationChanged} + {"handleLocationChanged", "(III)V", (void *)handleLocationChanged}, + {"dispatchGenericMotionEvent", "(Landroid/view/MotionEvent;)Z", reinterpret_cast<void *>(dispatchGenericMotionEvent)}, + {"dispatchKeyEvent", "(Landroid/view/KeyEvent;)Z", reinterpret_cast<void *>(dispatchKeyEvent)}, }; - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { - jclass appClass = QtAndroid::applicationClass(); - - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(QtJniTypes::Traits<QtJniTypes::QtInputDelegate>::className(), + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/androidjniinput.h b/src/plugins/platforms/android/androidjniinput.h index 7ef51ef9f7..28a2665bf6 100644 --- a/src/plugins/platforms/android/androidjniinput.h +++ b/src/plugins/platforms/android/androidjniinput.h @@ -6,10 +6,15 @@ #include <jni.h> #include <QtCore/qglobal.h> +#include <QtCore/QLoggingCategory> #include <QtCore/QRect> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaInputMethods); + +class QJniEnvironment; + namespace QtAndroidInput { // Software keyboard support @@ -26,7 +31,27 @@ namespace QtAndroidInput QPoint cursor = QPoint(), QPoint anchor = QPoint(), bool rtl = false); int getSelectHandleWidth(); - bool registerNatives(JNIEnv *env); + class GenericMotionEventListener + { + public: + virtual ~GenericMotionEventListener(); + virtual bool handleGenericMotionEvent(jobject event) = 0; + }; + + class KeyEventListener + { + public: + virtual ~KeyEventListener(); + virtual bool handleKeyEvent(jobject event) = 0; + }; + + void registerGenericMotionEventListener(GenericMotionEventListener *listener); + void unregisterGenericMotionEventListener(GenericMotionEventListener *listener); + + void registerKeyEventListener(KeyEventListener *listener); + void unregisterKeyEventListener(KeyEventListener *listener); + + bool registerNatives(QJniEnvironment &env); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp index 5e4f383e61..5bd2b924fc 100644 --- a/src/plugins/platforms/android/androidjnimain.cpp +++ b/src/plugins/platforms/android/androidjnimain.cpp @@ -10,14 +10,16 @@ #include "androidcontentfileengine.h" #include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" -#include "androidjniclipboard.h" #include "androidjniinput.h" #include "androidjnimain.h" #include "androidjnimenu.h" +#include "androidwindowembedding.h" #include "qandroidassetsfileenginehandler.h" #include "qandroideventdispatcher.h" #include "qandroidplatformdialoghelpers.h" #include "qandroidplatformintegration.h" +#include "qandroidplatformclipboard.h" +#include "qandroidplatformwindow.h" #include <android/api-level.h> #include <android/asset_manager_jni.h> @@ -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,9 @@ static jmethodID m_loadClassMethodID = nullptr; static AAssetManager *m_assetManager = nullptr; static jobject m_assets = nullptr; static jobject m_resourcesObj = nullptr; -static QtJniTypes::Activity m_activityObject = nullptr; -static jmethodID m_createSurfaceMethodID = nullptr; -static QtJniTypes::Service m_serviceObject = nullptr; -static jmethodID m_setSurfaceGeometryMethodID = nullptr; -static jmethodID m_destroySurfaceMethodID = nullptr; + +static jclass m_qtActivityClass = nullptr; +static jclass m_qtServiceClass = nullptr; static int m_pendingApplicationState = -1; static QBasicMutex m_platformMutex; @@ -67,10 +71,6 @@ static void *m_mainLibraryHnd = nullptr; static QList<QByteArray> m_applicationParams; static sem_t m_exitSemaphore, m_terminateSemaphore; -QHash<int, AndroidSurfaceClient *> m_surfaces; - -Q_CONSTINIT static QBasicMutex m_surfacesMutex; - static QAndroidPlatformIntegration *m_androidPlatformIntegration = nullptr; @@ -82,7 +82,7 @@ static double m_density = 1.0; static AndroidAssetsFileEngineHandler *m_androidAssetsFileEngineHandler = nullptr; static AndroidContentFileEngineHandler *m_androidContentFileEngineHandler = nullptr; - +static AndroidBackendRegister *m_backendRegister = nullptr; static const char m_qtTag[] = "Qt"; static const char m_classErrorMsg[] = "Can't find class \"%s\""; @@ -90,6 +90,9 @@ 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(QtWindowInterface, "org/qtproject/qt/android/QtWindowInterface") +Q_DECLARE_JNI_CLASS(QtAccessibilityInterface, "org/qtproject/qt/android/QtAccessibilityInterface"); + namespace QtAndroid { QBasicMutex *platformInterfaceMutex() @@ -100,6 +103,7 @@ namespace QtAndroid void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration) { m_androidPlatformIntegration = androidPlatformIntegration; + QtAndroid::notifyNativePluginIntegrationReady((bool)m_androidPlatformIntegration); // flush the pending state if necessary. if (m_androidPlatformIntegration && (m_pendingApplicationState != -1)) { @@ -125,6 +129,21 @@ namespace QtAndroid : 0; } + QWindow *windowFromId(int windowId) + { + if (!qGuiApp) + return nullptr; + + for (QWindow *w : qGuiApp->allWindows()) { + if (!w->handle()) + continue; + QAndroidPlatformWindow *window = static_cast<QAndroidPlatformWindow *>(w->handle()); + if (window->nativeViewId() == windowId) + return w; + } + return nullptr; + } + int availableWidthPixels() { return m_availableWidthPixels; @@ -160,53 +179,79 @@ namespace QtAndroid return m_applicationClass; } - QtJniTypes::Activity activity() + // TODO move calls from here to where they logically belong + void setSystemUiVisibility(SystemUiVisibility uiVisibility) { - return m_activityObject; + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("setSystemUiVisibility", + jint(uiVisibility)); } - QtJniTypes::Service service() + bool isQtApplication() { - return m_serviceObject; + // Returns true if the app is a Qt app, i.e. Qt controls the whole app and + // the Activity/Service is created by Qt. Returns false if instead Qt is + // embedded into a native Android app, where the Activity/Service is created + // by the user, outside of Qt, and Qt content is added as a view. + JNIEnv *env = QJniEnvironment::getJniEnv(); + auto activity = QtAndroidPrivate::activity(); + if (activity.isValid()) + return env->IsInstanceOf(activity.object(), m_qtActivityClass); + auto service = QtAndroidPrivate::service(); + if (service.isValid()) + return env->IsInstanceOf(QtAndroidPrivate::service().object(), m_qtServiceClass); + // return true as default as Qt application is our default use case. + // famous last words: we should not end up here + return true; } - void setSystemUiVisibility(SystemUiVisibility uiVisibility) + void initializeAccessibility() { - QJniObject::callStaticMethod<void>(m_applicationClass, "setSystemUiVisibility", "(I)V", jint(uiVisibility)); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "initializeAccessibility"); } void notifyAccessibilityLocationChange(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyAccessibilityLocationChange", - "(I)V", accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyLocationChange", accessibilityObjectId); } void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectHide", "(II)V", - accessibilityObjectId, parentObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectHide", accessibilityObjectId, parentObjectId); + } + + void notifyObjectShow(uint parentObjectId) + { + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectShow", parentObjectId); } void notifyObjectFocus(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectFocus","(I)V", accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectFocus", accessibilityObjectId); } void notifyValueChanged(uint accessibilityObjectId, jstring value) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyValueChanged", - "(ILjava/lang/String;)V", accessibilityObjectId, value); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyValueChanged", accessibilityObjectId, value); } void notifyScrolledEvent(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyScrolledEvent", "(I)V", - accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyScrolledEvent", accessibilityObjectId); } - void notifyQtAndroidPluginRunning(bool running) + void notifyNativePluginIntegrationReady(bool ready) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyQtAndroidPluginRunning","(Z)V", running); + QJniObject::callStaticMethod<void>(m_applicationClass, + "notifyNativePluginIntegrationReady", + ready); } jobject createBitmap(QImage img, JNIEnv *env) @@ -215,7 +260,7 @@ namespace QtAndroid return 0; if (img.format() != QImage::Format_RGBA8888 && img.format() != QImage::Format_RGB16) - img = img.convertToFormat(QImage::Format_RGBA8888); + img = std::move(img).convertToFormat(QImage::Format_RGBA8888); jobject bitmap = env->CallStaticObjectMethod(m_bitmapClass, m_createBitmapMethodID, @@ -303,62 +348,6 @@ namespace QtAndroid return manufacturer + u' ' + model; } - jint generateViewId() - { - return QJniObject::callStaticMethod<jint>("android/view/View", "generateViewId", "()I"); - } - - int createSurface(AndroidSurfaceClient *client, const QRect &geometry, bool onTop, int imageDepth) - { - QJniEnvironment env; - if (!env.jniEnv()) - return -1; - - m_surfacesMutex.lock(); - jint surfaceId = generateViewId(); - m_surfaces[surfaceId] = client; - m_surfacesMutex.unlock(); - - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) { - x = geometry.x(); - y = geometry.y(); - w = std::max(geometry.width(), 1); - h = std::max(geometry.height(), 1); - } - env->CallStaticVoidMethod(m_applicationClass, - m_createSurfaceMethodID, - surfaceId, - jboolean(onTop), - x, y, w, h, - imageDepth); - return surfaceId; - } - - int insertNativeView(jobject view, const QRect &geometry) - { - m_surfacesMutex.lock(); - jint surfaceId = generateViewId(); - m_surfaces[surfaceId] = nullptr; // dummy - m_surfacesMutex.unlock(); - - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) - geometry.getRect(&x, &y, &w, &h); - - QJniObject::callStaticMethod<void>(m_applicationClass, - "insertNativeView", - "(ILandroid/view/View;IIII)V", - surfaceId, - view, - x, - y, - qMax(w, 1), - qMax(h, 1)); - - return surfaceId; - } - void setViewVisibility(jobject view, bool visible) { QJniObject::callStaticMethod<void>(m_applicationClass, @@ -368,69 +357,6 @@ namespace QtAndroid visible); } - void setSurfaceGeometry(int surfaceId, const QRect &geometry) - { - if (surfaceId == -1) - return; - - QJniEnvironment env; - if (!env.jniEnv()) - return; - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) { - x = geometry.x(); - y = geometry.y(); - w = geometry.width(); - h = geometry.height(); - } - env->CallStaticVoidMethod(m_applicationClass, - m_setSurfaceGeometryMethodID, - surfaceId, - x, y, w, h); - } - - - void destroySurface(int surfaceId) - { - if (surfaceId == -1) - return; - - { - QMutexLocker lock(&m_surfacesMutex); - const auto &it = m_surfaces.find(surfaceId); - if (it != m_surfaces.end()) - m_surfaces.erase(it); - } - - QJniEnvironment env; - if (env.jniEnv()) - env->CallStaticVoidMethod(m_applicationClass, - m_destroySurfaceMethodID, - surfaceId); - } - - void bringChildToFront(int surfaceId) - { - if (surfaceId == -1) - return; - - QJniObject::callStaticMethod<void>(m_applicationClass, - "bringChildToFront", - "(I)V", - surfaceId); - } - - void bringChildToBack(int surfaceId) - { - if (surfaceId == -1) - return; - - QJniObject::callStaticMethod<void>(m_applicationClass, - "bringChildToBack", - "(I)V", - surfaceId); - } - bool blockEventLoopsWhenSuspended() { static bool block = qEnvironmentVariableIntValue("QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED"); @@ -442,18 +368,24 @@ namespace QtAndroid return m_assets; } + AndroidBackendRegister *backendRegister() + { + return m_backendRegister; + } + } // namespace QtAndroid static jboolean startQtAndroidPlugin(JNIEnv *env, jobject /*object*/, jstring paramsString) { + Q_UNUSED(env) + m_androidPlatformIntegration = nullptr; m_androidAssetsFileEngineHandler = new AndroidAssetsFileEngineHandler(); m_androidContentFileEngineHandler = new AndroidContentFileEngineHandler(); m_mainLibraryHnd = nullptr; + m_backendRegister = new AndroidBackendRegister(); - const char *nativeString = env->GetStringUTFChars(paramsString, 0); - 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 +428,7 @@ static void waitForServiceSetup(JNIEnv *env, jclass /*clazz*/) Q_UNUSED(env); // The service must wait until the QCoreApplication starts otherwise onBind will be // called too early - if (m_serviceObject) + if (QtAndroidPrivate::service().isValid() && QtAndroid::isQtApplication()) QtAndroidPrivate::waitForServiceSetup(); } @@ -527,7 +459,8 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) argv[argc] = nullptr; startQtAndroidPluginCalled.fetchAndAddRelease(1); - int ret = m_main(argc, argv.data()); + const int ret = m_main(argc, argv.data()); + qInfo() << "main() returned" << ret; if (m_mainLibraryHnd) { int res = dlclose(m_mainLibraryHnd); @@ -535,10 +468,8 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) qWarning() << "dlclose failed:" << dlerror(); } - if (m_applicationClass) { - qWarning("exit app 0"); + if (m_applicationClass) QJniObject::callStaticMethod<void>(m_applicationClass, "quitApp", "()V"); - } sem_post(&m_terminateSemaphore); sem_wait(&m_exitSemaphore); @@ -583,10 +514,6 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) env->DeleteGlobalRef(m_classLoaderObject); if (m_resourcesObj) env->DeleteGlobalRef(m_resourcesObj); - if (m_activityObject) - env->DeleteGlobalRef(m_activityObject); - if (m_serviceObject) - env->DeleteGlobalRef(m_serviceObject); if (m_bitmapClass) env->DeleteGlobalRef(m_bitmapClass); if (m_ARGB_8888_BitmapConfigValue) @@ -597,24 +524,18 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) env->DeleteGlobalRef(m_bitmapDrawableClass); if (m_assets) env->DeleteGlobalRef(m_assets); + if (m_qtActivityClass) + env->DeleteGlobalRef(m_qtActivityClass); + if (m_qtServiceClass) + env->DeleteGlobalRef(m_qtServiceClass); m_androidPlatformIntegration = nullptr; delete m_androidAssetsFileEngineHandler; m_androidAssetsFileEngineHandler = nullptr; + delete m_backendRegister; + m_backendRegister = nullptr; sem_post(&m_exitSemaphore); } -static void setSurface(JNIEnv *env, jobject /*thiz*/, jint id, jobject jSurface, jint w, jint h) -{ - QMutexLocker lock(&m_surfacesMutex); - const auto &it = m_surfaces.find(id); - if (it == m_surfaces.end()) - return; - - auto surfaceClient = it.value(); - if (surfaceClient) - surfaceClient->surfaceChanged(env, jSurface, w, h); -} - static void setDisplayMetrics(JNIEnv * /*env*/, jclass /*clazz*/, jint screenWidthPixels, jint screenHeightPixels, jint availableLeftPixels, jint availableTopPixels, jint availableWidthPixels, @@ -647,6 +568,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 +588,6 @@ static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/) QWindowSystemInterface::handleExposeEvent(w, QRegion(QRect(QPoint(), w->geometry().size()))); } } - - QAndroidPlatformScreen *screen = static_cast<QAndroidPlatformScreen *>(m_androidPlatformIntegration->screen()); - if (screen->rasterSurfaces()) - QMetaObject::invokeMethod(screen, "setDirty", Qt::QueuedConnection, Q_ARG(QRect,screen->geometry())); } static void updateApplicationState(JNIEnv */*env*/, jobject /*thiz*/, jint state) @@ -748,36 +666,42 @@ static void handleOrientationChanged(JNIEnv */*env*/, jobject /*thiz*/, jint new } } } +Q_DECLARE_JNI_NATIVE_METHOD(handleOrientationChanged) static void handleRefreshRateChanged(JNIEnv */*env*/, jclass /*cls*/, jfloat refreshRate) { if (m_androidPlatformIntegration) m_androidPlatformIntegration->setRefreshRate(refreshRate); } +Q_DECLARE_JNI_NATIVE_METHOD(handleRefreshRateChanged) static void handleScreenAdded(JNIEnv */*env*/, jclass /*cls*/, jint displayId) { if (m_androidPlatformIntegration) m_androidPlatformIntegration->handleScreenAdded(displayId); } +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenAdded) static void handleScreenChanged(JNIEnv */*env*/, jclass /*cls*/, jint displayId) { if (m_androidPlatformIntegration) m_androidPlatformIntegration->handleScreenChanged(displayId); } +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenChanged) static void handleScreenRemoved(JNIEnv */*env*/, jclass /*cls*/, jint displayId) { if (m_androidPlatformIntegration) m_androidPlatformIntegration->handleScreenRemoved(displayId); } +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenRemoved) static void handleUiDarkModeChanged(JNIEnv */*env*/, jobject /*thiz*/, jint newUiMode) { - QAndroidPlatformIntegration::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 +728,131 @@ static JNINativeMethod methods[] = { { "quitQtCoreApplication", "()V", (void *)quitQtCoreApplication }, { "terminateQt", "()V", (void *)terminateQt }, { "waitForServiceSetup", "()V", (void *)waitForServiceSetup }, - { "setDisplayMetrics", "(IIIIIIDDDDF)V", (void *)setDisplayMetrics }, - { "setSurface", "(ILjava/lang/Object;II)V", (void *)setSurface }, { "updateWindow", "()V", (void *)updateWindow }, { "updateApplicationState", "(I)V", (void *)updateApplicationState }, - { "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 +865,30 @@ Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void */*reserved*/) initialized = true; QT_USE_NAMESPACE - typedef union { - JNIEnv *nativeEnvironment; - void *venv; - } UnionJNIEnvToVoid; - - UnionJNIEnvToVoid uenv; - uenv.venv = nullptr; - m_javaVM = nullptr; - - if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) { - __android_log_print(ANDROID_LOG_FATAL, "Qt", "GetEnv failed"); + m_javaVM = vm; + QJniEnvironment env; + if (!env.isValid()) { + m_javaVM = nullptr; + __android_log_print(ANDROID_LOG_FATAL, "Qt", "Failed to initialize the JNI Environment"); return -1; } - JNIEnv *env = uenv.nativeEnvironment; if (!registerNatives(env) || !QtAndroidInput::registerNatives(env) || !QtAndroidMenu::registerNatives(env) || !QtAndroidAccessibility::registerNatives(env) - || !QtAndroidDialogHelpers::registerNatives(env)) { + || !QtAndroidDialogHelpers::registerNatives(env) + || !QAndroidPlatformClipboard::registerNatives(env) + || !QAndroidPlatformWindow::registerNatives(env) + || !QtAndroidWindowEmbedding::registerNatives(env) + || !AndroidBackendRegister::registerNatives()) { __android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed"); return -1; } QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); - m_javaVM = vm; // attach qt main thread data to this thread - QObject threadSetter; - if (threadSetter.thread()) - threadSetter.thread()->setObjectName("QtMainLoopThread"); + QThread::currentThread()->setObjectName("QtMainLoopThread"); __android_log_print(ANDROID_LOG_INFO, "Qt", "qt started"); return JNI_VERSION_1_6; } diff --git a/src/plugins/platforms/android/androidjnimain.h b/src/plugins/platforms/android/androidjnimain.h index 4879d7e5b2..9d616b18fb 100644 --- a/src/plugins/platforms/android/androidjnimain.h +++ b/src/plugins/platforms/android/androidjnimain.h @@ -12,6 +12,8 @@ #include <QImage> #include <private/qjnihelpers_p.h> +#include <QtCore/QJniObject> +#include <androidbackendregister.h> QT_BEGIN_NAMESPACE @@ -22,26 +24,23 @@ class QAndroidPlatformIntegration; class QWidget; class QString; class QWindow; -class AndroidSurfaceClient; +class QAndroidPlatformWindow; class QBasicMutex; +Q_DECLARE_JNI_CLASS(QtActivityDelegateBase, "org/qtproject/qt/android/QtActivityDelegateBase") +Q_DECLARE_JNI_CLASS(QtInputDelegate, "org/qtproject/qt/android/QtInputDelegate") + namespace QtAndroid { QBasicMutex *platformInterfaceMutex(); QAndroidPlatformIntegration *androidPlatformIntegration(); + AndroidBackendRegister *backendRegister(); void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration); void setQtThread(QThread *thread); - - - int createSurface(AndroidSurfaceClient * client, const QRect &geometry, bool onTop, int imageDepth); - int insertNativeView(jobject view, const QRect &geometry); void setViewVisibility(jobject view, bool visible); - void setSurfaceGeometry(int surfaceId, const QRect &geometry); - void destroySurface(int surfaceId); - void bringChildToFront(int surfaceId); - void bringChildToBack(int surfaceId); QWindow *topLevelWindowAt(const QPoint &globalPos); + QWindow *windowFromId(int windowId); int availableWidthPixels(); int availableHeightPixels(); double scaledDensity(); @@ -50,8 +49,6 @@ namespace QtAndroid jobject assets(); AAssetManager *assetManager(); jclass applicationClass(); - QtJniTypes::Activity activity(); - QtJniTypes::Service service(); // Keep synchronized with flags in ActivityDelegate.java enum SystemUiVisibility { @@ -65,12 +62,14 @@ namespace QtAndroid jobject createBitmap(int width, int height, QImage::Format format, JNIEnv *env); jobject createBitmapDrawable(jobject bitmap, JNIEnv *env = nullptr); + void initializeAccessibility(); void notifyAccessibilityLocationChange(uint accessibilityObjectId); void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId); + void notifyObjectShow(uint parentObjectId); void notifyObjectFocus(uint accessibilityObjectId); void notifyValueChanged(uint accessibilityObjectId, jstring value); void 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..5f8a0b92e9 100644 --- a/src/plugins/platforms/android/androidjnimenu.cpp +++ b/src/plugins/platforms/android/androidjnimenu.cpp @@ -20,6 +20,8 @@ QT_BEGIN_NAMESPACE using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtMenuInterface, "org/qtproject/qt/android/QtMenuInterface"); + namespace QtAndroidMenu { static QList<QAndroidPlatformMenu *> pendingContextMenus; @@ -31,8 +33,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 +46,35 @@ namespace QtAndroidMenu void resetMenuBar() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetOptionsMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("resetOptionsMenu"); } void openOptionsMenu() { - QJniObject::callStaticMethod<void>(applicationClass(), "openOptionsMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("openOptionsMenu"); } - void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env) + void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu) pendingContextMenus.append(visibleMenu); visibleMenu = menu; menu->aboutToShow(); - env->CallStaticVoidMethod(applicationClass(), openContextMenuMethodID, anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height()); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("openContextMenu", anchorRect.x(), + anchorRect.y(), anchorRect.width(), + anchorRect.height()); } void hideContextMenu(QAndroidPlatformMenu *menu) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu == menu) { - QJniObject::callStaticMethod<void>(applicationClass(), "closeContextMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("closeContextMenu"); pendingContextMenus.clear(); } else { pendingContextMenus.removeOne(menu); @@ -211,8 +217,10 @@ namespace QtAndroidMenu return order; } - static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) + env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) @@ -249,8 +257,11 @@ namespace QtAndroidMenu return order ? JNI_TRUE : JNI_FALSE; } - static jboolean onOptionsItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) + static jboolean onOptionsItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { + Q_UNUSED(env) + Q_UNUSED(thiz) + QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) return JNI_FALSE; @@ -260,7 +271,7 @@ namespace QtAndroidMenu QAndroidPlatformMenuItem *item = static_cast<QAndroidPlatformMenuItem *>(menus.front()->menuItemForId(menuId)); if (item) { if (item->menu()) { - showContextMenu(item->menu(), QRect(), env); + showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); @@ -270,18 +281,23 @@ namespace QtAndroidMenu } else { QAndroidPlatformMenu *menu = static_cast<QAndroidPlatformMenu *>(visibleMenuBar->menuForId(menuId)); if (menu) - showContextMenu(menu, QRect(), env); + showContextMenu(menu, QRect()); } return JNI_TRUE; } - static void onOptionsMenuClosed(JNIEnv */*env*/, jobject /*thiz*/, jobject /*menu*/) + static void onOptionsMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(env) + Q_UNUSED(thiz) + Q_UNUSED(menu) } - static void onCreateContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static void onCreateContextMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) + env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) @@ -295,8 +311,9 @@ namespace QtAndroidMenu addAllMenuItemsToMenu(env, menu, visibleMenu); } - static void fillContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static void fillContextMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) @@ -305,13 +322,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 +348,12 @@ namespace QtAndroidMenu return JNI_TRUE; } - static void onContextMenuClosed(JNIEnv *env, jobject /*thiz*/, jobject /*menu*/) + static void onContextMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(env) + Q_UNUSED(thiz) + Q_UNUSED(menu) + QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) return; @@ -337,7 +361,7 @@ namespace QtAndroidMenu visibleMenu->aboutToHide(); visibleMenu = 0; if (!pendingContextMenus.empty()) - showContextMenu(pendingContextMenus.takeLast(), QRect(), env); + showContextMenu(pendingContextMenus.takeLast(), QRect()); } static JNINativeMethod methods[] = { @@ -378,17 +402,15 @@ namespace QtAndroidMenu return false; \ } - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { jclass appClass = applicationClass(); - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(appClass, methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } - GET_AND_CHECK_STATIC_METHOD(openContextMenuMethodID, appClass, "openContextMenu", "(IIII)V"); - jclass clazz; FIND_AND_CHECK_CLASS("android/view/Menu"); GET_AND_CHECK_METHOD(clearMenuMethodID, clazz, "clear", "()V"); diff --git a/src/plugins/platforms/android/androidjnimenu.h b/src/plugins/platforms/android/androidjnimenu.h index 1645ce750b..e10ad930d9 100644 --- a/src/plugins/platforms/android/androidjnimenu.h +++ b/src/plugins/platforms/android/androidjnimenu.h @@ -15,12 +15,13 @@ class QAndroidPlatformMenuItem; class QWindow; class QRect; class QPoint; +class QJniEnvironment; namespace QtAndroidMenu { // Menu support void openOptionsMenu(); - void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env); + void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect); void hideContextMenu(QAndroidPlatformMenu *menu); void syncMenu(QAndroidPlatformMenu *menu); void androidPlatformMenuDestroyed(QAndroidPlatformMenu *menu); @@ -31,7 +32,7 @@ namespace QtAndroidMenu void removeMenuBar(QAndroidPlatformMenuBar *menuBar); // Menu support - bool registerNatives(JNIEnv *env); + bool registerNatives(QJniEnvironment &env); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidsurfaceclient.h b/src/plugins/platforms/android/androidsurfaceclient.h deleted file mode 100644 index dded9a1f66..0000000000 --- a/src/plugins/platforms/android/androidsurfaceclient.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef ANDROIDSURFACECLIENT_H -#define ANDROIDSURFACECLIENT_H -#include <QMutex> -#include <jni.h> - -QT_BEGIN_NAMESPACE - -class AndroidSurfaceClient -{ -public: - virtual void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) = 0; - void lockSurface() { m_surfaceMutex.lock(); } - void unlockSurface() { m_surfaceMutex.unlock(); } - -protected: - QMutex m_surfaceMutex; -}; - -QT_END_NAMESPACE - -#endif // ANDROIDSURFACECLIENT_H diff --git a/src/plugins/platforms/android/androidwindowembedding.cpp b/src/plugins/platforms/android/androidwindowembedding.cpp new file mode 100644 index 0000000000..65dabcac66 --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "androidwindowembedding.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtGui/qwindow.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(QtView, "org/qtproject/qt/android/QtView"); + +namespace QtAndroidWindowEmbedding { + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView, + jint x, jint y, jint width, jint height) + { + // QWindow should be constructed on the Qt thread rather than directly in the caller thread + // To avoid hitting checkReceiverThread assert in QCoreApplication::doNotify + QMetaObject::invokeMethod(qApp, [rootView, x, y, width, height] { + QWindow *parentWindow = QWindow::fromWinId(reinterpret_cast<WId>(rootView.object())); + parentWindow->setGeometry(x, y, width, height); + rootView.callMethod<void>("createWindow", reinterpret_cast<jlong>(parentWindow)); + }); + } + + void deleteWindow(JNIEnv *, jclass, jlong windowRef) + { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + window->deleteLater(); + } + + void setWindowVisible(JNIEnv *, jclass, jlong windowRef, jboolean visible) + { + QMetaObject::invokeMethod(qApp, [windowRef, visible] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + if (visible) { + window->showNormal(); + if (!window->parent()->isVisible()) + window->parent()->showNormal(); + } else { + window->hide(); + } + }); + } + + void resizeWindow(JNIEnv *, jclass, jlong windowRef, jint x, jint y, jint width, jint height) + { + QMetaObject::invokeMethod(qApp, [windowRef, x, y, width, height] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + QWindow *parent = window->parent(); + if (parent) + parent->setGeometry(x, y, width, height); + window->setGeometry(0, 0, width, height); + }); + } + + bool registerNatives(QJniEnvironment& env) { + return env.registerNativeMethods( + QtJniTypes::Traits<QtJniTypes::QtView>::className(), + { Q_JNI_NATIVE_SCOPED_METHOD(createRootWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(deleteWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(setWindowVisible, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(resizeWindow, QtAndroidWindowEmbedding) }); + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidwindowembedding.h b/src/plugins/platforms/android/androidwindowembedding.h new file mode 100644 index 0000000000..b7b0e1205f --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.h @@ -0,0 +1,41 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTANDROIDWINDOWEMBEDDING_H +#define QTANDROIDWINDOWEMBEDDING_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(View, "android/view/View"); + +namespace QtAndroidWindowEmbedding +{ + bool registerNatives(QJniEnvironment& env); + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView, + jint x, jint y,jint width, jint height); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(createRootWindow) + void deleteWindow(JNIEnv *, jclass, jlong window); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(deleteWindow) + void setWindowVisible(JNIEnv *, jclass, jlong window, jboolean visible); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setWindowVisible) + void resizeWindow(JNIEnv *, jclass, jlong windowRef, jint x, jint y, jint width, jint height); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(resizeWindow) +}; + +QT_END_NAMESPACE + +#endif // QTANDROIDWINDOWEMBEDDING_H diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 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..4d752a3cc3 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.cpp +++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp @@ -8,7 +8,6 @@ #include <qpa/qwindowsysteminterface.h> #include "qandroidplatformscreen.h" -#include "qandroidplatformbackingstore.h" #include "qandroidplatformintegration.h" #include "qandroidplatformwindow.h" #include "androidjnimain.h" @@ -53,8 +52,14 @@ 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_CLASS(QtWindowInterface, "org/qtproject/qt/android/QtWindowInterface") -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 +84,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 +120,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 +131,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 +154,6 @@ QWindow *QAndroidPlatformScreen::topLevelAt(const QPoint &p) const return 0; } -bool QAndroidPlatformScreen::event(QEvent *event) -{ - if (event->type() == QEvent::UpdateRequest) { - doRedraw(); - m_updatePending = false; - return true; - } - return QObject::event(event); -} - void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) { if (window->parent() && window->isRaster()) @@ -164,83 +163,56 @@ void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) return; m_windowStack.prepend(window); - if (window->isRaster()) { - m_rasterSurfaces.ref(); - setDirty(window->geometry()); - } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("addTopLevelWindow", + window->nativeWindow()); + + if (window->window()->isVisible()) + topVisibleWindowChanged(); } void QAndroidPlatformScreen::removeWindow(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - m_windowStack.removeOne(window); if (m_windowStack.contains(window)) qWarning() << "Failed to remove window"; - if (window->isRaster()) { - m_rasterSurfaces.deref(); - setDirty(window->geometry()); - } + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("removeTopLevelWindow", + window->nativeViewId()); - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::raise(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - int index = m_windowStack.indexOf(window); - if (index <= 0) + if (index < 0) return; - m_windowStack.move(index, 0); - if (window->isRaster()) { - setDirty(window->geometry()); + if (index > 0) { + m_windowStack.move(index, 0); + + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("bringChildToFront", + window->nativeViewId()); } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::lower(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - int index = m_windowStack.indexOf(window); if (index == -1 || index == (m_windowStack.size() - 1)) return; m_windowStack.move(index, m_windowStack.size() - 1); - if (window->isRaster()) { - setDirty(window->geometry()); - } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); -} -void QAndroidPlatformScreen::scheduleUpdate() -{ - if (!m_updatePending) { - m_updatePending = true; - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); - } -} + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("bringChildToBack", + window->nativeViewId()); -void QAndroidPlatformScreen::setDirty(const QRect &rect) -{ - QRect intersection = rect.intersected(m_availableGeometry); - m_dirtyRect |= intersection; - scheduleUpdate(); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::setPhysicalSize(const QSize &size) @@ -292,7 +264,6 @@ void QAndroidPlatformScreen::setOrientation(Qt::ScreenOrientation orientation) void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) { - QMutexLocker lock(&m_surfaceMutex); if (m_availableGeometry == rect) return; @@ -313,165 +284,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 +326,4 @@ Qt::ScreenOrientation QAndroidPlatformScreen::nativeOrientation() const { return QAndroidPlatformIntegration::m_nativeOrientation; } - -void QAndroidPlatformScreen::surfaceChanged(JNIEnv *env, jobject surface, int w, int h) -{ - lockSurface(); - if (surface && w > 0 && h > 0) { - releaseSurface(); - m_nativeSurface = ANativeWindow_fromSurface(env, surface); - QMetaObject::invokeMethod(this, "setDirty", Qt::QueuedConnection, Q_ARG(QRect, QRect(0, 0, w, h))); - } else { - releaseSurface(); - } - unlockSurface(); - m_surfaceWaitCondition.wakeOne(); -} - -void QAndroidPlatformScreen::releaseSurface() -{ - if (m_nativeSurface) { - ANativeWindow_release(m_nativeSurface); - m_nativeSurface = 0; - } -} - -/*! - This function is called when Qt needs to be able to grab the content of a window. - - Returns the content of the window specified with the WId handle within the boundaries of - QRect(x, y, width, height). -*/ -QPixmap QAndroidPlatformScreen::grabWindow(WId window, int x, int y, int width, int height) const -{ - QRectF screenshotRect(x, y, width, height); - QWindow* wnd = 0; - if (window) - { - const auto windowList = qApp->allWindows(); - for (QWindow *w : windowList) - if (w->winId() == window) { - wnd = w; - break; - } - } - if (wnd) { - const qreal factor = logicalDpi().first / androidLogicalDpi; //HighDPI factor; - QRectF wndRect = wnd->geometry(); - if (wnd->parent()) - wndRect.moveTopLeft(wnd->parent()->mapToGlobal(wndRect.topLeft().toPoint())); - if (!qFuzzyCompare(factor, 1)) - wndRect = QRectF(wndRect.left() * factor, wndRect.top() * factor, - wndRect.width() * factor, wndRect.height() * factor); - - if (!screenshotRect.isEmpty()) { - screenshotRect.moveTopLeft(wndRect.topLeft() + screenshotRect.topLeft()); - screenshotRect = screenshotRect.intersected(wndRect); - } else { - screenshotRect = wndRect; - } - } else { - screenshotRect = screenshotRect.isValid() ? screenshotRect : geometry(); - } - return const_cast<QAndroidPlatformScreen *>(this)->doScreenShot(screenshotRect.toRect()); -} - QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformscreen.h b/src/plugins/platforms/android/qandroidplatformscreen.h index 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..99eeabac1d 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.cpp +++ b/src/plugins/platforms/android/qandroidplatformtheme.cpp @@ -4,6 +4,7 @@ #include "androidjnimain.h" #include "androidjnimenu.h" #include "qandroidplatformtheme.h" +#include "qandroidplatformiconengine.h" #include "qandroidplatformmenubar.h" #include "qandroidplatformmenu.h" #include "qandroidplatformmenuitem.h" @@ -22,6 +23,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaMenus, "qt.qpa.menus") + using namespace Qt::StringLiterals; namespace { @@ -158,18 +161,14 @@ QJsonObject AndroidStyle::loadStyleData() if (!stylePath.isEmpty() && !stylePath.endsWith(slashChar)) stylePath += slashChar; - if (QAndroidPlatformIntegration::colorScheme() == Qt::ColorScheme::Dark) + const Qt::ColorScheme colorScheme = QAndroidPlatformTheme::instance() + ? QAndroidPlatformTheme::instance()->colorScheme() + : QAndroidPlatformIntegration::colorScheme(); + if (colorScheme == Qt::ColorScheme::Dark) stylePath += "darkUiMode/"_L1; 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,29 +399,46 @@ 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(); } Qt::ColorScheme QAndroidPlatformTheme::colorScheme() const { + if (m_colorSchemeOverride != Qt::ColorScheme::Unknown) + return m_colorSchemeOverride; return QAndroidPlatformIntegration::colorScheme(); } +void QAndroidPlatformTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + m_colorSchemeOverride = scheme; + QMetaObject::invokeMethod(qGuiApp, [this]{ + updateColorScheme(); + }); +} + static inline int paletteType(QPlatformTheme::Palette type) { switch (type) { @@ -488,6 +504,11 @@ const QFont *QAndroidPlatformTheme::font(Font type) const return 0; } +QIconEngine *QAndroidPlatformTheme::createIconEngine(const QString &iconName) const +{ + return new QAndroidPlatformIconEngine(iconName); +} + QVariant QAndroidPlatformTheme::themeHint(ThemeHint hint) const { switch (hint) { diff --git a/src/plugins/platforms/android/qandroidplatformtheme.h b/src/plugins/platforms/android/qandroidplatformtheme.h index bb8c5c4869..1b4ab5664d 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.h +++ b/src/plugins/platforms/android/qandroidplatformtheme.h @@ -9,13 +9,15 @@ #include <QtGui/qpalette.h> #include <QtCore/qhash.h> #include <QtCore/qbytearray.h> - +#include <QtCore/qloggingcategory.h> #include <QJsonObject> #include <memory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaMenus) + struct AndroidStyle { static QJsonObject loadStyleData(); @@ -38,8 +40,11 @@ public: QPlatformMenuItem *createPlatformMenuItem() const override; void showPlatformMenuBar() override; Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; + const QPalette *palette(Palette type = SystemPalette) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; QString standardButtonText(int button) const override; bool usePlatformNativeDialog(DialogType type) const override; @@ -54,6 +59,7 @@ private: std::shared_ptr<AndroidStyle> m_androidStyleData; QPalette m_defaultPalette; QFont m_systemFont; + Qt::ColorScheme m_colorSchemeOverride = Qt::ColorScheme::Unknown; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp b/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp index cb6bb6d390..4bf4f44fa1 100644 --- a/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp @@ -18,7 +18,6 @@ QT_BEGIN_NAMESPACE QAndroidPlatformVulkanWindow::QAndroidPlatformVulkanWindow(QWindow *window) : QAndroidPlatformWindow(window), - m_nativeSurfaceId(-1), m_nativeWindow(nullptr), m_vkSurface(0), m_createVkSurface(nullptr), @@ -29,11 +28,7 @@ QAndroidPlatformVulkanWindow::QAndroidPlatformVulkanWindow(QWindow *window) QAndroidPlatformVulkanWindow::~QAndroidPlatformVulkanWindow() { m_surfaceWaitCondition.wakeOne(); - lockSurface(); - if (m_nativeSurfaceId != -1) - QtAndroid::destroySurface(m_nativeSurfaceId); - clearSurface(); - unlockSurface(); + destroyAndClearSurface(); } void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) @@ -44,8 +39,8 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) m_oldGeometry = geometry(); QAndroidPlatformWindow::setGeometry(rect); - if (m_nativeSurfaceId != -1) - QtAndroid::setSurfaceGeometry(m_nativeSurfaceId, rect); + if (m_surfaceCreated) + setNativeGeometry(rect); QRect availableGeometry = screen()->availableGeometry(); if (rect.width() > 0 @@ -54,22 +49,13 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) && availableGeometry.height() > 0) { QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), rect.size())); } - - if (rect.topLeft() != m_oldGeometry.topLeft()) - repaint(QRegion(rect)); } void QAndroidPlatformVulkanWindow::applicationStateChanged(Qt::ApplicationState state) { QAndroidPlatformWindow::applicationStateChanged(state); if (state <= Qt::ApplicationHidden) { - lockSurface(); - if (m_nativeSurfaceId != -1) { - QtAndroid::destroySurface(m_nativeSurfaceId); - m_nativeSurfaceId = -1; - } - clearSurface(); - unlockSurface(); + destroyAndClearSurface(); } } @@ -91,27 +77,12 @@ void QAndroidPlatformVulkanWindow::clearSurface() } } -void QAndroidPlatformVulkanWindow::sendExpose() -{ - QRect availableGeometry = screen()->availableGeometry(); - if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) - QWindowSystemInterface::handleExposeEvent(window(), QRegion(QRect(QPoint(), geometry().size()))); -} - -void QAndroidPlatformVulkanWindow::surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) +void QAndroidPlatformVulkanWindow::destroyAndClearSurface() { - Q_UNUSED(jniEnv); - Q_UNUSED(w); - Q_UNUSED(h); - lockSurface(); - m_androidSurfaceObject = surface; - if (surface) - m_surfaceWaitCondition.wakeOne(); + destroySurface(); + clearSurface(); unlockSurface(); - - if (surface) - sendExpose(); } VkSurfaceKHR *QAndroidPlatformVulkanWindow::vkSurface() @@ -124,16 +95,15 @@ VkSurfaceKHR *QAndroidPlatformVulkanWindow::vkSurface() clearSurface(); QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1) { + if (!m_surfaceCreated) { AndroidDeadlockProtector protector; if (!protector.acquire()) return &m_vkSurface; - const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); - m_nativeSurfaceId = QtAndroid::createSurface(this, geometry(), windowStaysOnTop, 32); + createSurface(); m_surfaceWaitCondition.wait(&m_surfaceMutex); } - if (m_nativeSurfaceId == -1 || !m_androidSurfaceObject.isValid()) + if (!m_surfaceCreated || !m_androidSurfaceObject.isValid()) return &m_vkSurface; QJniEnvironment env; diff --git a/src/plugins/platforms/android/qandroidplatformvulkanwindow.h b/src/plugins/platforms/android/qandroidplatformvulkanwindow.h index 14d5b7fc0e..fa959239d1 100644 --- a/src/plugins/platforms/android/qandroidplatformvulkanwindow.h +++ b/src/plugins/platforms/android/qandroidplatformvulkanwindow.h @@ -10,7 +10,6 @@ #define VK_USE_PLATFORM_ANDROID_KHR -#include "androidsurfaceclient.h" #include "qandroidplatformvulkaninstance.h" #include "qandroidplatformwindow.h" @@ -20,7 +19,7 @@ QT_BEGIN_NAMESPACE -class QAndroidPlatformVulkanWindow : public QAndroidPlatformWindow, public AndroidSurfaceClient +class QAndroidPlatformVulkanWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformVulkanWindow(QWindow *window); @@ -32,17 +31,11 @@ public: VkSurfaceKHR *vkSurface(); -protected: - void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) override; - private: - void sendExpose(); void clearSurface(); + void destroyAndClearSurface(); - int m_nativeSurfaceId; ANativeWindow *m_nativeWindow; - QJniObject m_androidSurfaceObject; - QWaitCondition m_surfaceWaitCondition; QSurfaceFormat m_format; QRect m_oldGeometry; VkSurfaceKHR m_vkSurface; diff --git a/src/plugins/platforms/android/qandroidplatformwindow.cpp b/src/plugins/platforms/android/qandroidplatformwindow.cpp index b1eba17d04..2482160573 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformwindow.cpp @@ -14,40 +14,102 @@ QT_BEGIN_NAMESPACE -Q_CONSTINIT static QBasicAtomicInt winIdGenerator = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window") + +Q_DECLARE_JNI_CLASS(QtInputInterface, "org/qtproject/qt/android/QtInputInterface") +Q_DECLARE_JNI_CLASS(QtInputConnectionListener, + "org/qtproject/qt/android/QtInputConnection$QtInputConnectionListener") QAndroidPlatformWindow::QAndroidPlatformWindow(QWindow *window) - : QPlatformWindow(window) + : QPlatformWindow(window), m_nativeQtWindow(nullptr), + m_surfaceContainerType(SurfaceContainer::TextureView), m_nativeParentQtWindow(nullptr), + m_androidSurfaceObject(nullptr) { m_windowFlags = Qt::Widget; m_windowState = Qt::WindowNoState; - m_windowId = winIdGenerator.fetchAndAddRelaxed(1) + 1; + // the surfaceType is overwritten in QAndroidPlatformOpenGLWindow ctor so let's save + // the fact that it's a raster window for now + m_isRaster = window->surfaceType() == QSurface::RasterSurface; setWindowState(window->windowStates()); // the following is in relation to the virtual geometry const bool forceMaximize = m_windowState & (Qt::WindowMaximized | Qt::WindowFullScreen); - const QRect requestedNativeGeometry = - forceMaximize ? QRect() : QHighDpi::toNativePixels(window->geometry(), window); - const QRect availableDeviceIndependentGeometry = (window->parent()) - ? window->parent()->geometry() - : QHighDpi::fromNativePixels(platformScreen()->availableGeometry(), window); + const QRect nativeScreenGeometry = platformScreen()->availableGeometry(); + if (forceMaximize) { + setGeometry(nativeScreenGeometry); + } else { + const QRect requestedNativeGeometry = QHighDpi::toNativePixels(window->geometry(), window); + const QRect availableDeviceIndependentGeometry = (window->parent()) + ? window->parent()->geometry() + : QHighDpi::fromNativePixels(nativeScreenGeometry, window); + // initialGeometry returns in native pixels + const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( + window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), + availableDeviceIndependentGeometry.height()); + if (requestedNativeGeometry != finalNativeGeometry) + setGeometry(finalNativeGeometry); + } + + if (isEmbeddingContainer()) + return; - // initialGeometry returns in native pixels - const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( - window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), - availableDeviceIndependentGeometry.height()); + if (parent()) { + QAndroidPlatformWindow *androidParent = static_cast<QAndroidPlatformWindow*>(parent()); + if (!androidParent->isEmbeddingContainer()) + m_nativeParentQtWindow = androidParent->nativeWindow(); + } + + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + QtJniTypes::QtInputConnectionListener listener = + reg->callInterface<QtJniTypes::QtInputInterface, QtJniTypes::QtInputConnectionListener>( + "getInputConnectionListener"); + + m_nativeQtWindow = QJniObject::construct<QtJniTypes::QtWindow>( + QNativeInterface::QAndroidApplication::context(), m_nativeParentQtWindow, listener); + m_nativeViewId = m_nativeQtWindow.callMethod<jint>("getId"); + + if (window->isTopLevel()) + platformScreen()->addWindow(this); - if (requestedNativeGeometry != finalNativeGeometry) - setGeometry(finalNativeGeometry); + static bool ok = false; + static const int value = qEnvironmentVariableIntValue("QT_ANDROID_SURFACE_CONTAINER_TYPE", &ok); + if (ok) { + static const SurfaceContainer type = static_cast<SurfaceContainer>(value); + if (type == SurfaceContainer::SurfaceView || type == SurfaceContainer::TextureView) + m_surfaceContainerType = type; + } else if (platformScreen()->windows().size() <= 1) { + // TODO should handle case where this changes at runtime -> need to change existing window + // into TextureView (or perhaps not, if the parent window would be SurfaceView, as long as + // onTop was false it would stay below the children) + m_surfaceContainerType = SurfaceContainer::SurfaceView; + } + qCDebug(lcQpaWindow) << "Window" << m_nativeViewId << "using surface container type" + << static_cast<int>(m_surfaceContainerType); +} + +QAndroidPlatformWindow::~QAndroidPlatformWindow() +{ + if (window()->isTopLevel()) + platformScreen()->removeWindow(this); } + void QAndroidPlatformWindow::lower() { + if (m_nativeParentQtWindow.isValid()) { + m_nativeParentQtWindow.callMethod<void>("bringChildToBack", nativeViewId()); + return; + } platformScreen()->lower(this); } void QAndroidPlatformWindow::raise() { + if (m_nativeParentQtWindow.isValid()) { + m_nativeParentQtWindow.callMethod<void>("bringChildToFront", nativeViewId()); + QWindowSystemInterface::handleFocusWindowChanged(window(), Qt::ActiveWindowFocusReason); + return; + } updateSystemUiVisibility(); platformScreen()->raise(this); } @@ -71,23 +133,25 @@ void QAndroidPlatformWindow::setGeometry(const QRect &rect) void QAndroidPlatformWindow::setVisible(bool visible) { - if (visible) - updateSystemUiVisibility(); + if (isEmbeddingContainer()) + return; + m_nativeQtWindow.callMethod<void>("setVisible", visible); if (visible) { - if ((m_windowState & Qt::WindowFullScreen) - || ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) { - setGeometry(platformScreen()->geometry()); - } else if (m_windowState & Qt::WindowMaximized) { - setGeometry(platformScreen()->availableGeometry()); + if (window()->isTopLevel()) { + updateSystemUiVisibility(); + if ((m_windowState & Qt::WindowFullScreen) + || ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) { + setGeometry(platformScreen()->geometry()); + } else if (m_windowState & Qt::WindowMaximized) { + setGeometry(platformScreen()->availableGeometry()); + } + requestActivateWindow(); } + } else if (window()->isTopLevel() && window() == qGuiApp->focusWindow()) { + platformScreen()->topVisibleWindowChanged(); } - if (visible) - platformScreen()->addWindow(this); - else - platformScreen()->removeWindow(this); - QRect availableGeometry = screen()->availableGeometry(); if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) QPlatformWindow::setVisible(visible); @@ -120,7 +184,30 @@ Qt::WindowFlags QAndroidPlatformWindow::windowFlags() const void QAndroidPlatformWindow::setParent(const QPlatformWindow *window) { - Q_UNUSED(window); + using namespace QtJniTypes; + + if (window) { + auto androidWindow = static_cast<const QAndroidPlatformWindow*>(window); + if (androidWindow->isEmbeddingContainer()) + return; + // If we were a top level window, remove from screen + if (!m_nativeParentQtWindow.isValid()) + platformScreen()->removeWindow(this); + + const QtWindow parentWindow = androidWindow->nativeWindow(); + // If this was a child window of another window, the java method takes care of that + m_nativeQtWindow.callMethod<void, QtWindow>("setParent", parentWindow.object()); + m_nativeParentQtWindow = parentWindow; + } else if (QtAndroid::isQtApplication()) { + m_nativeQtWindow.callMethod<void, QtWindow>("setParent", nullptr); + m_nativeParentQtWindow = QJniObject(); + platformScreen()->addWindow(this); + } +} + +WId QAndroidPlatformWindow::winId() const +{ + return m_nativeQtWindow.isValid() ? reinterpret_cast<WId>(m_nativeQtWindow.object()) : 0L; } QAndroidPlatformScreen *QAndroidPlatformWindow::platformScreen() const @@ -135,7 +222,9 @@ void QAndroidPlatformWindow::propagateSizeHints() void QAndroidPlatformWindow::requestActivateWindow() { - platformScreen()->topWindowChanged(window()); + // raise() will handle differences between top level and child windows, and requesting focus + if (!blockedByModal()) + raise(); } void QAndroidPlatformWindow::updateSystemUiVisibility() @@ -169,4 +258,134 @@ void QAndroidPlatformWindow::applicationStateChanged(Qt::ApplicationState) QWindowSystemInterface::flushWindowSystemEvents(); } +void QAndroidPlatformWindow::createSurface() +{ + const QRect rect = geometry(); + jint x = 0, y = 0, w = -1, h = -1; + if (!rect.isNull()) { + x = rect.x(); + y = rect.y(); + w = std::max(rect.width(), 1); + h = std::max(rect.height(), 1); + } + + const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); + const bool isOpaque = !format().hasAlpha() && qFuzzyCompare(window()->opacity(), 1.0); + + m_nativeQtWindow.callMethod<void>("createSurface", windowStaysOnTop, x, y, w, h, 32, isOpaque, + m_surfaceContainerType); + m_surfaceCreated = true; +} + +void QAndroidPlatformWindow::destroySurface() +{ + if (m_surfaceCreated) { + m_nativeQtWindow.callMethod<void>("destroySurface"); + m_surfaceCreated = false; + } +} + +void QAndroidPlatformWindow::setNativeGeometry(const QRect &geometry) +{ + if (!m_surfaceCreated) + return; + + jint x = 0; + jint y = 0; + jint w = -1; + jint h = -1; + if (!geometry.isNull()) { + x = geometry.x(); + y = geometry.y(); + w = geometry.width(); + h = geometry.height(); + } + m_nativeQtWindow.callMethod<void>("setGeometry", x, y, w, h); +} + +void QAndroidPlatformWindow::onSurfaceChanged(QtJniTypes::Surface surface) +{ + lockSurface(); + m_androidSurfaceObject = surface; + if (m_androidSurfaceObject.isValid()) // wait until we have a valid surface to draw into + m_surfaceWaitCondition.wakeOne(); + unlockSurface(); + + if (m_androidSurfaceObject.isValid()) { + // repaint the window, when we have a valid surface + sendExpose(); + } +} + +void QAndroidPlatformWindow::sendExpose() const +{ + QRect availableGeometry = screen()->availableGeometry(); + if (!geometry().isNull() && !availableGeometry.isNull()) { + QWindowSystemInterface::handleExposeEvent(window(), + QRegion(QRect(QPoint(), geometry().size()))); + } +} + +bool QAndroidPlatformWindow::blockedByModal() const +{ + QWindow *modalWindow = QGuiApplication::modalWindow(); + return modalWindow && modalWindow != window(); +} + +bool QAndroidPlatformWindow::isEmbeddingContainer() const +{ + // Returns true if the window is a wrapper for a foreign window solely to allow embedding Qt + // into a native Android app, in which case we should not try to control it more than we "need" to + return !QtAndroid::isQtApplication() && window()->isTopLevel(); +} + +void QAndroidPlatformWindow::setSurface(JNIEnv *env, jobject object, jint windowId, + QtJniTypes::Surface surface) +{ + Q_UNUSED(env) + Q_UNUSED(object) + + if (!qGuiApp) + return; + + const QList<QWindow*> windows = qGuiApp->allWindows(); + for (QWindow * window : windows) { + if (!window->handle()) + continue; + QAndroidPlatformWindow *platformWindow = + static_cast<QAndroidPlatformWindow *>(window->handle()); + if (platformWindow->nativeViewId() == windowId) + platformWindow->onSurfaceChanged(surface); + } +} + +void QAndroidPlatformWindow::windowFocusChanged(JNIEnv *env, jobject object, + jboolean focus, jint windowId) +{ + Q_UNUSED(env) + Q_UNUSED(object) + QWindow* window = QtAndroid::windowFromId(windowId); + Q_ASSERT_X(window, "QAndroidPlatformWindow", "windowFocusChanged event window should exist"); + if (focus) { + QWindowSystemInterface::handleFocusWindowChanged(window); + } else if (!focus && window == qGuiApp->focusWindow()) { + // Clear focus if current window has lost focus + QWindowSystemInterface::handleFocusWindowChanged(nullptr); + } +} + +bool QAndroidPlatformWindow::registerNatives(QJniEnvironment &env) +{ + if (!env.registerNativeMethods(QtJniTypes::Traits<QtJniTypes::QtWindow>::className(), + { + Q_JNI_NATIVE_SCOPED_METHOD(setSurface, QAndroidPlatformWindow), + Q_JNI_NATIVE_SCOPED_METHOD(windowFocusChanged, QAndroidPlatformWindow) + })) { + qCCritical(lcQpaWindow) << "RegisterNatives failed for" + << QtJniTypes::Traits<QtJniTypes::QtWindow>::className(); + return false; + } + return true; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformwindow.h b/src/plugins/platforms/android/qandroidplatformwindow.h index 6fccc2e7fe..3f1e8ac992 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.h +++ b/src/plugins/platforms/android/qandroidplatformwindow.h @@ -7,17 +7,32 @@ #include <qobject.h> #include <qrect.h> #include <qpa/qplatformwindow.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qmutex.h> +#include <QtCore/qwaitcondition.h> +#include <jni.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow) +Q_DECLARE_JNI_CLASS(QtWindow, "org/qtproject/qt/android/QtWindow") +Q_DECLARE_JNI_CLASS(Surface, "android/view/Surface") + class QAndroidPlatformScreen; -class QAndroidPlatformBackingStore; class QAndroidPlatformWindow: public QPlatformWindow { public: - explicit QAndroidPlatformWindow(QWindow *window); + enum class SurfaceContainer { + SurfaceView, + TextureView + }; + explicit QAndroidPlatformWindow(QWindow *window); + ~QAndroidPlatformWindow(); void lower() override; void raise() override; @@ -27,7 +42,8 @@ public: void setWindowFlags(Qt::WindowFlags flags) override; Qt::WindowFlags windowFlags() const; void setParent(const QPlatformWindow *window) override; - WId winId() const override { return m_windowId; } + + WId winId() const override; bool setMouseGrabEnabled(bool grab) override { Q_UNUSED(grab); return false; } bool setKeyboardGrabEnabled(bool grab) override { Q_UNUSED(grab); return false; } @@ -39,32 +55,46 @@ public: void propagateSizeHints() override; void requestActivateWindow() override; void updateSystemUiVisibility(); - inline bool isRaster() const { - if (isForeignWindow()) - return false; - - return window()->surfaceType() == QSurface::RasterSurface - || window()->surfaceType() == QSurface::RasterGLSurface; - } + inline bool isRaster() const { return m_isRaster; } bool isExposed() const override; + QtJniTypes::QtWindow nativeWindow() const { return m_nativeQtWindow; } virtual void applicationStateChanged(Qt::ApplicationState); + int nativeViewId() const { return m_nativeViewId; } - void setBackingStore(QAndroidPlatformBackingStore *store) { m_backingStore = store; } - QAndroidPlatformBackingStore *backingStore() const { return m_backingStore; } - - virtual void repaint(const QRegion &) { } + static bool registerNatives(QJniEnvironment &env); + void onSurfaceChanged(QtJniTypes::Surface surface); protected: void setGeometry(const QRect &rect) override; + void lockSurface() { m_surfaceMutex.lock(); } + void unlockSurface() { m_surfaceMutex.unlock(); } + void createSurface(); + void destroySurface(); + void setNativeGeometry(const QRect &geometry); + void sendExpose() const; + bool blockedByModal() const; + bool isEmbeddingContainer() const; -protected: Qt::WindowFlags m_windowFlags; Qt::WindowStates m_windowState; - - WId m_windowId; - - QAndroidPlatformBackingStore *m_backingStore = nullptr; + bool m_isRaster; + + int m_nativeViewId = -1; + QtJniTypes::QtWindow m_nativeQtWindow; + SurfaceContainer m_surfaceContainerType = SurfaceContainer::SurfaceView; + QtJniTypes::QtWindow m_nativeParentQtWindow; + // The Android Surface, accessed from multiple threads, guarded by m_surfaceMutex + QtJniTypes::Surface m_androidSurfaceObject; + QWaitCondition m_surfaceWaitCondition; + bool m_surfaceCreated = false; + QMutex m_surfaceMutex; + +private: + static void setSurface(JNIEnv *env, jobject obj, jint windowId, QtJniTypes::Surface surface); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setSurface) + static void windowFocusChanged(JNIEnv *env, jobject object, jboolean focus, jint windowId); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(windowFocusChanged) }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidsystemlocale.cpp b/src/plugins/platforms/android/qandroidsystemlocale.cpp index 858934b1f8..c5f2e59097 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.cpp +++ b/src/plugins/platforms/android/qandroidsystemlocale.cpp @@ -12,34 +12,38 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(Locale, "java/util/Locale") +Q_DECLARE_JNI_CLASS(Resources, "android/content/res/Resources") +Q_DECLARE_JNI_CLASS(Configuration, "android/content/res/Configuration") +Q_DECLARE_JNI_CLASS(LocaleList, "android/os/LocaleList") + +using namespace QtJniTypes; + QAndroidSystemLocale::QAndroidSystemLocale() : m_locale(QLocale::C) { } void QAndroidSystemLocale::getLocaleFromJava() const { - QWriteLocker locker(&m_lock); - - QJniObject javaLocaleObject; - QJniObject javaActivity(QtAndroid::activity()); - if (!javaActivity.isValid()) - javaActivity = QtAndroid::service(); - if (javaActivity.isValid()) { - QJniObject resources = javaActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;"); - QJniObject configuration = resources.callObjectMethod("getConfiguration", "()Landroid/content/res/Configuration;"); - - javaLocaleObject = configuration.getObjectField("locale", "Ljava/util/Locale;"); - } else { - javaLocaleObject = QJniObject::callStaticObjectMethod("java/util/Locale", "getDefault", "()Ljava/util/Locale;"); - } + const Locale javaLocaleObject = []{ + const QJniObject javaContext = QtAndroidPrivate::context(); + if (javaContext.isValid()) { + const QJniObject resources = javaContext.callMethod<Resources>("getResources"); + const QJniObject configuration = resources.callMethod<Configuration>("getConfiguration"); + return configuration.getField<Locale>("locale"); + } else { + return Locale::callStaticMethod<Locale>("getDefault"); + } + }(); - QString languageCode = javaLocaleObject.callObjectMethod("getLanguage", "()Ljava/lang/String;").toString(); - QString countryCode = javaLocaleObject.callObjectMethod("getCountry", "()Ljava/lang/String;").toString(); + const QString languageCode = javaLocaleObject.callMethod<QString>("getLanguage"); + const QString countryCode = javaLocaleObject.callMethod<QString>("getCountry"); + QWriteLocker locker(&m_lock); m_locale = QLocale(languageCode + u'_' + countryCode); } -QVariant QAndroidSystemLocale::query(QueryType type, QVariant in) const +QVariant QAndroidSystemLocale::query(QueryType type, QVariant &&in) const { if (type == LocaleChanged) { getLocaleFromJava(); @@ -142,12 +146,9 @@ QVariant QAndroidSystemLocale::query(QueryType type, QVariant in) const Q_ASSERT_X(false, Q_FUNC_INFO, "This can't happen."); case UILanguages: { if (QtAndroidPrivate::androidSdkVersion() >= 24) { - QJniObject localeListObject = - QJniObject::callStaticObjectMethod("android/os/LocaleList", "getDefault", - "()Landroid/os/LocaleList;"); + LocaleList localeListObject = LocaleList::callStaticMethod<LocaleList>("getDefault"); if (localeListObject.isValid()) { - QString lang = localeListObject.callObjectMethod("toLanguageTags", - "()Ljava/lang/String;").toString(); + QString lang = localeListObject.callMethod<QString>("toLanguageTags"); // Some devices return with it enclosed in []'s so check if both exists before // removing to ensure it is formatted correctly if (lang.startsWith(QChar('[')) && lang.endsWith(QChar(']'))) diff --git a/src/plugins/platforms/android/qandroidsystemlocale.h b/src/plugins/platforms/android/qandroidsystemlocale.h index 48e1d94a56..cd37b48270 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.h +++ b/src/plugins/platforms/android/qandroidsystemlocale.h @@ -11,10 +11,11 @@ QT_BEGIN_NAMESPACE class QAndroidSystemLocale : public QSystemLocale { + Q_DISABLE_COPY_MOVE(QAndroidSystemLocale) public: QAndroidSystemLocale(); - QVariant query(QueryType type, QVariant in) const override; + QVariant query(QueryType type, QVariant &&in) const override; QLocale fallbackLocale() const override; private: diff --git a/src/plugins/platforms/cocoa/CMakeLists.txt b/src/plugins/platforms/cocoa/CMakeLists.txt index af8434daaa..491c61703f 100644 --- a/src/plugins/platforms/cocoa/CMakeLists.txt +++ b/src/plugins/platforms/cocoa/CMakeLists.txt @@ -7,7 +7,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin OUTPUT_NAME qcocoa - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES cocoa + DEFAULT_IF "cocoa" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms SOURCES main.mm @@ -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..40c1e90511 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -36,6 +36,23 @@ void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) } switch (event->type()) { + case QAccessible::Announcement: { + auto *announcementEvent = static_cast<QAccessibleAnnouncementEvent *>(event); + auto priorityLevel = (announcementEvent->priority() == QAccessible::AnnouncementPriority::Assertive) + ? NSAccessibilityPriorityHigh + : NSAccessibilityPriorityMedium; + NSDictionary *announcementInfo = @{ + NSAccessibilityPriorityKey: [NSNumber numberWithInt:priorityLevel], + NSAccessibilityAnnouncementKey: announcementEvent->message().toNSString() + }; + // post event for application element, as the comment for + // NSAccessibilityAnnouncementRequestedNotification in the + // NSAccessibilityConstants.h header says + NSAccessibilityPostNotificationWithUserInfo(NSApp, + NSAccessibilityAnnouncementRequestedNotification, + announcementInfo); + break; + } case QAccessible::Focus: { NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); break; @@ -117,7 +134,6 @@ static void populateRoleMap() roleMap[QAccessible::ColumnHeader] = NSAccessibilityColumnRole; roleMap[QAccessible::Row] = NSAccessibilityRowRole; roleMap[QAccessible::RowHeader] = NSAccessibilityRowRole; - roleMap[QAccessible::Cell] = NSAccessibilityTextFieldRole; roleMap[QAccessible::Button] = NSAccessibilityButtonRole; roleMap[QAccessible::EditableText] = NSAccessibilityTextFieldRole; roleMap[QAccessible::Link] = NSAccessibilityLinkRole; @@ -125,7 +141,7 @@ static void populateRoleMap() roleMap[QAccessible::Splitter] = NSAccessibilitySplitGroupRole; roleMap[QAccessible::List] = NSAccessibilityListRole; roleMap[QAccessible::ListItem] = NSAccessibilityStaticTextRole; - roleMap[QAccessible::Cell] = NSAccessibilityStaticTextRole; + roleMap[QAccessible::Cell] = NSAccessibilityCellRole; roleMap[QAccessible::Client] = NSAccessibilityGroupRole; roleMap[QAccessible::Paragraph] = NSAccessibilityGroupRole; roleMap[QAccessible::Section] = NSAccessibilityGroupRole; @@ -137,6 +153,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..97e0f633a7 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; @@ -43,6 +44,7 @@ public: static const char *name; + void requestColorScheme(Qt::ColorScheme scheme) override; void handleSystemThemeChange(); #ifndef QT_NO_SHORTCUT @@ -54,6 +56,9 @@ private: QMacNotificationObserver m_systemColorObserver; mutable QHash<QPlatformTheme::Palette, QPalette*> m_palettes; QMacKeyValueObserver m_appearanceObserver; + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + void updateColorScheme(); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index 594df204e5..d9135c76c8 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,36 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const Qt::ColorScheme QCocoaTheme::colorScheme() const { - return qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; + return m_colorScheme; +} + +void QCocoaTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + NSAppearance *appearance = nil; + switch (scheme) { + case Qt::ColorScheme::Dark: + appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + break; + case Qt::ColorScheme::Light: + appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + break; + case Qt::ColorScheme::Unknown: + break; + } + if (appearance != NSApp.effectiveAppearance) + NSApplication.sharedApplication.appearance = appearance; +} + +/* + Update the theme's color scheme based on the current appearance. + + We can only reference the appearance on the main thread, but the + CoreText font engine needs to know the color scheme, and might be + used from secondary threads, so we cache the color scheme. +*/ +void QCocoaTheme::updateColorScheme() +{ + m_colorScheme = qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; } QString QCocoaTheme::standardButtonText(int button) const @@ -490,12 +522,16 @@ QKeySequence QCocoaTheme::standardButtonShortcut(int button) const QPlatformMenuItem *QCocoaTheme::createPlatformMenuItem() const { - return new QCocoaMenuItem(); + auto *menuItem = new QCocoaMenuItem(); + qCDebug(lcQpaMenus) << "Created" << menuItem; + return menuItem; } QPlatformMenu *QCocoaTheme::createPlatformMenu() const { - return new QCocoaMenu(); + auto *menu = new QCocoaMenu(); + qCDebug(lcQpaMenus) << "Created" << menu; + return menu; } QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const @@ -508,7 +544,9 @@ QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const SLOT(onAppFocusWindowChanged(QWindow*))); } - return new QCocoaMenuBar(); + auto *menuBar = new QCocoaMenuBar(); + qCDebug(lcQpaMenus) << "Created" << menuBar; + return menuBar; } #ifndef QT_NO_SHORTCUT diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 2083803c6d..d2c9bb0196 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 @@ -467,7 +514,7 @@ NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags) auto *nsWindow = transientCocoaWindow->nativeWindow(); // We only upgrade the window level for "special" windows, to work - // around Qt Designer parenting the designer windows to the widget + // around Qt Widgets Designer parenting the designer windows to the widget // palette window (QTBUG-31779). This should be fixed in designer. if (type != Qt::Window) windowLevel = qMax(windowLevel, nsWindow.level); @@ -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..cb4b5d1eb9 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: @@ -90,7 +92,7 @@ endif() qt_internal_add_plugin(QEglFSIntegrationPlugin OUTPUT_NAME qeglfs PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES eglfs + DEFAULT_IF "eglfs" IN_LIST QT_QPA_PLATFORMS SOURCES qeglfsmain.cpp DEFINES 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..51c1b52cf3 100644 --- a/src/plugins/platforms/ios/CMakeLists.txt +++ b/src/plugins/platforms/ios/CMakeLists.txt @@ -5,10 +5,23 @@ ## QIOSIntegrationPlugin Plugin: ##################################################################### +if(VISIONOS) + include(SwiftIntegration.cmake) + + qt_install(TARGETS QIOSIntegrationPluginSwift + EXPORT "${INSTALL_CMAKE_NAMESPACE}QIOSIntegrationPluginTargets" + DESTINATION "${INSTALL_LIBDIR}" + ) + qt_internal_add_targets_to_additional_targets_export_file( + TARGETS QIOSIntegrationPluginSwift + EXPORT_NAME_PREFIX "${INSTALL_CMAKE_NAMESPACE}QIOSIntegrationPlugin" + ) +endif() + qt_internal_add_plugin(QIOSIntegrationPlugin OUTPUT_NAME qios STATIC # Force static, even in shared builds - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES ios + DEFAULT_IF "ios" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms SOURCES plugin.mm @@ -27,7 +40,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 +71,35 @@ 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) + +if(VISIONOS) + target_link_libraries(QIOSIntegrationPlugin PRIVATE QIOSIntegrationPluginSwift) +endif() diff --git a/src/plugins/platforms/ios/SwiftIntegration.cmake b/src/plugins/platforms/ios/SwiftIntegration.cmake new file mode 100644 index 0000000000..d52edb3ad2 --- /dev/null +++ b/src/plugins/platforms/ios/SwiftIntegration.cmake @@ -0,0 +1,78 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +set(CMAKE_Swift_COMPILER_TARGET arm64-apple-xros) +if($CACHE{CMAKE_OSX_SYSROOT} MATCHES "^[a-z]+simulator$") + set(CMAKE_Swift_COMPILER_TARGET "${CMAKE_Swift_COMPILER_TARGET}-simulator") +endif() + +cmake_policy(SET CMP0157 NEW) +enable_language(Swift) + +# Verify that we have a new enough compiler +if("${CMAKE_Swift_COMPILER_VERSION}" VERSION_LESS 5.9) + message(FATAL_ERROR "Swift 5.9 required for C++ interoperability") +endif() + +get_target_property(QT_CORE_INCLUDES Qt6::Core INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_GUI_INCLUDES Qt6::Gui INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_CORE_PRIVATE_INCLUDES Qt6::CorePrivate INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_GUI_PRIVATE_INCLUDES Qt6::GuiPrivate INTERFACE_INCLUDE_DIRECTORIES) + +set(target QIOSIntegrationPluginSwift) +# Swift library +set(SWIFT_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/qiosapplication.swift" +) +add_library(${target} STATIC ${SWIFT_SOURCES}) +set_target_properties(${target} PROPERTIES + Swift_MODULE_NAME ${target}) +target_include_directories(${target} PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + "${QT_CORE_INCLUDES}" + "${QT_GUI_INCLUDES}" + "${QT_CORE_PRIVATE_INCLUDES}" + "${QT_GUI_PRIVATE_INCLUDES}" + +) +target_compile_options(${target} PUBLIC + $<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default> + $<$<COMPILE_LANGUAGE:Swift>:-Xcc -std=c++17>) + +# Swift to C++ bridging header +set(SWIFT_BRIDGING_HEADER "${CMAKE_CURRENT_BINARY_DIR}/qiosswiftintegration.h") +list(TRANSFORM QT_CORE_INCLUDES PREPEND "-I") +list(TRANSFORM QT_GUI_INCLUDES PREPEND "-I") +list(TRANSFORM QT_CORE_PRIVATE_INCLUDES PREPEND "-I") +list(TRANSFORM QT_GUI_PRIVATE_INCLUDES PREPEND "-I") +add_custom_command( + COMMAND + ${CMAKE_Swift_COMPILER} -frontend -typecheck + ${SWIFT_SOURCES} + -I ${CMAKE_CURRENT_SOURCE_DIR} + ${QT_CORE_INCLUDES} + ${QT_GUI_INCLUDES} + ${QT_CORE_PRIVATE_INCLUDES} + ${QT_GUI_PRIVATE_INCLUDES} + -sdk ${CMAKE_OSX_SYSROOT} + -module-name ${target} + -cxx-interoperability-mode=default + -Xcc -std=c++17 + -emit-clang-header-path "${SWIFT_BRIDGING_HEADER}" + -target ${CMAKE_Swift_COMPILER_TARGET} + OUTPUT + "${SWIFT_BRIDGING_HEADER}" + DEPENDS + ${SWIFT_SOURCES} + ) + +set(header_target "${target}Header") +add_custom_target(${header_target} + DEPENDS "${SWIFT_BRIDGING_HEADER}" +) +# Make sure the "'__bridge_transfer' casts have no effect when not using ARC" +# warning doesn't break warnings-are-error builds. +target_compile_options(${target} INTERFACE + -Wno-error=arc-bridge-casts-disallowed-in-nonarc) + +add_dependencies(${target} ${header_target}) diff --git a/src/plugins/platforms/ios/module.modulemap b/src/plugins/platforms/ios/module.modulemap new file mode 100644 index 0000000000..af42b3e1f5 --- /dev/null +++ b/src/plugins/platforms/ios/module.modulemap @@ -0,0 +1,4 @@ +module QIOSIntegrationPlugin { + header "qiosapplicationdelegate.h" + header "qiosintegration.h" +} diff --git a/src/plugins/platforms/ios/optional/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/qiosapplication.swift b/src/plugins/platforms/ios/qiosapplication.swift new file mode 100644 index 0000000000..6f75ebd0b5 --- /dev/null +++ b/src/plugins/platforms/ios/qiosapplication.swift @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import SwiftUI +import CompositorServices +import QIOSIntegrationPlugin +import RealityKit + +struct QIOSSwiftApplication: App { + @UIApplicationDelegateAdaptor private var appDelegate: QIOSApplicationDelegate + + var body: some SwiftUI.Scene { + WindowGroup() { + ImmersiveSpaceControlView() + } + + ImmersiveSpace(id: "QIOSImmersiveSpace") { + CompositorLayer(configuration: QIOSLayerConfiguration()) { layerRenderer in + QIOSIntegration.instance().renderCompositorLayer(layerRenderer) + } + } + // CompositorLayer immersive spaces are always full, and should not need + // to set the immersion style, but lacking this we get a warning in the + // console about not being able to "configure an immersive space with + // selected style 'AutomaticImmersionStyle' since it is not in the list + // of supported styles for this type of content: 'FullImmersionStyle'." + .immersionStyle(selection: .constant(.full), in: .full) + } +} + +public struct QIOSLayerConfiguration: CompositorLayerConfiguration { + public func makeConfiguration(capabilities: LayerRenderer.Capabilities, + configuration: inout LayerRenderer.Configuration) { + // Use reflection to pull out underlying C handles + // FIXME: Use proper bridging APIs when available + let capabilitiesMirror = Mirror(reflecting: capabilities) + let configurationMirror = Mirror(reflecting: configuration) + QIOSIntegration.instance().configureCompositorLayer( + capabilitiesMirror.descendant("c_capabilities") as? cp_layer_renderer_capabilities_t, + configurationMirror.descendant("box", "value") as? cp_layer_renderer_configuration_t + ) + } +} + +public func runSwiftAppMain() { + QIOSSwiftApplication.main() +} + +public class ImmersiveState: ObservableObject { + static let shared = ImmersiveState() + @Published var showImmersiveSpace: Bool = false +} + +struct ImmersiveSpaceControlView: View { + @ObservedObject private var immersiveState = ImmersiveState.shared + + @Environment(\.openImmersiveSpace) var openImmersiveSpace + @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace + + var body: some View { + VStack {} + .onChange(of: immersiveState.showImmersiveSpace) { _, newValue in + Task { + if newValue { + await openImmersiveSpace(id: "QIOSImmersiveSpace") + } else { + await dismissImmersiveSpace() + } + } + } + } +} + +public class ImmersiveSpaceManager : NSObject { + @objc public static func openImmersiveSpace() { + ImmersiveState.shared.showImmersiveSpace = true + } + + @objc public static func dismissImmersiveSpace() { + ImmersiveState.shared.showImmersiveSpace = false + } +} diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.h b/src/plugins/platforms/ios/qiosapplicationdelegate.h index 39bb9fdedb..7e12d64cbf 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.h +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.h @@ -1,6 +1,9 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QIOSAPPLICATIONDELEGATE_H +#define QIOSAPPLICATIONDELEGATE_H + #import <UIKit/UIKit.h> #import <QtGui/QtGui> @@ -8,3 +11,5 @@ @interface QIOSApplicationDelegate : UIResponder <UIApplicationDelegate> @end + +#endif // QIOSAPPLICATIONDELEGATE_H diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm index a017fef457..c6e5a83874 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm @@ -3,15 +3,21 @@ #include "qiosapplicationdelegate.h" +#include "qiosglobal.h" #include "qiosintegration.h" #include "qiosservices.h" #include "qiosviewcontroller.h" #include "qioswindow.h" +#include "qiosscreen.h" +#include "quiwindow.h" #include <qpa/qplatformintegration.h> #include <QtCore/QtCore> +@interface QIOSWindowSceneDelegate : NSObject<UIWindowSceneDelegate> +@end + @implementation QIOSApplicationDelegate - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *restorableObjects))restorationHandler @@ -50,5 +56,44 @@ return iosServices->handleUrl(QUrl::fromNSURL(url)); } +- (UISceneConfiguration *)application:(UIApplication *)application + configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession + options:(UISceneConnectionOptions *)options +{ + qCDebug(lcQpaWindowScene) << "Configuring scene for" << connectingSceneSession + << "with options" << options; + + auto *sceneConfig = connectingSceneSession.configuration; + sceneConfig.delegateClass = QIOSWindowSceneDelegate.class; + return sceneConfig; +} + @end +@implementation QIOSWindowSceneDelegate + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions +{ + qCDebug(lcQpaWindowScene) << "Connecting" << scene << "to" << session; + + Q_ASSERT([scene isKindOfClass:UIWindowScene.class]); + UIWindowScene *windowScene = static_cast<UIWindowScene*>(scene); + + QUIWindow *window = [[QUIWindow alloc] initWithWindowScene:windowScene]; + + QIOSScreen *screen = [&]{ + for (auto *screen : qGuiApp->screens()) { + auto *platformScreen = static_cast<QIOSScreen*>(screen->handle()); +#if !defined(Q_OS_VISIONOS) + if (platformScreen->uiScreen() == windowScene.screen) +#endif + return platformScreen; + } + Q_UNREACHABLE(); + }(); + + window.rootViewController = [[[QIOSViewController alloc] + initWithWindow:window andScreen:screen] autorelease]; +} + +@end diff --git a/src/plugins/platforms/ios/qiosclipboard.mm b/src/plugins/platforms/ios/qiosclipboard.mm index 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..09e2f2f4c3 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:UTTypeContent]; + [docTypes addObject:UTTypeItem]; + [docTypes addObject:UTTypeData]; break; // Showing files is not supported in Directory mode in iOS case QFileDialogOptions::Directory: case QFileDialogOptions::DirectoryOnly: - [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)kUTTypeFolder]]; + [docTypes addObject:UTTypeFolder]; break; } } diff --git a/src/plugins/platforms/ios/qioseventdispatcher.h b/src/plugins/platforms/ios/qioseventdispatcher.h index b40024ec19..5eee0556f5 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.h +++ b/src/plugins/platforms/ios/qioseventdispatcher.h @@ -16,6 +16,8 @@ public: static QIOSEventDispatcher* create(); bool processPostedEvents() override; + static bool isQtApplication(); + protected: explicit QIOSEventDispatcher(QObject *parent = nullptr); }; diff --git a/src/plugins/platforms/ios/qioseventdispatcher.mm b/src/plugins/platforms/ios/qioseventdispatcher.mm index 24d9d88294..710a834bfd 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.mm +++ b/src/plugins/platforms/ios/qioseventdispatcher.mm @@ -5,6 +5,10 @@ #include "qiosapplicationdelegate.h" #include "qiosglobal.h" +#if defined(Q_OS_VISIONOS) +#include "qiosswiftintegration.h" +#endif + #include <QtCore/qprocessordetection.h> #include <QtCore/private/qcoreapplication_p.h> #include <QtCore/private/qthread_p.h> @@ -173,12 +177,16 @@ namespace QAppleLogActivity UIApplicationMain; QAppleLogActivity applicationDidFinishLaunching; } logActivity; + + static bool s_isQtApplication = false; } using namespace QT_PREPEND_NAMESPACE(QtPrivate); extern "C" int qt_main_wrapper(int argc, char *argv[]) { + s_isQtApplication = true; + @autoreleasepool { size_t defaultStackSize = 512 * kBytesPerKiloByte; // Same as secondary threads @@ -202,8 +210,16 @@ extern "C" int qt_main_wrapper(int argc, char *argv[]) logActivity.UIApplicationMain = QT_APPLE_LOG_ACTIVITY( lcEventDispatcher().isDebugEnabled(), "UIApplicationMain").enter(); +#if defined(Q_OS_VISIONOS) + Q_UNUSED(argc); + Q_UNUSED(argv); + qCDebug(lcEventDispatcher) << "Starting Swift app"; + QIOSIntegrationPluginSwift::runSwiftAppMain(); + Q_UNREACHABLE(); +#else qCDebug(lcEventDispatcher) << "Running UIApplicationMain"; return UIApplicationMain(argc, argv, nil, NSStringFromClass([QIOSApplicationDelegate class])); +#endif } } @@ -424,6 +440,11 @@ QIOSEventDispatcher::QIOSEventDispatcher(QObject *parent) QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } +bool QIOSEventDispatcher::isQtApplication() +{ + return s_isQtApplication; +} + /*! Override of the CoreFoundation posted events runloop source callback so that we can send window system (QPA) events in addition to sending diff --git a/src/plugins/platforms/ios/qiosfiledialog.h b/src/plugins/platforms/ios/qiosfiledialog.h index 12a3af7181..f00c154c03 100644 --- a/src/plugins/platforms/ios/qiosfiledialog.h +++ b/src/plugins/platforms/ios/qiosfiledialog.h @@ -39,6 +39,7 @@ private: bool showImagePickerDialog(QWindow *parent); bool showNativeDocumentPickerDialog(QWindow *parent); + void showImagePickerDialog_helper(QWindow *parent); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosfiledialog.mm b/src/plugins/platforms/ios/qiosfiledialog.mm index 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..1722e09aaa 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -5,6 +5,8 @@ #include "qiosapplicationdelegate.h" #include "qiosviewcontroller.h" #include "qiosscreen.h" +#include "quiwindow.h" +#include "qioseventdispatcher.h" #include <QtCore/private/qcore_mac_p.h> @@ -13,20 +15,26 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcQpaApplication, "qt.qpa.application"); Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window"); +Q_LOGGING_CATEGORY(lcQpaWindowScene, "qt.qpa.window.scene"); bool isQtApplication() { - if (qt_apple_isApplicationExtension()) - return false; - // Returns \c true if the plugin is in full control of the whole application. This means // that we control the application delegate and the top view controller, and can take // actions that impacts all parts of the application. The opposite means that we are // embedded inside a native iOS application, and should be more focused on playing along // with native UIControls, and less inclined to change structures that lies outside the // scope of our QWindows/UIViews. - static bool isQt = ([qt_apple_sharedApplication().delegate isKindOfClass:[QIOSApplicationDelegate class]]); - return isQt; + return QIOSEventDispatcher::isQtApplication(); +} + +bool isRunningOnVisionOS() +{ + static bool result = []{ + // This class is documented to only be available on visionOS + return NSClassFromString(@"UIWindowSceneGeometryPreferencesVision"); + }(); + return result; } #ifndef Q_OS_TVOS @@ -85,6 +93,53 @@ int infoPlistValue(NSString* key, int defaultValue) return value ? [value intValue] : defaultValue; } +UIWindow *presentationWindow(QWindow *window) +{ + UIWindow *uiWindow = window ? reinterpret_cast<UIView *>(window->winId()).window : nullptr; + if (!uiWindow) { + auto *scenes = [qt_apple_sharedApplication().connectedScenes allObjects]; + if (scenes.count > 0) { + auto *windowScene = static_cast<UIWindowScene*>(scenes[0]); + uiWindow = windowScene.keyWindow; + if (!uiWindow && windowScene.windows.count) + uiWindow = windowScene.windows[0]; + } + } + return uiWindow; +} + +UIView *rootViewForScreen(QScreen *screen) +{ + const auto *iosScreen = static_cast<QIOSScreen *>(screen->handle()); + for (UIScene *scene in [qt_apple_sharedApplication().connectedScenes allObjects]) { + if (![scene isKindOfClass:UIWindowScene.class]) + continue; + + auto *windowScene = static_cast<UIWindowScene*>(scene); + +#if !defined(Q_OS_VISIONOS) + if (windowScene.screen != iosScreen->uiScreen()) + continue; +#else + Q_UNUSED(iosScreen); +#endif + + UIWindow *uiWindow = qt_objc_cast<QUIWindow*>(windowScene.keyWindow); + if (!uiWindow) { + for (UIWindow *win in windowScene.windows) { + if (qt_objc_cast<QUIWindow*>(win)) { + uiWindow = win; + break; + } + } + } + + return uiWindow.rootViewController.view; + } + + return nullptr; +} + QT_END_NAMESPACE // ------------------------------------------------------------------------- @@ -119,7 +174,7 @@ QT_END_NAMESPACE @implementation UIResponder (QtFirstResponder) -+ (id)currentFirstResponder ++ (id)qt_currentFirstResponder { if (qt_apple_isApplicationExtension()) { qWarning() << "can't get first responder in application extensions!"; diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index 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..53f64c1748 100644 --- a/src/plugins/platforms/ios/qiosintegration.h +++ b/src/plugins/platforms/ios/qiosintegration.h @@ -11,15 +11,29 @@ #include <QtCore/private/qfactoryloader_p.h> #include "qiosapplicationstate.h" -#ifndef Q_OS_TVOS + +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) #include "qiostextinputoverlay.h" #endif +#if defined(Q_OS_VISIONOS) +#include <swift/bridging> +#endif + QT_BEGIN_NAMESPACE +using namespace QNativeInterface; + class QIOSServices; -class QIOSIntegration : public QPlatformNativeInterface, public QPlatformIntegration +class +#if defined(Q_OS_VISIONOS) + SWIFT_IMMORTAL_REFERENCE +#endif +QIOSIntegration : public QPlatformNativeInterface, public QPlatformIntegration +#if defined(Q_OS_VISIONOS) + , public QVisionOSApplication +#endif { Q_OBJECT public: @@ -31,6 +45,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 +55,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; @@ -73,9 +90,20 @@ public: QIOSApplicationState applicationState; +#if defined(Q_OS_VISIONOS) + void openImmersiveSpace() override; + void dismissImmersiveSpace() override; + + using CompositorLayer = QVisionOSApplication::ImmersiveSpaceCompositorLayer; + void setImmersiveSpaceCompositorLayer(CompositorLayer *layer) override; + + void configureCompositorLayer(cp_layer_renderer_capabilities_t, cp_layer_renderer_configuration_t); + void renderCompositorLayer(cp_layer_renderer_t); +#endif + private: QPlatformFontDatabase *m_fontDatabase; -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) QPlatformClipboard *m_clipboard; #endif QPlatformInputContext *m_inputContext; @@ -83,9 +111,13 @@ private: QIOSServices *m_platformServices; mutable QPlatformAccessibility *m_accessibility; QFactoryLoader *m_optionalPlugins; -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QIOSTextInputOverlay m_textInputOverlay; #endif + +#if defined(Q_OS_VISIONOS) + CompositorLayer *m_immersiveSpaceCompositorLayer = nullptr; +#endif }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index 27d0f7f2ba..2c32957c03 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" @@ -15,6 +17,10 @@ #include "qiosservices.h" #include "qiosoptionalplugininterface.h" +#if defined(Q_OS_VISIONOS) +#include "qiosswiftintegration.h" +#endif + #include <QtGui/qpointingdevice.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qrhibackingstore_p.h> @@ -49,7 +55,7 @@ QIOSIntegration *QIOSIntegration::instance() QIOSIntegration::QIOSIntegration() : m_fontDatabase(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>) -#if !defined(Q_OS_TVOS) && !defined(QT_NO_CLIPBOARD) +#if QT_CONFIG(clipboard) , m_clipboard(new QIOSClipboard) #endif , m_inputContext(0) @@ -70,6 +76,10 @@ QIOSIntegration::QIOSIntegration() void QIOSIntegration::initialize() { +#if defined(Q_OS_VISIONOS) + // Qt requires a screen, so let's give it a dummy one + QWindowSystemInterface::handleScreenAdded(new QIOSScreen); +#else UIScreen *mainScreen = [UIScreen mainScreen]; NSMutableArray<UIScreen *> *screens = [[[UIScreen screens] mutableCopy] autorelease]; if (![screens containsObject:mainScreen]) { @@ -79,6 +89,7 @@ void QIOSIntegration::initialize() for (UIScreen *screen in screens) QWindowSystemInterface::handleScreenAdded(new QIOSScreen(screen)); +#endif // Depends on a primary screen being present m_inputContext = new QIOSInputContext; @@ -86,8 +97,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 +118,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 +161,8 @@ bool QIOSIntegration::hasCapability(Capability cap) const return false; case ApplicationState: return true; + case ForeignWindows: + return true; default: return QPlatformIntegration::hasCapability(cap); } @@ -158,6 +173,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 +219,10 @@ QPlatformFontDatabase * QIOSIntegration::fontDatabase() const return m_fontDatabase; } -#ifndef QT_NO_CLIPBOARD +#if QT_CONFIG(clipboard) QPlatformClipboard *QIOSIntegration::clipboard() const { -#ifndef Q_OS_TVOS return m_clipboard; -#else - return QPlatformIntegration::clipboard(); -#endif } #endif @@ -284,6 +300,39 @@ void QIOSIntegration::setApplicationBadge(qint64 number) // --------------------------------------------------------- +#if defined(Q_OS_VISIONOS) +void QIOSIntegration::openImmersiveSpace() +{ + [ImmersiveSpaceManager openImmersiveSpace]; +} + +void QIOSIntegration::dismissImmersiveSpace() +{ + [ImmersiveSpaceManager dismissImmersiveSpace]; +} + +void QIOSIntegration::setImmersiveSpaceCompositorLayer(CompositorLayer *layer) +{ + m_immersiveSpaceCompositorLayer = layer; +} + +void QIOSIntegration::configureCompositorLayer(cp_layer_renderer_capabilities_t capabilities, + cp_layer_renderer_configuration_t configuration) +{ + if (m_immersiveSpaceCompositorLayer) + m_immersiveSpaceCompositorLayer->configure(capabilities, configuration); +} + +void QIOSIntegration::renderCompositorLayer(cp_layer_renderer_t renderer) +{ + if (m_immersiveSpaceCompositorLayer) + m_immersiveSpaceCompositorLayer->render(renderer); +} + +#endif + +// --------------------------------------------------------- + void *QIOSIntegration::nativeResourceForWindow(const QByteArray &resource, QWindow *window) { if (!window || !window->handle()) 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..83170c1851 100644 --- a/src/plugins/platforms/ios/qiostextinputoverlay.mm +++ b/src/plugins/platforms/ios/qiostextinputoverlay.mm @@ -434,7 +434,7 @@ static void executeBlockWithoutAnimation(Block block) if (enabled) { _focusView = [reinterpret_cast<UIView *>(qApp->focusWindow()->winId()) retain]; - _desktopView = [qt_apple_sharedApplication().keyWindow.rootViewController.view retain]; + _desktopView = [presentationWindow(nullptr).rootViewController.view retain]; Q_ASSERT(_focusView && _desktopView && _desktopView.superview); [_desktopView addGestureRecognizer:self]; } else { @@ -947,7 +947,17 @@ static void executeBlockWithoutAnimation(Block block) int cursorPosOnRelease = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, touchPos).toInt(); if (cursorPosOnRelease == _cursorPosOnPress) { + // We've recognized a gesture to open the menu, but we don't know + // whether the user tapped a control that was overlaid our input + // area, since we don't do any granular hit-testing in touchesBegan. + // To ensure that the gesture doesn't eat touch events that should + // have reached another UI control we report the gesture as failed + // here, and then manually show the menu at the next runloop pass. _menuShouldBeVisible = true; + self.state = UIGestureRecognizerStateFailed; + dispatch_async(dispatch_get_main_queue(), ^{ + QIOSTextInputOverlay::s_editMenu.visible = _menuShouldBeVisible; + }); } else { // The menu is hidden, and the cursor will change position once // Qt receive the touch release. We therefore fail so that we @@ -996,19 +1006,26 @@ QIOSTextInputOverlay::~QIOSTextInputOverlay() void QIOSTextInputOverlay::updateFocusObject() { + // Destroy old recognizers since they were created with + // dependencies to the old focus object (focus view). if (m_cursorRecognizer) { - // Destroy old recognizers since they were created with - // dependencies to the old focus object (focus view). m_cursorRecognizer.enabled = NO; - m_selectionRecognizer.enabled = NO; - m_openMenuOnTapRecognizer.enabled = NO; [m_cursorRecognizer release]; - [m_selectionRecognizer release]; - [m_openMenuOnTapRecognizer release]; - [s_editMenu release]; m_cursorRecognizer = nullptr; + } + if (m_selectionRecognizer) { + m_selectionRecognizer.enabled = NO; + [m_selectionRecognizer release]; m_selectionRecognizer = nullptr; + } + if (m_openMenuOnTapRecognizer) { + m_openMenuOnTapRecognizer.enabled = NO; + [m_openMenuOnTapRecognizer release]; m_openMenuOnTapRecognizer = nullptr; + } + + if (s_editMenu) { + [s_editMenu release]; s_editMenu = nullptr; } diff --git a/src/plugins/platforms/ios/qiostextresponder.mm b/src/plugins/platforms/ios/qiostextresponder.mm index 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..70e2c37ff1 100644 --- a/src/plugins/platforms/ios/qiostheme.h +++ b/src/plugins/platforms/ios/qiostheme.h @@ -4,6 +4,8 @@ #ifndef QIOSTHEME_H #define QIOSTHEME_H +#import <UIKit/UIKit.h> + #include <QtCore/QHash> #include <QtGui/QPalette> #include <qpa/qplatformtheme.h> @@ -22,21 +24,27 @@ public: QVariant themeHint(ThemeHint hint) const override; Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QPlatformMenuItem* createPlatformMenuItem() const override; QPlatformMenu* createPlatformMenu() const override; +#endif bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; static const char *name; static void initializeSystemPalette(); + static void applyTheme(UIWindow *window); private: static QPalette s_systemPalette; + static inline Qt::ColorScheme s_colorSchemeOverride = Qt::ColorScheme::Unknown; QMacNotificationObserver m_contentSizeCategoryObserver; }; diff --git a/src/plugins/platforms/ios/qiostheme.mm b/src/plugins/platforms/ios/qiostheme.mm index d1356ab8f7..0b420a875a 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,63 @@ 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 + if (s_colorSchemeOverride != Qt::ColorScheme::Unknown) + return s_colorSchemeOverride; + + // Set the appearance based on the QUIWindow // Fallback to the UIScreen if no window is created yet - 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 +} + +void QIOSTheme::requestColorScheme(Qt::ColorScheme scheme) +{ +#if defined(Q_OS_VISIONOS) + Q_UNUSED(scheme); +#else + s_colorSchemeOverride = scheme; + + const NSArray<UIWindow *> *windows = qt_apple_sharedApplication().windows; + for (UIWindow *window in windows) { + // don't apply a theme to windows we don't own + if (qt_objc_cast<QUIWindow*>(window)) + applyTheme(window); + } +#endif +} + +void QIOSTheme::applyTheme(UIWindow *window) +{ + const UIUserInterfaceStyle style = []{ + switch (s_colorSchemeOverride) { + case Qt::ColorScheme::Dark: + return UIUserInterfaceStyleDark; + case Qt::ColorScheme::Light: + return UIUserInterfaceStyleLight; + case Qt::ColorScheme::Unknown: + return UIUserInterfaceStyleUnspecified; + } + }(); + + window.overrideUserInterfaceStyle = style; } const QFont *QIOSTheme::font(Font type) const @@ -164,4 +213,9 @@ const QFont *QIOSTheme::font(Font type) const return coreTextFontDatabase->themeFont(type); } +QIconEngine *QIOSTheme::createIconEngine(const QString &iconName) const +{ + return new QAppleIconEngine(iconName); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosviewcontroller.h b/src/plugins/platforms/ios/qiosviewcontroller.h index 237cd57edf..1f8da41ba4 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.h +++ b/src/plugins/platforms/ios/qiosviewcontroller.h @@ -12,14 +12,12 @@ QT_END_NAMESPACE @interface QIOSViewController : UIViewController -- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen; +- (instancetype)initWithWindow:(UIWindow*)window andScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen; - (void)updateProperties; - (NSArray*)keyCommands; - (void)handleShortcut:(UIKeyCommand*)keyCommand; #ifndef Q_OS_TVOS -@property (nonatomic, assign) UIInterfaceOrientation lockedOrientation; - // UIViewController @property (nonatomic, assign) BOOL prefersStatusBarHidden; @property (nonatomic, assign) UIStatusBarAnimation preferredStatusBarUpdateAnimation; diff --git a/src/plugins/platforms/ios/qiosviewcontroller.mm b/src/plugins/platforms/ios/qiosviewcontroller.mm index c4e8968232..436d1e7bed 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.mm +++ b/src/plugins/platforms/ios/qiosviewcontroller.mm @@ -21,9 +21,12 @@ #include "qioswindow.h" #include "quiview.h" +#include <QtCore/qpointer.h> + // ------------------------------------------------------------------------- @interface QIOSViewController () +@property (nonatomic, assign) UIWindow *window; @property (nonatomic, assign) QPointer<QT_PREPEND_NAMESPACE(QIOSScreen)> platformScreen; @property (nonatomic, assign) BOOL changingOrientation; @end @@ -88,27 +91,30 @@ { Q_UNUSED(subview); - QT_PREPEND_NAMESPACE(QIOSScreen) *screen = self.qtViewController.platformScreen; - - // The 'window' property of our view is not valid until the window - // has been shown, so we have to access it through the QIOSScreen. - UIWindow *uiWindow = screen->uiWindow(); + // Track UIWindow via explicit property on QIOSViewController, + // as the window property of our own view is not valid until + // the window has been shown (below). + UIWindow *uiWindow = self.qtViewController.window; if (uiWindow.hidden) { - // Associate UIWindow to screen and show it the first time a QWindow - // is mapped to the screen. For external screens this means disabling - // mirroring mode and presenting alternate content on the screen. - uiWindow.screen = screen->uiScreen(); + // Show the UIWindow the first time a QWindow is mapped to the screen. + // For the main screen this hides the launch screen, while for external + // screens this disables mirroring of the main screen, so the external + // screen can be used for alternate content. uiWindow.hidden = NO; } } +#if !defined(Q_OS_VISIONOS) - (void)willRemoveSubview:(UIView *)subview { Q_UNUSED(subview); - Q_ASSERT(self.window); UIWindow *uiWindow = self.window; + // uiWindow can be null when closing from the ios "app manager" and the app is + // showing a native window like UIDocumentBrowserViewController + if (!uiWindow) + return; if (uiWindow.screen != [UIScreen mainScreen] && self.subviews.count == 1) { // We're about to remove the last view of an external screen, so go back @@ -116,10 +122,10 @@ // to ensure that we don't try to layout the view that's being removed. dispatch_async(dispatch_get_main_queue(), ^{ uiWindow.hidden = YES; - uiWindow.screen = [UIScreen mainScreen]; }); } } +#endif - (void)layoutSubviews { @@ -225,15 +231,14 @@ @synthesize preferredStatusBarStyle; #endif -- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen +- (instancetype)initWithWindow:(UIWindow*)window andScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen { if (self = [self init]) { + self.window = window; self.platformScreen = screen; self.changingOrientation = NO; #ifndef Q_OS_TVOS - self.lockedOrientation = UIInterfaceOrientationUnknown; - // Status bar may be initially hidden at startup through Info.plist self.prefersStatusBarHidden = infoPlistValue(@"UIStatusBarHidden", false); self.preferredStatusBarUpdateAnimation = UIStatusBarAnimationNone; @@ -280,7 +285,7 @@ Q_ASSERT(!qt_apple_isApplicationExtension()); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(willChangeStatusBarFrame:) name:UIApplicationWillChangeStatusBarFrameNotification @@ -290,6 +295,15 @@ name:UIApplicationDidChangeStatusBarOrientationNotification object:qt_apple_sharedApplication()]; #endif + + // Make sure any top level windows that have already been created + // for this screen are reparented into our desktop manager view. + for (auto *window : qGuiApp->topLevelWindows()) { + if (window->screen()->handle() != self.platformScreen) + continue; + if (auto *platformWindow = window->handle()) + platformWindow->setParent(nullptr); + } } - (void)viewDidUnload @@ -300,26 +314,6 @@ // ------------------------------------------------------------------------- -- (BOOL)shouldAutorotate -{ -#ifndef Q_OS_TVOS - return self.platformScreen && self.platformScreen->uiScreen() == [UIScreen mainScreen] && !self.lockedOrientation; -#else - return NO; -#endif -} - -- (NSUInteger)supportedInterfaceOrientations -{ - // As documented by Apple in the iOS 6.0 release notes, setStatusBarOrientation:animated: - // only works if the supportedInterfaceOrientations of the view controller is 0, making - // us responsible for ensuring that the status bar orientation is consistent. We enter - // this mode when auto-rotation is disabled due to an explicit content orientation being - // set on the focus window. Note that this is counter to what the documentation for - // supportedInterfaceOrientations says, which states that the method should not return 0. - return [self shouldAutorotate] ? UIInterfaceOrientationMaskAll : 0; -} - - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration { self.changingOrientation = YES; @@ -334,6 +328,7 @@ [super didRotateFromInterfaceOrientation:orientation]; } +#if !defined(Q_OS_VISIONOS) - (void)willChangeStatusBarFrame:(NSNotification*)notification { Q_UNUSED(notification); @@ -377,6 +372,7 @@ [self.view setNeedsLayout]; } +#endif - (void)viewWillLayoutSubviews { @@ -397,10 +393,12 @@ if (!self.platformScreen || !self.platformScreen->screen()) return; +#if !defined(Q_OS_VISIONOS) // For now we only care about the main screen, as both the statusbar // visibility and orientation is only appropriate for the main screen. if (self.platformScreen->uiScreen() != [UIScreen mainScreen]) return; +#endif // Prevent recursion caused by updating the status bar appearance (position // or visibility), which in turn may cause a layout of our subviews, and @@ -427,7 +425,7 @@ // All decisions are based on the top level window focusWindow = qt_window_private(focusWindow)->topLevelWindow(); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) // -------------- Status bar style and visbility --------------- @@ -447,51 +445,6 @@ [self setNeedsStatusBarAppearanceUpdate]; [self.view setNeedsLayout]; } - - - // -------------- Content orientation --------------- - - UIApplication *uiApplication = qt_apple_sharedApplication(); - - static BOOL kAnimateContentOrientationChanges = YES; - - Qt::ScreenOrientation contentOrientation = focusWindow->contentOrientation(); - if (contentOrientation != Qt::PrimaryOrientation) { - // An explicit content orientation has been reported for the focus window, - // so we keep the status bar in sync with content orientation. This will ensure - // that the task bar (and associated gestures) are also rotated accordingly. - - if (!self.lockedOrientation) { - // We are moving from Qt::PrimaryOrientation to an explicit orientation, - // so we need to store the current statusbar orientation, as we need it - // later when mapping screen coordinates for QScreen and for returning - // to Qt::PrimaryOrientation. - self.lockedOrientation = uiApplication.statusBarOrientation; - } - - [uiApplication setStatusBarOrientation: - UIInterfaceOrientation(fromQtScreenOrientation(contentOrientation)) - animated:kAnimateContentOrientationChanges]; - - } else { - // The content orientation is set to Qt::PrimaryOrientation, meaning - // that auto-rotation should be enabled. But we may be coming out of - // a state of locked orientation, which needs some cleanup before we - // can enable auto-rotation again. - if (self.lockedOrientation) { - // First we need to restore the statusbar to what it was at the - // time of locking the orientation, otherwise iOS will be very - // confused when it starts doing auto-rotation again. - [uiApplication setStatusBarOrientation:self.lockedOrientation - animated:kAnimateContentOrientationChanges]; - - // Then we can re-enable auto-rotation - self.lockedOrientation = UIInterfaceOrientationUnknown; - - // And finally let iOS rotate the root view to match the device orientation - [UIViewController attemptRotationToDeviceOrientation]; - } - } #endif } diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 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..f461a5f55b 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,50 @@ QT_BEGIN_NAMESPACE -QIOSWindow::QIOSWindow(QWindow *window) +enum { + defaultWindowWidth = 160, + defaultWindowHeight = 160 +}; + +QIOSWindow::QIOSWindow(QWindow *window, WId nativeHandle) : QPlatformWindow(window) , m_windowLevel(0) { -#ifdef Q_OS_IOS - if (window->surfaceType() == QSurface::RasterSurface) - window->setSurfaceType(QSurface::MetalSurface); + if (nativeHandle) { + m_view = reinterpret_cast<UIView *>(nativeHandle); + [m_view retain]; + } else { +#if QT_CONFIG(metal) + if (window->surfaceType() == QSurface::RasterSurface) + window->setSurfaceType(QSurface::MetalSurface); - if (window->surfaceType() == QSurface::MetalSurface) - m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; - else + if (window->surfaceType() == QSurface::MetalSurface) + m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; + else #endif - m_view = [[QUIView alloc] initWithQIOSWindow:this]; + m_view = [[QUIView alloc] initWithQIOSWindow:this]; + } connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QIOSWindow::applicationStateChanged); + // Always set parent, even if we don't have a parent window, + // as we use setParent to reparent top levels into our desktop + // manager view. setParent(QPlatformWindow::parent()); - // Resolve default window geometry in case it was not set before creating the - // platform window. This picks up eg. minimum-size if set, and defaults to - // the "maxmized" geometry (even though we're not in that window state). - // FIXME: Detect if we apply a maximized geometry and send a window state - // change event in that case. - m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), - screen()->availableGeometry().width(), screen()->availableGeometry().height()); + if (!isForeignWindow()) { + // Resolve default window geometry in case it was not set before creating the + // platform window. This picks up eg. minimum-size if set. + m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), + defaultWindowWidth, defaultWindowHeight); - setWindowState(window->windowStates()); - setOpacity(window->opacity()); + setWindowState(window->windowStates()); + setOpacity(window->opacity()); + setMask(QHighDpi::toNativeLocalRegion(window->mask(), window)); + } else { + // Pick up essential foreign window state + QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect()); + } Qt::ScreenOrientation initialOrientation = window->contentOrientation(); if (initialOrientation != Qt::PrimaryOrientation) { @@ -75,8 +94,15 @@ QIOSWindow::~QIOSWindow() [m_view touchesCancelled:[NSSet set] withEvent:0]; clearAccessibleCache(); - m_view.platformWindow = 0; - [m_view removeFromSuperview]; + + quiview_cast(m_view).platformWindow = nullptr; + + // Remove from superview, unless we're a foreign window without a + // Qt window parent, in which case the foreign window is used as + // a window container for a Qt UI hierarchy inside a native UI. + if (!(isForeignWindow() && !QPlatformWindow::parent())) + [m_view removeFromSuperview]; + [m_view release]; } @@ -115,7 +141,7 @@ void QIOSWindow::setVisible(bool visible) if (visible && shouldAutoActivateWindow()) { if (!window()->property("_q_showWithoutActivating").toBool()) requestActivateWindow(); - } else if (!visible && [m_view isActiveWindow]) { + } else if (!visible && [quiview_cast(m_view) isActiveWindow]) { // Our window was active/focus window but now hidden, so relinquish // focus to the next possible window in the stack. NSArray<UIView *> *subviews = m_view.viewController.view.subviews; @@ -204,7 +230,7 @@ void QIOSWindow::applyGeometry(const QRect &rect) QMargins QIOSWindow::safeAreaMargins() const { - UIEdgeInsets safeAreaInsets = m_view.qt_safeAreaInsets; + UIEdgeInsets safeAreaInsets = m_view.safeAreaInsets; return QMargins(safeAreaInsets.left, safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom); } @@ -228,22 +254,45 @@ void QIOSWindow::setWindowState(Qt::WindowStates state) if (state & Qt::WindowMinimized) { applyGeometry(QRect()); } else if (state & (Qt::WindowFullScreen | Qt::WindowMaximized)) { - // When an application is in split-view mode, the UIScreen still has the - // same geometry, but the UIWindow is resized to the area reserved for the - // application. We use this to constrain the geometry used when applying the - // fullscreen or maximized window states. Note that we do not do this - // in applyGeometry(), as we don't want to artificially limit window - // placement "outside" of the screen bounds if that's what the user wants. - QRect uiWindowBounds = QRectF::fromCGRect(m_view.window.bounds).toRect(); - QRect fullscreenGeometry = screen()->geometry().intersected(uiWindowBounds); - QRect maximizedGeometry = window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint ? - fullscreenGeometry : screen()->availableGeometry().intersected(uiWindowBounds); + if (NSProcessInfo.processInfo.iOSAppOnMac) { + // iOS apps running as "Designed for iPad" on macOS do not match + // our current window management implementation where a single + // UIWindow is tied to a single screen. And even if we're on the + // right screen, the UIScreen does not account for the 77% scale + // of the UIUserInterfaceIdiomPad environment, so we can't use + // it to clamp the window geometry. Instead just use the UIWindow + // directly, which represents our "screen". + applyGeometry(uiWindowBounds); + } else if (isRunningOnVisionOS()) { + // On visionOS there is no concept of a screen, and hence no concept of + // screen-relative system UI that we should keep top level windows away + // from, so don't apply the UIWindow safe area insets to the screen. + applyGeometry(uiWindowBounds); + } else { + QRect fullscreenGeometry = screen()->geometry(); + QRect maximizedGeometry = fullscreenGeometry; + +#if !defined(Q_OS_VISIONOS) + if (!(window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint)) { + // If the safe area margins reflect the screen's outer edges, + // then reduce the maximized geometry accordingly. Otherwise + // leave it as is, and assume the client will take the safe + // are margins into account explicitly. + UIScreen *uiScreen = m_view.window.windowScene.screen; + UIEdgeInsets safeAreaInsets = m_view.window.safeAreaInsets; + if (m_view.window.bounds.size.width == uiScreen.bounds.size.width) + maximizedGeometry.adjust(safeAreaInsets.left, 0, -safeAreaInsets.right, 0); + if (m_view.window.bounds.size.height == uiScreen.bounds.size.height) + maximizedGeometry.adjust(0, safeAreaInsets.top, 0, -safeAreaInsets.bottom); + } +#endif - if (state & Qt::WindowFullScreen) - applyGeometry(fullscreenGeometry); - else - applyGeometry(maximizedGeometry); + if (state & Qt::WindowFullScreen) + applyGeometry(fullscreenGeometry.intersected(uiWindowBounds)); + else + applyGeometry(maximizedGeometry.intersected(uiWindowBounds)); + } } else { applyGeometry(m_normalGeometry); } @@ -251,10 +300,16 @@ void QIOSWindow::setWindowState(Qt::WindowStates state) void QIOSWindow::setParent(const QPlatformWindow *parentWindow) { - UIView *parentView = parentWindow ? reinterpret_cast<UIView *>(parentWindow->winId()) - : isQtApplication() ? static_cast<QIOSScreen *>(screen())->uiWindow().rootViewController.view : 0; - - [parentView addSubview:m_view]; + UIView *superview = nullptr; + if (parentWindow) + superview = reinterpret_cast<UIView *>(parentWindow->winId()); + else if (isQtApplication() && !isForeignWindow()) + superview = rootViewForScreen(window()->screen()); + + if (superview) + [superview addSubview:m_view]; + else if (quiview_cast(m_view.superview)) + [m_view removeFromSuperview]; } void QIOSWindow::requestActivateWindow() @@ -265,7 +320,6 @@ void QIOSWindow::requestActivateWindow() if (blockedByModal()) return; - Q_ASSERT(m_view.window); [m_view.window makeKeyWindow]; [m_view becomeFirstResponder]; @@ -275,8 +329,6 @@ void QIOSWindow::requestActivateWindow() void QIOSWindow::raiseOrLower(bool raise) { - // Re-insert m_view at the correct index among its sibling views - // (QWindows) according to their current m_windowLevel: if (!isQtApplication()) return; @@ -284,17 +336,27 @@ void QIOSWindow::raiseOrLower(bool raise) if (subviews.count == 1) return; - for (int i = int(subviews.count) - 1; i >= 0; --i) { - UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); - if (view.hidden || view == m_view || !view.qwindow) - continue; - int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; - if (m_windowLevel > level || (raise && m_windowLevel == level)) { - [m_view.superview insertSubview:m_view aboveSubview:view]; - return; + if (m_view.superview == m_view.qtViewController.view) { + // We're a top level window, so we need to take window + // levels into account. + for (int i = int(subviews.count) - 1; i >= 0; --i) { + UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); + if (view.hidden || view == m_view || !view.qwindow) + continue; + int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; + if (m_windowLevel > level || (raise && m_windowLevel == level)) { + [m_view.superview insertSubview:m_view aboveSubview:view]; + return; + } } + [m_view.superview insertSubview:m_view atIndex:0]; + } else { + // Child window, or embedded into a non-Qt view controller + if (raise) + [m_view.superview bringSubviewToFront:m_view]; + else + [m_view.superview sendSubviewToBack:m_view]; } - [m_view.superview insertSubview:m_view atIndex:0]; } void QIOSWindow::updateWindowLevel() @@ -323,20 +385,13 @@ void QIOSWindow::updateWindowLevel() m_windowLevel = qMax(transientParentWindow->m_windowLevel, m_windowLevel); } -void QIOSWindow::handleContentOrientationChange(Qt::ScreenOrientation orientation) -{ - // Update the QWindow representation straight away, so that - // we can update the statusbar orientation based on the new - // content orientation. - qt_window_private(window())->contentOrientation = orientation; - - [m_view.qtViewController updateProperties]; -} - void QIOSWindow::applicationStateChanged(Qt::ApplicationState) { + if (isForeignWindow()) + return; + if (window()->isExposed() != isExposed()) - [m_view sendUpdatedExposeEvent]; + [quiview_cast(m_view) sendUpdatedExposeEvent]; } qreal QIOSWindow::devicePixelRatio() const @@ -346,7 +401,10 @@ qreal QIOSWindow::devicePixelRatio() const void QIOSWindow::clearAccessibleCache() { - [m_view clearAccessibleCache]; + if (isForeignWindow()) + return; + + [quiview_cast(m_view) clearAccessibleCache]; } void QIOSWindow::requestUpdate() @@ -354,6 +412,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 +447,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..783e243e10 --- /dev/null +++ b/src/plugins/platforms/ios/quiwindow.mm @@ -0,0 +1,65 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "quiwindow.h" + +#include "qiostheme.h" + +#include <QtCore/qscopedvaluerollback.h> + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> + +#include <UIKit/UIKit.h> + +@implementation QUIWindow + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) + self->_sendingEvent = NO; + + return self; +} + +- (instancetype)initWithWindowScene:(UIWindowScene *)windowScene +{ + if ((self = [super initWithWindowScene:windowScene])) + self->_sendingEvent = NO; + + QIOSTheme::applyTheme(self); + return self; +} + +- (void)sendEvent:(UIEvent *)event +{ + QScopedValueRollback<BOOL> sendingEvent(self->_sendingEvent, YES); + [super sendEvent:event]; +} + +#if !defined(Q_OS_VISIONOS) +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection +{ + [super traitCollectionDidChange:previousTraitCollection]; + + if (!qGuiApp) + return; + + Qt::ColorScheme colorScheme = self.traitCollection.userInterfaceStyle + == UIUserInterfaceStyleDark + ? Qt::ColorScheme::Dark + : Qt::ColorScheme::Light; + + if (self.screen == UIScreen.mainScreen) { + // Check if the current userInterfaceStyle reports a different appearance than + // the platformTheme's appearance. We might have set that one based on the UIScreen + if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle + || QGuiApplicationPrivate::platformTheme()->colorScheme() != colorScheme) { + QIOSTheme::initializeSystemPalette(); + QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); + } + } +} +#endif + +@end diff --git a/src/plugins/platforms/linuxfb/CMakeLists.txt b/src/plugins/platforms/linuxfb/CMakeLists.txt index 9f75f53828..ba18cea50c 100644 --- a/src/plugins/platforms/linuxfb/CMakeLists.txt +++ b/src/plugins/platforms/linuxfb/CMakeLists.txt @@ -8,7 +8,7 @@ qt_internal_add_plugin(QLinuxFbIntegrationPlugin OUTPUT_NAME qlinuxfb PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES linuxfb + DEFAULT_IF "linuxfb" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qlinuxfbintegration.cpp qlinuxfbintegration.h diff --git a/src/plugins/platforms/minimal/CMakeLists.txt b/src/plugins/platforms/minimal/CMakeLists.txt index f3683deccf..18d8828134 100644 --- a/src/plugins/platforms/minimal/CMakeLists.txt +++ b/src/plugins/platforms/minimal/CMakeLists.txt @@ -10,7 +10,7 @@ qt_find_package(WrapFreetype PROVIDED_TARGETS WrapFreetype::WrapFreetype) qt_internal_add_plugin(QMinimalIntegrationPlugin OUTPUT_NAME qminimal PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES minimal + DEFAULT_IF "minimal" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qminimalbackingstore.cpp qminimalbackingstore.h diff --git a/src/plugins/platforms/minimalegl/CMakeLists.txt b/src/plugins/platforms/minimalegl/CMakeLists.txt index a6ec8be781..b93f325b8f 100644 --- a/src/plugins/platforms/minimalegl/CMakeLists.txt +++ b/src/plugins/platforms/minimalegl/CMakeLists.txt @@ -10,7 +10,7 @@ qt_find_package(EGL) qt_internal_add_plugin(QMinimalEglIntegrationPlugin OUTPUT_NAME qminimalegl PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES minimalegl + DEFAULT_IF "minimalegl" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qminimaleglintegration.cpp qminimaleglintegration.h diff --git a/src/plugins/platforms/offscreen/CMakeLists.txt b/src/plugins/platforms/offscreen/CMakeLists.txt index 09ad9a384d..907c2c9cc6 100644 --- a/src/plugins/platforms/offscreen/CMakeLists.txt +++ b/src/plugins/platforms/offscreen/CMakeLists.txt @@ -8,7 +8,7 @@ qt_internal_add_plugin(QOffscreenIntegrationPlugin OUTPUT_NAME qoffscreen PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES offscreen + DEFAULT_IF "offscreen" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qoffscreencommon.cpp qoffscreencommon.h 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..0f9deaa00b 100644 --- a/src/plugins/platforms/qnx/CMakeLists.txt +++ b/src/plugins/platforms/qnx/CMakeLists.txt @@ -8,7 +8,7 @@ qt_internal_add_plugin(QQnxIntegrationPlugin OUTPUT_NAME qqnx PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES qnx + DEFAULT_IF "qnx" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp main.h qqnxabstractcover.h @@ -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..b308c956f2 100644 --- a/src/plugins/platforms/qnx/qqnxintegration.cpp +++ b/src/plugins/platforms/qnx/qqnxintegration.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2013 BlackBerry Limited. All rights reserved. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qqnxglobal.h" #include "qqnxintegration.h" @@ -47,6 +49,7 @@ #include <qpa/qwindowsysteminterface.h> #include <QtGui/private/qguiapplication_p.h> +#include <QtGui/private/qrhibackingstore_p.h> #if !defined(QT_NO_OPENGL) #include "qqnxglcontext.h" @@ -62,14 +65,10 @@ #include <QtCore/QFile> #include <errno.h> -#if defined(QQNXINTEGRATION_DEBUG) -#define qIntegrationDebug qDebug -#else -#define qIntegrationDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE +// Q_LOGGING_CATEGORY(lcQpaQnx, "qt.qpa.qnx"); + using namespace Qt::StringLiterals; QQnxIntegration *QQnxIntegration::ms_instance; @@ -98,7 +97,7 @@ static inline QQnxIntegration::Options parseOptions(const QStringList ¶mList static inline int getContextCapabilities(const QStringList ¶mList) { - QString contextCapabilitiesPrefix = QStringLiteral("screen-context-capabilities="); + constexpr auto contextCapabilitiesPrefix = "screen-context-capabilities="_L1; int contextCapabilities = SCREEN_APPLICATION_CONTEXT; for (const QString ¶m : paramList) { if (param.startsWith(contextCapabilitiesPrefix)) { @@ -142,7 +141,7 @@ QQnxIntegration::QQnxIntegration(const QStringList ¶mList) { ms_instance = this; m_options = parseOptions(paramList); - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Open connection to QNX composition manager if (screen_create_context(&m_screenContext, getContextCapabilities(paramList))) { @@ -219,7 +218,7 @@ QQnxIntegration::QQnxIntegration(const QStringList ¶mList) QQnxIntegration::~QQnxIntegration() { - qIntegrationDebug("platform plugin shutdown begin"); + qCDebug(lcQpaQnx) << "Platform plugin shutdown begin"; delete m_nativeInterface; #if QT_CONFIG(draganddrop) @@ -276,12 +275,12 @@ QQnxIntegration::~QQnxIntegration() ms_instance = nullptr; - qIntegrationDebug("platform plugin shutdown end"); + qCDebug(lcQpaQnx) << "Platform plugin shutdown end"; } bool QQnxIntegration::hasCapability(QPlatformIntegration::Capability cap) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; switch (cap) { case MultipleWindows: case ForeignWindows: @@ -312,7 +311,7 @@ QPlatformWindow *QQnxIntegration::createForeignWindow(QWindow *window, WId nativ QPlatformWindow *QQnxIntegration::createPlatformWindow(QWindow *window) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QSurface::SurfaceType surfaceType = window->surfaceType(); const bool needRootWindow = options() & RootWindow; switch (surfaceType) { @@ -330,14 +329,25 @@ QPlatformWindow *QQnxIntegration::createPlatformWindow(QWindow *window) const QPlatformBackingStore *QQnxIntegration::createPlatformBackingStore(QWindow *window) const { - qIntegrationDebug(); - return new QQnxRasterBackingStore(window); + QSurface::SurfaceType surfaceType = window->surfaceType(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO << surfaceType; + switch (surfaceType) { + case QSurface::RasterSurface: + return new QQnxRasterBackingStore(window); +#if !defined(QT_NO_OPENGL) + // Return a QRhiBackingStore for non-raster surface windows + case QSurface::OpenGLSurface: + return new QRhiBackingStore(window); +#endif + default: + return nullptr; + } } #if !defined(QT_NO_OPENGL) QPlatformOpenGLContext *QQnxIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Get color channel sizes from window format QSurfaceFormat format = context->format(); @@ -396,7 +406,7 @@ QPlatformOpenGLContext *QQnxIntegration::createPlatformOpenGLContext(QOpenGLCont #if QT_CONFIG(qqnx_pps) QPlatformInputContext *QQnxIntegration::inputContext() const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; if (m_qpaInputContext) return m_qpaInputContext; return m_inputContext; @@ -405,7 +415,7 @@ QPlatformInputContext *QQnxIntegration::inputContext() const void QQnxIntegration::moveToScreen(QWindow *window, int screen) { - qIntegrationDebug() << "w =" << window << ", s =" << screen; + qCDebug(lcQpaQnx) << Q_FUNC_INFO << "w =" << window << ", s =" << screen; // get platform window used by widget QQnxWindow *platformWindow = static_cast<QQnxWindow *>(window->handle()); @@ -419,7 +429,7 @@ void QQnxIntegration::moveToScreen(QWindow *window, int screen) QAbstractEventDispatcher *QQnxIntegration::createEventDispatcher() const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // We transfer ownersip of the event-dispatcher to QtCoreApplication QAbstractEventDispatcher *eventDispatcher = m_eventDispatcher; @@ -436,7 +446,7 @@ QPlatformNativeInterface *QQnxIntegration::nativeInterface() const #if !defined(QT_NO_CLIPBOARD) QPlatformClipboard *QQnxIntegration::clipboard() const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; #if QT_CONFIG(qqnx_pps) if (!m_clipboard) @@ -455,7 +465,7 @@ QPlatformDrag *QQnxIntegration::drag() const QVariant QQnxIntegration::styleHint(QPlatformIntegration::StyleHint hint) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; if ((hint == ShowIsFullScreen) && (m_options & FullScreenApplication)) return true; @@ -469,7 +479,7 @@ QPlatformServices * QQnxIntegration::services() const QWindow *QQnxIntegration::window(screen_window_t qnxWindow) const { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QMutexLocker locker(&m_windowMapperMutex); Q_UNUSED(locker); return m_windowMapper.value(qnxWindow, 0); @@ -477,7 +487,7 @@ QWindow *QQnxIntegration::window(screen_window_t qnxWindow) const void QQnxIntegration::addWindow(screen_window_t qnxWindow, QWindow *window) { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QMutexLocker locker(&m_windowMapperMutex); Q_UNUSED(locker); m_windowMapper.insert(qnxWindow, window); @@ -485,7 +495,7 @@ void QQnxIntegration::addWindow(screen_window_t qnxWindow, QWindow *window) void QQnxIntegration::removeWindow(screen_window_t qnxWindow) { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; QMutexLocker locker(&m_windowMapperMutex); Q_UNUSED(locker); m_windowMapper.remove(qnxWindow); @@ -594,7 +604,7 @@ QList<screen_display_t *> QQnxIntegration::sortDisplays(screen_display_t *availa void QQnxIntegration::createDisplays() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Query number of displays int displayCount = 0; int result = screen_get_context_property_iv(m_screenContext, SCREEN_PROPERTY_DISPLAY_COUNT, @@ -624,11 +634,12 @@ void QQnxIntegration::createDisplays() Q_SCREEN_CHECKERROR(result, "Failed to query display attachment"); if (!isAttached) { - qIntegrationDebug("Skipping non-attached display %d", i); + qCDebug(lcQpaQnx) << "Skipping non-attached display " << i; continue; } - qIntegrationDebug("Creating screen for display %d", i); + qCDebug(lcQpaQnx) << "Creating screen for display " << i; + createDisplay(*orderedDisplays[i], /*isPrimary=*/false); } // of displays iteration } @@ -662,7 +673,8 @@ void QQnxIntegration::removeDisplay(QQnxScreen *screen) void QQnxIntegration::destroyDisplays() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; + Q_FOREACH (QQnxScreen *screen, m_screens) { QWindowSystemInterface::handleScreenRemoved(screen); } @@ -713,7 +725,7 @@ bool QQnxIntegration::supportsNavigatorEvents() const #if QT_CONFIG(opengl) void QQnxIntegration::createEglDisplay() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Initialize connection to EGL m_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); @@ -727,7 +739,7 @@ void QQnxIntegration::createEglDisplay() void QQnxIntegration::destroyEglDisplay() { - qIntegrationDebug(); + qCDebug(lcQpaQnx) << Q_FUNC_INFO; // Close connection to EGL eglTerminate(m_eglDisplay); diff --git a/src/plugins/platforms/qnx/qqnxintegration.h b/src/plugins/platforms/qnx/qqnxintegration.h index 590b9450c2..8a78d54ceb 100644 --- a/src/plugins/platforms/qnx/qqnxintegration.h +++ b/src/plugins/platforms/qnx/qqnxintegration.h @@ -6,9 +6,11 @@ #include <qpa/qplatformintegration.h> #include <private/qtguiglobal_p.h> +#include <QtCore/qhash.h> #include <QtCore/qmutex.h> #include <screen/screen.h> +#include <QtCore/QLoggingCategory> #if QT_CONFIG(opengl) #include <EGL/egl.h> @@ -16,6 +18,9 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnx); +Q_DECLARE_LOGGING_CATEGORY(lcQpaGLContext); + class QQnxScreenEventThread; class QQnxFileDialogHelper; class QQnxNativeInterface; @@ -39,8 +44,7 @@ class QQnxButtonEventNotifier; class QQnxClipboard; #endif -template<class K, class V> class QHash; -typedef QHash<screen_window_t, QWindow *> QQnxWindowMapper; +using QQnxWindowMapper = QHash<screen_window_t, QWindow *>; class QQnxIntegration : public QPlatformIntegration { diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp index cee86a68a0..7580e560aa 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.cpp @@ -10,11 +10,7 @@ #include <QGuiApplication> #include <qpa/qwindowsysteminterface.h> -#if defined(QQNXNAVIGATOREVENTHANDLER_DEBUG) -#define qNavigatorEventHandlerDebug qDebug -#else -#define qNavigatorEventHandlerDebug QT_NO_QDEBUG_MACRO -#endif +Q_LOGGING_CATEGORY(lcQpaQnxNavigatorEvents, "qt.qpa.qnx.navigator.events"); QT_BEGIN_NAMESPACE @@ -27,20 +23,20 @@ bool QQnxNavigatorEventHandler::handleOrientationCheck(int angle) { // reply to navigator that (any) orientation is acceptable // TODO: check if top window flags prohibit orientation change - qNavigatorEventHandlerDebug("angle=%d", angle); + qCDebug(lcQpaQnxNavigatorEvents, "angle=%d", angle); return true; } void QQnxNavigatorEventHandler::handleOrientationChange(int angle) { // update screen geometry and reply to navigator that we're ready - qNavigatorEventHandlerDebug("angle=%d", angle); + qCDebug(lcQpaQnxNavigatorEvents, "angle=%d", angle); emit rotationChanged(angle); } void QQnxNavigatorEventHandler::handleSwipeDown() { - qNavigatorEventHandlerDebug(); + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO; Q_EMIT swipeDown(); } @@ -48,25 +44,25 @@ void QQnxNavigatorEventHandler::handleSwipeDown() void QQnxNavigatorEventHandler::handleExit() { // shutdown everything - qNavigatorEventHandlerDebug(); + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO; QCoreApplication::quit(); } void QQnxNavigatorEventHandler::handleWindowGroupActivated(const QByteArray &id) { - qNavigatorEventHandlerDebug() << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << id; Q_EMIT windowGroupActivated(id); } void QQnxNavigatorEventHandler::handleWindowGroupDeactivated(const QByteArray &id) { - qNavigatorEventHandlerDebug() << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << id; Q_EMIT windowGroupDeactivated(id); } void QQnxNavigatorEventHandler::handleWindowGroupStateChanged(const QByteArray &id, Qt::WindowState state) { - qNavigatorEventHandlerDebug() << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << id; Q_EMIT windowGroupStateChanged(id, state); } diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h index 342b6c3ef6..826c9a0ff9 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventhandler.h @@ -5,9 +5,12 @@ #define QQNXNAVIGATOREVENTHANDLER_H #include <QObject> +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnxNavigatorEvents); + class QQnxNavigatorEventHandler : public QObject { Q_OBJECT diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp index 5d099b7e46..44c71dad52 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.cpp @@ -17,16 +17,12 @@ #include <sys/types.h> #include <sys/stat.h> -#if defined(QQNXNAVIGATOREVENTNOTIFIER_DEBUG) -#define qNavigatorEventNotifierDebug qDebug -#else -#define qNavigatorEventNotifierDebug QT_NO_QDEBUG_MACRO -#endif +QT_BEGIN_NAMESPACE -static const char *navigatorControlPath = "/pps/services/navigator/control"; -static const int ppsBufferSize = 4096; +// Q_LOGGING_CATEGORY(lcQpaQnxNavigatorEvents, "qt.qpa.qnx.navigator.events"); -QT_BEGIN_NAMESPACE +const char *QQnxNavigatorEventNotifier::navigatorControlPath = "/pps/services/navigator/control"; +const size_t QQnxNavigatorEventNotifier::ppsBufferSize = 4096; QQnxNavigatorEventNotifier::QQnxNavigatorEventNotifier(QQnxNavigatorEventHandler *eventHandler, QObject *parent) : QObject(parent), @@ -44,18 +40,18 @@ QQnxNavigatorEventNotifier::~QQnxNavigatorEventNotifier() if (m_fd != -1) close(m_fd); - qNavigatorEventNotifierDebug("navigator event notifier stopped"); + qCDebug(lcQpaQnxNavigatorEvents) << "Navigator event notifier stopped"; } void QQnxNavigatorEventNotifier::start() { - qNavigatorEventNotifierDebug("navigator event notifier started"); + qCDebug(lcQpaQnxNavigatorEvents) << "Navigator event notifier started"; // open connection to navigator errno = 0; m_fd = open(navigatorControlPath, O_RDWR); if (m_fd == -1) { - qNavigatorEventNotifierDebug("failed to open navigator pps: %s", strerror(errno)); + qCDebug(lcQpaQnxNavigatorEvents, "Failed to open navigator pps: %s", strerror(errno)); return; } @@ -65,7 +61,7 @@ void QQnxNavigatorEventNotifier::start() void QQnxNavigatorEventNotifier::parsePPS(const QByteArray &ppsData, QByteArray &msg, QByteArray &dat, QByteArray &id) { - qNavigatorEventNotifierDebug() << "data=" << ppsData; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "data=" << ppsData; // tokenize pps data into lines QList<QByteArray> lines = ppsData.split('\n'); @@ -79,7 +75,7 @@ void QQnxNavigatorEventNotifier::parsePPS(const QByteArray &ppsData, QByteArray // tokenize current attribute const QByteArray &attr = lines.at(i); - qNavigatorEventNotifierDebug() << "attr=" << attr; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "attr=" << attr; int firstColon = attr.indexOf(':'); if (firstColon == -1) { @@ -96,8 +92,7 @@ void QQnxNavigatorEventNotifier::parsePPS(const QByteArray &ppsData, QByteArray QByteArray key = attr.left(firstColon); QByteArray value = attr.mid(secondColon + 1); - qNavigatorEventNotifierDebug() << "key=" << key; - qNavigatorEventNotifierDebug() << "val=" << value; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "key =" << key << "value =" << value; // save attribute value if (key == "msg") @@ -124,7 +119,7 @@ void QQnxNavigatorEventNotifier::replyPPS(const QByteArray &res, const QByteArra } ppsData += "\n"; - qNavigatorEventNotifierDebug() << "reply=" << ppsData; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "reply=" << ppsData; // send pps message to navigator errno = 0; @@ -135,7 +130,7 @@ void QQnxNavigatorEventNotifier::replyPPS(const QByteArray &res, const QByteArra void QQnxNavigatorEventNotifier::handleMessage(const QByteArray &msg, const QByteArray &dat, const QByteArray &id) { - qNavigatorEventNotifierDebug() << "msg=" << msg << ", dat=" << dat << ", id=" << id; + qCDebug(lcQpaQnxNavigatorEvents) << Q_FUNC_INFO << "msg=" << msg << ", dat=" << dat << ", id=" << id; // check message type if (msg == "orientationCheck") { @@ -159,7 +154,7 @@ void QQnxNavigatorEventNotifier::handleMessage(const QByteArray &msg, const QByt void QQnxNavigatorEventNotifier::readData() { - qNavigatorEventNotifierDebug("reading navigator data"); + qCDebug(lcQpaQnxNavigatorEvents) << "Reading navigator data"; // allocate buffer for pps data char buffer[ppsBufferSize]; diff --git a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h index 6ecf776f36..66100ece3f 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h +++ b/src/plugins/platforms/qnx/qqnxnavigatoreventnotifier.h @@ -32,6 +32,9 @@ private: int m_fd; QSocketNotifier *m_readNotifier; QQnxNavigatorEventHandler *m_eventHandler; + + static const char *navigatorControlPath; + static const size_t ppsBufferSize; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp b/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp index 9ca402822d..3a2fee0afb 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp +++ b/src/plugins/platforms/qnx/qqnxnavigatorpps.cpp @@ -8,17 +8,11 @@ #include <QByteArray> #include <private/qcore_unix_p.h> -#if defined(QQNXNAVIGATOR_DEBUG) -#define qNavigatorDebug qDebug -#else -#define qNavigatorDebug QT_NO_QDEBUG_MACRO -#endif - -static const char *navigatorControlPath = "/pps/services/navigator/control"; -static const int ppsBufferSize = 4096; - QT_BEGIN_NAMESPACE +const char *QQnxNavigatorPps::navigatorControlPath = "/pps/services/navigator/control"; +const size_t QQnxNavigatorPps::ppsBufferSize = 4096; + QQnxNavigatorPps::QQnxNavigatorPps(QObject *parent) : QQnxAbstractNavigator(parent) , m_fd(-1) @@ -45,7 +39,7 @@ bool QQnxNavigatorPps::openPpsConnection() return false; } - qNavigatorDebug("successfully connected to Navigator. fd=%d", m_fd); + qCDebug(lcQpaQnxNavigator) << "successfully connected to Navigator. fd=" << m_fd; return true; } @@ -67,7 +61,7 @@ bool QQnxNavigatorPps::sendPpsMessage(const QByteArray &message, const QByteArra ppsMessage += "\n"; - qNavigatorDebug() << "sending PPS message:\n" << ppsMessage; + qCDebug(lcQpaQnxNavigator) << "sending PPS message:\n" << ppsMessage; // send pps message to navigator errno = 0; @@ -89,7 +83,7 @@ bool QQnxNavigatorPps::sendPpsMessage(const QByteArray &message, const QByteArra // ensure data is null terminated buffer[bytes] = '\0'; - qNavigatorDebug() << "received PPS message:\n" << buffer; + qCDebug(lcQpaQnxNavigator) << "received PPS message:\n" << buffer; // process received message QByteArray ppsData(buffer); @@ -108,7 +102,7 @@ bool QQnxNavigatorPps::sendPpsMessage(const QByteArray &message, const QByteArra void QQnxNavigatorPps::parsePPS(const QByteArray &ppsData, QHash<QByteArray, QByteArray> &messageFields) { - qNavigatorDebug() << "data=" << ppsData; + qCDebug(lcQpaQnxNavigator) << "data=" << ppsData; // tokenize pps data into lines QList<QByteArray> lines = ppsData.split('\n'); @@ -123,7 +117,7 @@ void QQnxNavigatorPps::parsePPS(const QByteArray &ppsData, QHash<QByteArray, QBy // tokenize current attribute const QByteArray &attr = lines.at(i); - qNavigatorDebug() << "attr=" << attr; + qCDebug(lcQpaQnxNavigator) << "attr=" << attr; int firstColon = attr.indexOf(':'); if (firstColon == -1) { @@ -140,8 +134,7 @@ void QQnxNavigatorPps::parsePPS(const QByteArray &ppsData, QHash<QByteArray, QBy QByteArray key = attr.left(firstColon); QByteArray value = attr.mid(secondColon + 1); - qNavigatorDebug() << "key=" << key; - qNavigatorDebug() << "val=" << value; + qCDebug(lcQpaQnxNavigator) << "key=" << key << "value=" << value; messageFields[key] = value; } } diff --git a/src/plugins/platforms/qnx/qqnxnavigatorpps.h b/src/plugins/platforms/qnx/qqnxnavigatorpps.h index 7f23097bc9..889b9f62eb 100644 --- a/src/plugins/platforms/qnx/qqnxnavigatorpps.h +++ b/src/plugins/platforms/qnx/qqnxnavigatorpps.h @@ -5,9 +5,12 @@ #define QQNXNAVIGATORPPS_H #include "qqnxabstractnavigator.h" +#include <QtCore/QLoggingCategory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaQnxNavigator); + template <typename K, typename V> class QHash; class QQnxNavigatorPps : public QQnxAbstractNavigator @@ -26,8 +29,9 @@ private: bool sendPpsMessage(const QByteArray &message, const QByteArray &data); void parsePPS(const QByteArray &ppsData, QHash<QByteArray, QByteArray> &messageFields); -private: int m_fd; + static const char *navigatorControlPath; + static const size_t ppsBufferSize; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp b/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp index cf80e44f84..b94c056a79 100644 --- a/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp +++ b/src/plugins/platforms/qnx/qqnxrasterbackingstore.cpp @@ -10,12 +10,6 @@ #include <errno.h> -#if defined(QQNXRASTERBACKINGSTORE_DEBUG) -#define qRasterBackingStoreDebug qDebug -#else -#define qRasterBackingStoreDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE QQnxRasterBackingStore::QQnxRasterBackingStore(QWindow *window) @@ -23,14 +17,14 @@ QQnxRasterBackingStore::QQnxRasterBackingStore(QWindow *window) m_needsPosting(false), m_scrolled(false) { - qRasterBackingStoreDebug() << "w =" << window; + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window; m_window = window; } QQnxRasterBackingStore::~QQnxRasterBackingStore() { - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); } QPaintDevice *QQnxRasterBackingStore::paintDevice() @@ -45,7 +39,7 @@ void QQnxRasterBackingStore::flush(QWindow *window, const QRegion ®ion, const { Q_UNUSED(offset); - qRasterBackingStoreDebug() << "w =" << this->window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << this->window(); // Sometimes this method is called even though there is nothing to be // flushed (posted in "screen" parlance), for instance, after an expose @@ -67,7 +61,7 @@ void QQnxRasterBackingStore::resize(const QSize &size, const QRegion &staticCont { Q_UNUSED(size); Q_UNUSED(staticContents); - qRasterBackingStoreDebug() << "w =" << window() << ", s =" << size; + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window() << ", s =" << size; // NOTE: defer resizing window buffers until next paint as // resize() can be called multiple times before a paint occurs @@ -75,7 +69,7 @@ void QQnxRasterBackingStore::resize(const QSize &size, const QRegion &staticCont bool QQnxRasterBackingStore::scroll(const QRegion &area, int dx, int dy) { - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); m_needsPosting = true; @@ -91,7 +85,7 @@ void QQnxRasterBackingStore::beginPaint(const QRegion ®ion) { Q_UNUSED(region); - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); m_needsPosting = true; platformWindow()->adjustBufferSize(); @@ -119,7 +113,7 @@ void QQnxRasterBackingStore::beginPaint(const QRegion ®ion) void QQnxRasterBackingStore::endPaint() { - qRasterBackingStoreDebug() << "w =" << window(); + qCDebug(lcQpaBackingStore) << Q_FUNC_INFO << "w =" << window(); } QQnxRasterWindow *QQnxRasterBackingStore::platformWindow() const diff --git a/src/plugins/platforms/qnx/qqnxrasterwindow.cpp b/src/plugins/platforms/qnx/qqnxrasterwindow.cpp index fb7a1d3fd3..303c9e7c06 100644 --- a/src/plugins/platforms/qnx/qqnxrasterwindow.cpp +++ b/src/plugins/platforms/qnx/qqnxrasterwindow.cpp @@ -10,12 +10,6 @@ #include <errno.h> -#if defined(QQNXRASTERWINDOW_DEBUG) -#define qRasterWindowDebug qDebug -#else -#define qRasterWindowDebug QT_NO_QDEBUG_MACRO -#endif - QT_BEGIN_NAMESPACE QQnxRasterWindow::QQnxRasterWindow(QWindow *window, screen_context_t context, bool needRootWindow) : @@ -61,7 +55,7 @@ void QQnxRasterWindow::post(const QRegion &dirty) // Check if render buffer exists and something was rendered if (m_currentBufferIndex != -1 && !dirty.isEmpty()) { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); QQnxBuffer ¤tBuffer = m_buffers[m_currentBufferIndex]; // Copy unmodified region from old render buffer to new render buffer; @@ -94,14 +88,14 @@ void QQnxRasterWindow::post(const QRegion &dirty) void QQnxRasterWindow::scroll(const QRegion ®ion, int dx, int dy, bool flush) { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); blitPreviousToCurrent(region, dx, dy, flush); m_scrolled += region; } QQnxBuffer &QQnxRasterWindow::renderBuffer() { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); // Check if render buffer is invalid if (m_currentBufferIndex == -1) { @@ -162,7 +156,7 @@ void QQnxRasterWindow::resetBuffers() void QQnxRasterWindow::blitPreviousToCurrent(const QRegion ®ion, int dx, int dy, bool flush) { - qRasterWindowDebug() << "window =" << window(); + qCDebug(lcQpaWindow) << Q_FUNC_INFO << "window = " << window(); // Abort if previous buffer is invalid or if nothing to copy if (m_previousBufferIndex == -1 || region.isEmpty()) diff --git a/src/plugins/platforms/qnx/qqnxscreen.cpp b/src/plugins/platforms/qnx/qqnxscreen.cpp index 66f8bfae01..f2c3b3847d 100644 --- a/src/plugins/platforms/qnx/qqnxscreen.cpp +++ b/src/plugins/platforms/qnx/qqnxscreen.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2011 - 2013 BlackBerry Limited. All rights reserved. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qqnxglobal.h" #include "qqnxscreen.h" @@ -13,12 +15,6 @@ #include <errno.h> -#if defined(QQNXSCREEN_DEBUG) -#define qScreenDebug qDebug -#else -#define qScreenDebug QT_NO_QDEBUG_MACRO -#endif - #if defined(QQNX_PHYSICAL_SCREEN_WIDTH) && QQNX_PHYSICAL_SCREEN_WIDTH > 0 \ && defined(QQNX_PHYSICAL_SCREEN_HEIGHT) && QQNX_PHYSICAL_SCREEN_HEIGHT > 0 #define QQNX_PHYSICAL_SCREEN_SIZE_DEFINED @@ -32,6 +28,8 @@ static const int MAX_UNDERLAY_ZORDER = -1; QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaScreen, "qt.qpa.screen"); + static QSize determineScreenSize(screen_display_t display, bool primaryScreen) { int val[2]; @@ -44,9 +42,9 @@ static QSize determineScreenSize(screen_display_t display, bool primaryScreen) { if (val[0] > 0 && val[1] > 0) return QSize(val[0], val[1]); - qScreenDebug("QQnxScreen: screen_get_display_property_iv() reported an invalid " - "physical screen size (%dx%d). Falling back to QQNX_PHYSICAL_SCREEN_SIZE " - "environment variable.", val[0], val[1]); + qCDebug(lcQpaScreen, "QQnxScreen: screen_get_display_property_iv() reported an invalid " + "physical screen size (%dx%d). Falling back to QQNX_PHYSICAL_SCREEN_SIZE " + "environment variable.", val[0], val[1]); const QString envPhySizeStr = qgetenv("QQNX_PHYSICAL_SCREEN_SIZE"); if (!envPhySizeStr.isEmpty()) { @@ -88,7 +86,7 @@ QQnxScreen::QQnxScreen(screen_context_t screenContext, screen_display_t display, m_coverWindow(0), m_cursor(new QQnxCursor()) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; // Cache initial orientation of this display int result = screen_get_display_property_iv(m_display, SCREEN_PROPERTY_ROTATION, &m_initialRotation); @@ -125,7 +123,7 @@ QQnxScreen::QQnxScreen(screen_context_t screenContext, screen_display_t display, QQnxScreen::~QQnxScreen() { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; Q_FOREACH (QQnxWindow *childWindow, m_childWindows) childWindow->setScreen(0); @@ -234,7 +232,7 @@ QPixmap QQnxScreen::grabWindow(WId window, int x, int y, int width, int height) static int defaultDepth() { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; static int defaultDepth = 0; if (defaultDepth == 0) { // check if display depth was specified in environment variable; @@ -248,7 +246,7 @@ static int defaultDepth() QRect QQnxScreen::availableGeometry() const { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; // available geometry = total geometry - keyboard return QRect(m_currentGeometry.x(), m_currentGeometry.y(), m_currentGeometry.width(), m_currentGeometry.height() - m_keyboardHeight); @@ -268,12 +266,12 @@ qreal QQnxScreen::refreshRate() const qWarning("QQnxScreen: Failed to query screen mode. Using default value of 60Hz"); return 60.0; } - qScreenDebug("screen mode:\n" - " width = %u\n" - " height = %u\n" - " refresh = %u\n" - " interlaced = %u", - uint(displayMode.width), uint(displayMode.height), uint(displayMode.refresh), uint(displayMode.interlaced)); + qCDebug(lcQpaScreen, "screen mode:\n" + " width = %u\n" + " height = %u\n" + " refresh = %u\n" + " interlaced = %u", + uint(displayMode.width), uint(displayMode.height), uint(displayMode.refresh), uint(displayMode.interlaced)); return static_cast<qreal>(displayMode.refresh); } @@ -307,7 +305,7 @@ Qt::ScreenOrientation QQnxScreen::orientation() const else orient = Qt::InvertedLandscapeOrientation; } - qScreenDebug() << "orientation =" << orient; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Orientation =" << orient; return orient; } @@ -331,7 +329,7 @@ static bool isOrthogonal(int angle1, int angle2) void QQnxScreen::setRotation(int rotation) { - qScreenDebug("orientation = %d", rotation); + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "orientation =" << rotation; // Check if rotation changed // We only want to rotate if we are the primary screen if (m_currentRotation != rotation && isPrimaryScreen()) { @@ -352,7 +350,7 @@ void QQnxScreen::setRotation(int rotation) // Resize root window if we've rotated 90 or 270 from previous orientation if (isOrthogonal(m_currentRotation, rotation)) { - qScreenDebug() << "resize, size =" << m_currentGeometry.size(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "resize, size =" << m_currentGeometry.size(); if (rootWindow()) rootWindow()->setGeometry(QRect(QPoint(0,0), m_currentGeometry.size())); @@ -499,7 +497,7 @@ QQnxWindow *QQnxScreen::findWindow(screen_window_t windowHandle) const void QQnxScreen::addWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (m_childWindows.contains(window)) return; @@ -522,7 +520,7 @@ void QQnxScreen::addWindow(QQnxWindow *window) void QQnxScreen::removeWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (window != m_coverWindow) { const int numWindowsRemoved = m_childWindows.removeAll(window); @@ -537,7 +535,7 @@ void QQnxScreen::removeWindow(QQnxWindow *window) void QQnxScreen::raiseWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (window != m_coverWindow) { removeWindow(window); @@ -547,7 +545,7 @@ void QQnxScreen::raiseWindow(QQnxWindow *window) void QQnxScreen::lowerWindow(QQnxWindow *window) { - qScreenDebug() << "window =" << window; + qCDebug(lcQpaScreen) << Q_FUNC_INFO << "Window =" << window; if (window != m_coverWindow) { removeWindow(window); @@ -557,7 +555,7 @@ void QQnxScreen::lowerWindow(QQnxWindow *window) void QQnxScreen::updateHierarchy() { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; QList<QQnxWindow*>::const_iterator it; int result; @@ -707,7 +705,7 @@ void QQnxScreen::windowClosed(void *window) void QQnxScreen::windowGroupStateChanged(const QByteArray &id, Qt::WindowState state) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; if (!rootWindow() || id != rootWindow()->groupName()) return; @@ -722,7 +720,7 @@ void QQnxScreen::windowGroupStateChanged(const QByteArray &id, Qt::WindowState s void QQnxScreen::activateWindowGroup(const QByteArray &id) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; if (!rootWindow() || id != rootWindow()->groupName()) return; @@ -741,7 +739,7 @@ void QQnxScreen::activateWindowGroup(const QByteArray &id) void QQnxScreen::deactivateWindowGroup(const QByteArray &id) { - qScreenDebug(); + qCDebug(lcQpaScreen) << Q_FUNC_INFO; if (!rootWindow() || id != rootWindow()->groupName()) return; diff --git a/src/plugins/platforms/qnx/qqnxscreen.h b/src/plugins/platforms/qnx/qqnxscreen.h index f988b42ab4..17b282bdc1 100644 --- a/src/plugins/platforms/qnx/qqnxscreen.h +++ b/src/plugins/platforms/qnx/qqnxscreen.h @@ -30,6 +30,10 @@ const int SCREEN_PROPERTY_SYM = SCREEN_PROPERTY_KEY_SYM; QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen); +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreenEvents); +Q_DECLARE_LOGGING_CATEGORY(lcQpaScreenBuffer); + class QQnxWindow; class QQnxScreen : public QObject, public QPlatformScreen diff --git a/src/plugins/platforms/qnx/qqnxscreeneventhandler.cpp b/src/plugins/platforms/qnx/qqnxscreeneventhandler.cpp index 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/CMakeLists.txt b/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt index 719e5c45e6..406487f1e9 100644 --- a/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt +++ b/src/plugins/platforms/vkkhrdisplay/CMakeLists.txt @@ -6,7 +6,7 @@ qt_find_package(WrapFreetype PROVIDED_TARGETS WrapFreetype::WrapFreetype) qt_internal_add_plugin(QVkKhrDisplayIntegrationPlugin OUTPUT_NAME qvkkhrdisplay PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES vkkhrdisplay + DEFAULT_IF "vkkhrdisplay" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qvkkhrdisplayintegration.cpp qvkkhrdisplayintegration.h diff --git a/src/plugins/platforms/vkkhrdisplay/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/vnc/CMakeLists.txt b/src/plugins/platforms/vnc/CMakeLists.txt index 25cb399bd0..34370807ae 100644 --- a/src/plugins/platforms/vnc/CMakeLists.txt +++ b/src/plugins/platforms/vnc/CMakeLists.txt @@ -8,7 +8,7 @@ qt_internal_add_plugin(QVncIntegrationPlugin OUTPUT_NAME qvnc PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES vnc + DEFAULT_IF "vnc" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qvnc.cpp qvnc_p.h diff --git a/src/plugins/platforms/wasm/CMakeLists.txt b/src/plugins/platforms/wasm/CMakeLists.txt index d7c96afdaa..775946aaf9 100644 --- a/src/plugins/platforms/wasm/CMakeLists.txt +++ b/src/plugins/platforms/wasm/CMakeLists.txt @@ -7,9 +7,8 @@ qt_internal_add_plugin(QWasmIntegrationPlugin OUTPUT_NAME qwasm - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES wasm + DEFAULT_IF "wasm" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms - STATIC SOURCES main.cpp qwasmaccessibility.cpp qwasmaccessibility.h @@ -32,9 +31,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 +48,6 @@ qt_internal_add_plugin(QWasmIntegrationPlugin # Resources: set(wasmfonts_resource_files - "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/Vera.ttf" "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSans.ttf" "${QtBase_SOURCE_DIR}/src/3rdparty/wasm/DejaVuSansMono.ttf" ) 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..e418263655 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,18 +101,21 @@ 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>(); deadKey = isDeadKeyEvent(webKey.c_str()); - + autoRepeat = event["repeat"].as<bool>(); modifiers = KeyboardModifier::getForEvent(event); key = webKeyToQtKey(code, webKey, deadKey, modifiers); text = QString::fromUtf8(webKey); if (text.size() > 1) text.clear(); + + if (key == Qt::Key_Tab) + text = "\t"; } KeyEvent::~KeyEvent() = default; @@ -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..bd0fb39f11 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 @@ -146,6 +153,7 @@ struct KeyEvent : public Event QFlags<Qt::KeyboardModifier> modifiers; bool deadKey; QString text; + bool autoRepeat; }; struct MouseEvent : public Event @@ -214,6 +222,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 +233,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..0490b2bfe0 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,31 @@ 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()) { + const QWindowList list = child->window()->findChildren<QWindow *>(Qt::FindChildrenRecursively); + for (auto child : list) { + auto handle = child->handle(); + if (handle) { + auto wnd = static_cast<QWasmWindow *>(handle); + windows.push_back(wnd); + } + } + windows.push_back(child); + } + return windows; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/wasm/qwasmscreen.h b/src/plugins/platforms/wasm/qwasmscreen.h index 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..0513f46e5b 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; @@ -469,7 +502,7 @@ bool QWasmWindow::processKey(const KeyEvent &event) const auto result = QWindowSystemInterface::handleKeyEvent( 0, event.type == EventType::KeyDown ? QEvent::KeyPress : QEvent::KeyRelease, event.key, - event.modifiers, event.text); + event.modifiers, event.text, event.autoRepeat); return clipboardResult == ProcessKeyboardResult::NativeClipboardEventAndCopiedDataNeeded ? ProceedToNativeEvent : result; @@ -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..6e93955552 100644 --- a/src/plugins/platforms/wasm/wasm_shell.html +++ b/src/plugins/platforms/wasm/wasm_shell.html @@ -1,3 +1,8 @@ +<!-- +Copyright (C) 2024 The Qt Company Ltd. +SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +--> + <!doctype html> <html lang="en-us"> <head> @@ -27,42 +32,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'); + + 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...'; - 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 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..8cd84e208b 100644 --- a/src/plugins/platforms/windows/CMakeLists.txt +++ b/src/plugins/platforms/windows/CMakeLists.txt @@ -8,20 +8,20 @@ qt_internal_add_plugin(QWindowsIntegrationPlugin OUTPUT_NAME qwindows PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES windows + DEFAULT_IF "windows" IN_LIST QT_QPA_PLATFORMS SOURCES main.cpp qtwindowsglobal.h qwin10helpers.cpp qwin10helpers.h qwindowsapplication.cpp qwindowsapplication.h qwindowsbackingstore.cpp qwindowsbackingstore.h - qwindowscombase.h qwindowscontext.cpp qwindowscontext.h qwindowscursor.cpp qwindowscursor.h qwindowsdialoghelpers.cpp qwindowsdialoghelpers.h qwindowsdropdataobject.cpp qwindowsdropdataobject.h qwindowsgdiintegration.cpp qwindowsgdiintegration.h qwindowsgdinativeinterface.cpp qwindowsgdinativeinterface.h + qwindowsiconengine.cpp qwindowsiconengine.h qwindowsinputcontext.cpp qwindowsinputcontext.h qwindowsintegration.cpp qwindowsintegration.h qwindowsinternalmimedata.cpp qwindowsinternalmimedata.h @@ -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..8c0261d568 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); @@ -201,6 +201,8 @@ QWindowsContext::~QWindowsContext() if (d->m_powerDummyWindow) DestroyWindow(d->m_powerDummyWindow); + d->m_screenManager.destroyWindow(); + unregisterWindowClasses(); if (d->m_oleInitializeResult == S_OK || d->m_oleInitializeResult == S_FALSE) { #ifdef QT_USE_FACTORY_CACHE_REGISTRATION @@ -461,11 +463,6 @@ bool QWindowsContext::setProcessDpiAwareness(QtWindows::DpiAwareness dpiAwarenes return true; } -bool QWindowsContext::isDarkMode() -{ - return QWindowsContextPrivate::m_darkMode; -} - QWindowsContext *QWindowsContext::instance() { return m_instance; @@ -481,9 +478,9 @@ bool QWindowsContext::useRTLExtensions() const return d->m_keyMapper.useRTLExtensions(); } -QList<int> QWindowsContext::possibleKeys(const QKeyEvent *e) const +QPlatformKeyMapper *QWindowsContext::keyMapper() const { - return d->m_keyMapper.possibleKeys(e); + return &d->m_keyMapper; } QWindowsContext::HandleBaseWindowHash &QWindowsContext::windows() @@ -535,6 +532,8 @@ QString QWindowsContext::classNamePrefix() # define xstr(s) str(s) # define str(s) #s str << xstr(QT_NAMESPACE); +# undef str +# undef xstr #endif } return result; @@ -1001,9 +1000,10 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, MSG msg; msg.hwnd = hwnd; // re-create MSG structure - msg.message = message; // time and pt fields ignored + msg.message = message; msg.wParam = wParam; msg.lParam = lParam; + msg.time = GetMessageTime(); msg.pt.x = msg.pt.y = 0; if (et != QtWindows::CursorEvent && (et & (QtWindows::MouseEventFlag | QtWindows::NonClientEventFlag))) { msg.pt.x = GET_X_LPARAM(lParam); @@ -1087,21 +1087,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, // Only refresh the window theme if the user changes the personalize settings. if ((wParam == 0) && (lParam != 0) // lParam sometimes may be NULL. && (wcscmp(reinterpret_cast<LPCWSTR>(lParam), L"ImmersiveColorSet") == 0)) { - const bool darkMode = QWindowsTheme::queryDarkMode(); - 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 +1104,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, d->m_creationContext->applyToMinMaxInfo(reinterpret_cast<MINMAXINFO *>(lParam)); return true; case QtWindows::ResizeEvent: - d->m_creationContext->obtainedSize = QSize(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + d->m_creationContext->obtainedSize = QSize(LOWORD(lParam), HIWORD(lParam)); return true; case QtWindows::MoveEvent: d->m_creationContext->obtainedPos = QPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); @@ -1161,7 +1147,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, if (wParam == DBT_DEVNODES_CHANGED) initTouch(); break; - case QtWindows::KeyboardLayoutChangeEvent: + case QtWindows::InputLanguageChangeEvent: if (QWindowsInputContext *wic = windowsInputContext()) wic->handleInputLanguageChanged(wParam, lParam); Q_FALLTHROUGH(); @@ -1205,21 +1191,29 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, case QtWindows::ExposeEvent: return platformWindow->handleWmPaint(hwnd, message, wParam, lParam, result); case QtWindows::NonClientMouseEvent: - if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer) && platformWindow->frameStrutEventsEnabled()) + if (!platformWindow->frameStrutEventsEnabled()) + break; + if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer)) return sessionManagerInteractionBlocked() || d->m_pointerHandler.translateMouseEvent(platformWindow->window(), hwnd, et, msg, result); else return sessionManagerInteractionBlocked() || d->m_mouseHandler.translateMouseEvent(platformWindow->window(), hwnd, et, msg, result); case QtWindows::NonClientPointerEvent: - if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer) && platformWindow->frameStrutEventsEnabled()) + if (!platformWindow->frameStrutEventsEnabled()) + break; + if ((d->m_systemInfo & QWindowsContext::SI_SupportsPointer)) return sessionManagerInteractionBlocked() || d->m_pointerHandler.translatePointerEvent(platformWindow->window(), hwnd, et, msg, result); break; case QtWindows::EnterSizeMoveEvent: platformWindow->setFlag(QWindowsWindow::ResizeMoveActive); + if (!IsZoomed(hwnd)) + platformWindow->updateRestoreGeometry(); return true; case QtWindows::ExitSizeMoveEvent: platformWindow->clearFlag(QWindowsWindow::ResizeMoveActive); platformWindow->checkForScreenChanged(); handleExitSizeMove(platformWindow->window()); + if (!IsZoomed(hwnd)) + platformWindow->updateRestoreGeometry(); return true; case QtWindows::ScrollEvent: if (!(d->m_systemInfo & QWindowsContext::SI_SupportsPointer)) @@ -1265,6 +1259,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, QWindowSystemInterface::handleCloseEvent(platformWindow->window()); return true; case QtWindows::ThemeChanged: { + QWindowsThemeCache::clearThemeCache(platformWindow->handle()); // Switch from Aero to Classic changes margins. if (QWindowsTheme *theme = QWindowsTheme::instance()) theme->windowsThemeChanged(platformWindow->window()); @@ -1403,7 +1398,7 @@ void QWindowsContext::handleFocusEvent(QtWindows::WindowsEventType et, } if (nextActiveWindow != d->m_lastActiveWindow) { d->m_lastActiveWindow = nextActiveWindow; - QWindowSystemInterface::handleWindowActivated(nextActiveWindow, Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged(nextActiveWindow, Qt::ActiveWindowFocusReason); } } @@ -1433,7 +1428,7 @@ bool QWindowsContext::handleContextMenuEvent(QWindow *window, const MSG &msg) } QWindowSystemInterface::handleContextMenuEvent(window, mouseTriggered, pos, globalPos, - QWindowsKeyMapper::queryKeyboardModifiers()); + keyMapper()->queryKeyboardModifiers()); return true; } #endif @@ -1454,7 +1449,7 @@ void QWindowsContext::handleExitSizeMove(QWindow *window) const Qt::MouseButtons appButtons = QGuiApplication::mouseButtons(); if (currentButtons == appButtons) return; - const Qt::KeyboardModifiers keyboardModifiers = QWindowsKeyMapper::queryKeyboardModifiers(); + const Qt::KeyboardModifiers keyboardModifiers = keyMapper()->queryKeyboardModifiers(); const QPoint globalPos = QWindowsCursor::mousePosition(); const QPlatformWindow *platWin = window->handle(); const QPoint localPos = platWin->mapFromGlobal(globalPos); @@ -1463,8 +1458,7 @@ void QWindowsContext::handleExitSizeMove(QWindow *window) for (Qt::MouseButton button : {Qt::LeftButton, Qt::RightButton, Qt::MiddleButton}) { if (appButtons.testFlag(button) && !currentButtons.testFlag(button)) { QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, - currentButtons, button, type, - keyboardModifiers); + currentButtons, button, type, keyboardModifiers); } } if (d->m_systemInfo & QWindowsContext::SI_SupportsPointer) 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..1f22fb4f60 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; - } - } - } + }); - return false; + pathInfos.erase(discardThese, pathInfos.end()); + + 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); @@ -545,11 +698,15 @@ void QWindowsScreenManager::initialize() handleScreenChanges(); } -QWindowsScreenManager::~QWindowsScreenManager() +void QWindowsScreenManager::destroyWindow() { + qCDebug(lcQpaScreen) << "Destroying display change observer" << m_displayChangeObserver; DestroyWindow(m_displayChangeObserver); + m_displayChangeObserver = nullptr; } +QWindowsScreenManager::~QWindowsScreenManager() = default; + bool QWindowsScreenManager::isSingleScreen() { return QWindowsContext::instance()->screenManager().screens().size() < 2; diff --git a/src/plugins/platforms/windows/qwindowsscreen.h b/src/plugins/platforms/windows/qwindowsscreen.h index b1c94d2204..ea6a29efe3 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; @@ -100,6 +105,7 @@ public: QWindowsScreenManager(); void initialize(); + void destroyWindow(); ~QWindowsScreenManager(); void clearScreens(); diff --git a/src/plugins/platforms/windows/qwindowsservices.cpp b/src/plugins/platforms/windows/qwindowsservices.cpp index 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..b6017c7692 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 + { + 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() { - connect(this, &QThread::finished, this, &QObject::deleteLater); + 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); - -#if QT_CONFIG(cpp_winrt) - // respect the Windows 11 accent color - using namespace winrt::Windows::UI::ViewManagement; - const auto settings = UISettings(); + const QColor background = getSysColor(COLOR_BTNFACE); + const QColor textColor = getSysColor(COLOR_WINDOWTEXT); - 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,8 +299,11 @@ void QWindowsTheme::populateLightSystemBasePalette(QPalette &result) result.setColor(QPalette::Midlight, result.button().color().lighter(110)); } -static void populateDarkSystemBasePalette(QPalette &result) +void QWindowsTheme::populateDarkSystemBasePalette(QPalette &result) { + QColor foreground, background, + accent, accentDark, accentDarker, accentDarkest, + accentLight, accentLighter, accentLightest; #if QT_CONFIG(cpp_winrt) using namespace winrt::Windows::UI::ViewManagement; const auto settings = UISettings(); @@ -278,34 +311,38 @@ static void populateDarkSystemBasePalette(QPalette &result) // We have to craft a palette from these colors. The settings.UIElementColor(UIElementType) API // returns the old system colors, not the dark mode colors. If the background is black (which it // usually), then override it with a dark gray instead so that we can go up and down the lightness. - const QColor foreground = getSysColor(settings.GetColorValue(UIColorType::Foreground)); - const QColor background = [&settings]() -> QColor { - auto systemBackground = getSysColor(settings.GetColorValue(UIColorType::Background)); - if (systemBackground == Qt::black) - systemBackground = QColor(0x1E, 0x1E, 0x1E); - return systemBackground; - }(); - - const QColor accent = getSysColor(settings.GetColorValue(UIColorType::Accent)); - const QColor accentDark = getSysColor(settings.GetColorValue(UIColorType::AccentDark1)); - const QColor accentDarker = getSysColor(settings.GetColorValue(UIColorType::AccentDark2)); - const QColor accentDarkest = getSysColor(settings.GetColorValue(UIColorType::AccentDark3)); - 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 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; + if (QWindowsTheme::queryColorScheme() == Qt::ColorScheme::Dark) { + // the system is actually running in dark mode, so UISettings will give us dark colors + foreground = getSysColor(settings.GetColorValue(UIColorType::Foreground)); + background = [&settings]() -> QColor { + auto systemBackground = getSysColor(settings.GetColorValue(UIColorType::Background)); + if (systemBackground == Qt::black) + systemBackground = QColor(0x1E, 0x1E, 0x1E); + return systemBackground; + }(); + + accent = getSysColor(settings.GetColorValue(UIColorType::Accent)); + accentDark = getSysColor(settings.GetColorValue(UIColorType::AccentDark1)); + accentDarker = getSysColor(settings.GetColorValue(UIColorType::AccentDark2)); + accentDarkest = getSysColor(settings.GetColorValue(UIColorType::AccentDark3)); + accentLight = getSysColor(settings.GetColorValue(UIColorType::AccentLight1)); + accentLighter = getSysColor(settings.GetColorValue(UIColorType::AccentLight2)); + accentLightest = getSysColor(settings.GetColorValue(UIColorType::AccentLight3)); + } else #endif + { + // If the system is running in light mode, then we need to make up our own dark palette + foreground = Qt::white; + background = QColor(0x1E, 0x1E, 0x1E); + accent = qt_accentColor(AccentColorNormal); + accentDark = accent.darker(120); + accentDarker = accentDark.darker(120); + accentDarkest = accentDarker.darker(120); + accentLight = accent.lighter(120); + accentLighter = accentLight.lighter(120); + accentLightest = accentLighter.lighter(120); + } + const QColor linkColor = accent; const QColor buttonColor = background.lighter(200); result.setColor(QPalette::All, QPalette::WindowText, foreground); @@ -331,36 +368,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 +460,7 @@ QWindowsTheme *QWindowsTheme::m_instance = nullptr; QWindowsTheme::QWindowsTheme() { m_instance = this; + s_colorScheme = QWindowsTheme::queryColorScheme(); std::fill(m_fonts, m_fonts + NFonts, nullptr); std::fill(m_palettes, m_palettes + NPalettes, nullptr); refresh(); @@ -473,7 +482,10 @@ static inline QStringList iconThemeSearchPaths() static inline QStringList styleNames() { - return { QStringLiteral("WindowsVista"), QStringLiteral("Windows") }; + QStringList styles = { QStringLiteral("WindowsVista"), QStringLiteral("Windows") }; + if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11) + styles.prepend(QStringLiteral("Windows11")); + return styles; } static inline int uiEffects() @@ -538,7 +550,43 @@ 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; + if (s_colorSchemeOverride != Qt::ColorScheme::Unknown) + return s_colorSchemeOverride; + if (s_colorScheme != Qt::ColorScheme::Unknown) + return s_colorScheme; + return queryColorScheme(); +} + +void QWindowsTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + s_colorSchemeOverride = scheme; + handleSettingsChanged(); +} + +void QWindowsTheme::handleSettingsChanged() +{ + const auto oldColorScheme = s_colorScheme; + s_colorScheme = Qt::ColorScheme::Unknown; // make effectiveColorScheme() query registry + const auto newColorScheme = effectiveColorScheme(); + const bool colorSchemeChanged = newColorScheme != oldColorScheme; + s_colorScheme = newColorScheme; + auto integration = QWindowsIntegration::instance(); + integration->updateApplicationBadge(); + if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle)) { + QWindowsTheme::instance()->refresh(); + QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); + } + if (colorSchemeChanged) { + for (QWindowsWindow *w : std::as_const(QWindowsContext::instance()->windows())) + w->setDarkBorder(s_colorScheme == Qt::ColorScheme::Dark); + } } void QWindowsTheme::clearPalettes() @@ -549,41 +597,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 +768,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 +881,18 @@ QPixmap QWindowsTheme::standardPixmap(StandardPixmap sp, const QSizeF &pixmapSiz } if (stockId != SIID_INVALID) { - QPixmap pixmap; SHSTOCKICONINFO iconInfo; memset(&iconInfo, 0, sizeof(iconInfo)); iconInfo.cbSize = sizeof(iconInfo); - stockFlags |= (pixmapSize.width() > 16 ? SHGFI_LARGEICON : SHGFI_SMALLICON); - if (SHGetStockIconInfo(stockId, SHGFI_ICON | stockFlags, &iconInfo) == S_OK) { - pixmap = qt_pixmapFromWinHICON(iconInfo.hIcon); - DestroyIcon(iconInfo.hIcon); - return pixmap; + stockFlags |= SHGSI_ICONLOCATION; + if (SHGetStockIconInfo(stockId, stockFlags, &iconInfo) == S_OK) { + const auto iconSize = pixmapSize.width(); + HICON icon; + if (SHDefExtractIcon(iconInfo.szPath, iconInfo.iIcon, 0, &icon, nullptr, iconSize) == S_OK) { + QPixmap pixmap = qt_pixmapFromWinHICON(icon); + DestroyIcon(icon); + return pixmap; + } } } @@ -890,10 +962,9 @@ public: // Shell image list helper functions. -static QPixmap pixmapFromShellImageList(int iImageList, const SHFILEINFO &info) +static QPixmap pixmapFromShellImageList(int iImageList, int iIcon) { QPixmap result; -#ifdef USE_IIMAGELIST // For MinGW: static const IID iID_IImageList = {0x46eb5926, 0x582e, 0x4017, {0x9f, 0xdf, 0xe8, 0x99, 0x8d, 0xaa, 0x9, 0x50}}; @@ -902,16 +973,12 @@ static QPixmap pixmapFromShellImageList(int iImageList, const SHFILEINFO &info) if (hr != S_OK) return result; HICON hIcon; - hr = imageList->GetIcon(info.iIcon, ILD_TRANSPARENT, &hIcon); + hr = imageList->GetIcon(iIcon, ILD_TRANSPARENT, &hIcon); if (hr == S_OK) { result = qt_pixmapFromWinHICON(hIcon); DestroyIcon(hIcon); } imageList->Release(); -#else - Q_UNUSED(iImageList); - Q_UNUSED(info); -#endif // USE_IIMAGELIST return result; } @@ -963,13 +1030,9 @@ QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon const int width = int(size.width()); const int iconSize = width > fileIconSizes[SmallFileIcon] ? SHGFI_LARGEICON : SHGFI_SMALLICON; const int requestedImageListSize = -#ifdef USE_IIMAGELIST width > fileIconSizes[ExtraLargeFileIcon] ? sHIL_JUMBO : (width > fileIconSizes[LargeFileIcon] ? sHIL_EXTRALARGE : 0); -#else - 0; -#endif // !USE_IIMAGELIST bool cacheableDirIcon = fileInfo().isDir() && !fileInfo().isRoot(); if (cacheableDirIcon) { QMutexLocker locker(&mx); @@ -985,7 +1048,6 @@ QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon } } - SHFILEINFO info; unsigned int flags = SHGFI_ICON | iconSize | SHGFI_SYSICONINDEX | SHGFI_ADDOVERLAYS | SHGFI_OVERLAYINDEX; DWORD attributes = 0; QString path = filePath; @@ -997,43 +1059,43 @@ QPixmap QWindowsFileIconEngine::filePixmap(const QSize &size, QIcon::Mode, QIcon flags |= SHGFI_USEFILEATTRIBUTES; attributes |= FILE_ATTRIBUTE_NORMAL; } - const bool val = shGetFileInfoBackground(path, attributes, &info, flags); - + auto task = QSharedPointer<QShGetFileInfoThread::Task>( + new QShGetFileInfoThread::Task(path, attributes, flags)); + s_shGetFileInfoThread()->runWithParams(task); // Even if GetFileInfo returns a valid result, hIcon can be empty in some cases - if (val && info.hIcon) { + if (task->resultValid()) { QString key; if (cacheableDirIcon) { if (useDefaultFolderIcon && defaultFolderIIcon < 0) - defaultFolderIIcon = info.iIcon; + defaultFolderIIcon = task->iIcon; //using the unique icon index provided by windows save us from duplicate keys - key = dirIconPixmapCacheKey(info.iIcon, iconSize, requestedImageListSize); + key = dirIconPixmapCacheKey(task->iIcon, iconSize, requestedImageListSize); QPixmapCache::find(key, &pixmap); if (!pixmap.isNull()) { QMutexLocker locker(&mx); - dirIconEntryCache.insert(filePath, FakePointer<int>::create(info.iIcon)); + dirIconEntryCache.insert(filePath, FakePointer<int>::create(task->iIcon)); } } if (pixmap.isNull()) { if (requestedImageListSize) { - pixmap = pixmapFromShellImageList(requestedImageListSize, info); + pixmap = pixmapFromShellImageList(requestedImageListSize, task->iIcon); if (pixmap.isNull() && requestedImageListSize == sHIL_JUMBO) - pixmap = pixmapFromShellImageList(sHIL_EXTRALARGE, info); + pixmap = pixmapFromShellImageList(sHIL_EXTRALARGE, task->iIcon); } if (pixmap.isNull()) - pixmap = qt_pixmapFromWinHICON(info.hIcon); + pixmap = qt_pixmapFromWinHICON(task->hIcon); if (!pixmap.isNull()) { if (cacheableDirIcon) { QMutexLocker locker(&mx); QPixmapCache::insert(key, pixmap); - dirIconEntryCache.insert(filePath, FakePointer<int>::create(info.iIcon)); + dirIconEntryCache.insert(filePath, FakePointer<int>::create(task->iIcon)); } } else { qWarning("QWindowsTheme::fileIconPixmap() no icon found"); } } - DestroyIcon(info.hIcon); } return pixmap; @@ -1044,6 +1106,11 @@ QIcon QWindowsTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOpt return QIcon(new QWindowsFileIconEngine(fileInfo, iconOptions)); } +QIconEngine *QWindowsTheme::createIconEngine(const QString &iconName) const +{ + return new QWindowsIconEngine(iconName); +} + static inline bool doUseNativeMenus() { const unsigned options = QWindowsIntegration::instance()->options(); @@ -1068,19 +1135,23 @@ bool QWindowsTheme::useNativeMenus() return result; } -bool QWindowsTheme::queryDarkMode() +Qt::ColorScheme QWindowsTheme::queryColorScheme() { - if (queryHighContrast()) { - return false; - } + if (queryHighContrast()) + return Qt::ColorScheme::Unknown; + const auto setting = QWinRegistryKey(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)") .dwordValue(L"AppsUseLightTheme"); - return setting.second && setting.first == 0; + return setting.second && setting.first == 0 ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; } bool QWindowsTheme::queryHighContrast() { - return booleanSystemParametersInfo(SPI_GETHIGHCONTRAST, false); + HIGHCONTRAST hcf = {}; + hcf.cbSize = static_cast<UINT>(sizeof(HIGHCONTRAST)); + if (SystemParametersInfo(SPI_GETHIGHCONTRAST, hcf.cbSize, &hcf, FALSE)) + return hcf.dwFlags & HCF_HIGHCONTRASTON; + return false; } QPlatformMenuItem *QWindowsTheme::createPlatformMenuItem() const diff --git a/src/plugins/platforms/windows/qwindowstheme.h b/src/plugins/platforms/windows/qwindowstheme.h index 7f320da967..a89fb1e5bd 100644 --- a/src/plugins/platforms/windows/qwindowstheme.h +++ b/src/plugins/platforms/windows/qwindowstheme.h @@ -32,6 +32,9 @@ public: QVariant themeHint(ThemeHint) const override; Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; + + static void handleSettingsChanged(); const QPalette *palette(Palette type = SystemPalette) const override { return m_palettes[type]; } @@ -41,6 +44,7 @@ public: QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions iconOptions = {}) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; void windowsThemeChanged(QWindow *window); void displayChanged() { refreshIconPixmapSizes(); } @@ -53,15 +57,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 +71,17 @@ 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; + static inline Qt::ColorScheme s_colorSchemeOverride = Qt::ColorScheme::Unknown; + QPalette *m_palettes[NPalettes]; QFont *m_fonts[NFonts]; QList<QSize> m_fileIconSizes; diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp index d0107145af..acfff64751 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; } } @@ -842,7 +850,8 @@ static inline bool shouldApplyDarkFrame(const QWindow *w) { if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) return false; - // the application has explicitly opted out of dark frames + + // the user of the application has explicitly opted out of dark frames if (!QWindowsIntegration::instance()->darkModeHandling().testFlag(QWindowsApplication::DarkModeWindowFrames)) return false; @@ -922,7 +931,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 +942,7 @@ QWindowsWindowData QRect obtainedGeometry(context->obtainedPos, context->obtainedSize); result.geometry = obtainedGeometry; + result.restoreGeometry = frameGeometry(result.hwnd, topLevel); result.fullFrameMargins = context->margins; result.embedded = embedded; result.hasFrame = hasFrame; @@ -1007,6 +1017,21 @@ static QSize toNativeSizeConstrained(QSize dip, const QScreen *s) return dip; } +// Helper for checking if frame adjustment needs to be skipped +// NOTE: Unmaximized frameless windows will skip margins calculation +static bool shouldOmitFrameAdjustment(const Qt::WindowFlags flags, DWORD style) +{ + return flags.testFlag(Qt::FramelessWindowHint) && !(style & WS_MAXIMIZE); +} + +// Helper for checking if frame adjustment needs to be skipped +// NOTE: Unmaximized frameless windows will skip margins calculation +static bool shouldOmitFrameAdjustment(const Qt::WindowFlags flags, HWND hwnd) +{ + DWORD style = hwnd != nullptr ? DWORD(GetWindowLongPtr(hwnd, GWL_STYLE)) : 0; + return flags.testFlag(Qt::FramelessWindowHint) && !(style & WS_MAXIMIZE); +} + /*! \class QWindowsGeometryHint \brief Stores geometry constraints and provides utility functions. @@ -1019,7 +1044,7 @@ static QSize toNativeSizeConstrained(QSize dip, const QScreen *s) QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, DWORD style, DWORD exStyle) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; RECT rect = {0,0,0,0}; style &= ~DWORD(WS_OVERLAPPED); // Not permitted, see docs. @@ -1035,15 +1060,13 @@ QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, DWORD styl QMargins QWindowsGeometryHint::frameOnPrimaryScreen(const QWindow *w, HWND hwnd) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) - return {}; return frameOnPrimaryScreen(w, DWORD(GetWindowLongPtr(hwnd, GWL_STYLE)), DWORD(GetWindowLongPtr(hwnd, GWL_EXSTYLE))); } QMargins QWindowsGeometryHint::frame(const QWindow *w, DWORD style, DWORD exStyle, qreal dpi) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; RECT rect = {0,0,0,0}; style &= ~DWORD(WS_OVERLAPPED); // Not permitted, see docs. @@ -1061,7 +1084,7 @@ QMargins QWindowsGeometryHint::frame(const QWindow *w, DWORD style, DWORD exStyl QMargins QWindowsGeometryHint::frame(const QWindow *w, HWND hwnd, DWORD style, DWORD exStyle) { - if (!w->isTopLevel() || w->flags().testFlag(Qt::FramelessWindowHint)) + if (!w->isTopLevel() || shouldOmitFrameAdjustment(w->flags(), style)) return {}; if (QWindowsScreenManager::isSingleScreen()) return frameOnPrimaryScreen(w, style, exStyle); @@ -1075,8 +1098,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 +1106,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 +1359,8 @@ QWindowsForeignWindow::QWindowsForeignWindow(QWindow *window, HWND hwnd) , m_hwnd(hwnd) , m_topLevelStyle(0) { + if (QPlatformWindow::parent()) + setParent(QPlatformWindow::parent()); } void QWindowsForeignWindow::setParent(const QPlatformWindow *newParentWindow) @@ -1513,6 +1536,7 @@ QWindowsWindow::QWindowsWindow(QWindow *aWindow, const QWindowsWindowData &data) QWindowsWindow::~QWindowsWindow() { setFlag(WithinDestroy); + QWindowsThemeCache::clearThemeCache(m_data.hwnd); if (testFlag(TouchRegistered)) UnregisterTouchWindow(m_data.hwnd); destroyWindow(); @@ -1954,6 +1978,12 @@ void QWindowsWindow::handleCompositionSettingsChanged() } } +qreal QWindowsWindow::dpiRelativeScale(const UINT dpi) const +{ + return QHighDpiScaling::roundScaleFactor(qreal(dpi) / QWindowsScreen::baseDpi) / + QHighDpiScaling::roundScaleFactor(qreal(savedDpi()) / QWindowsScreen::baseDpi); +} + void QWindowsWindow::handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT *result) { // We want to keep QWindow's device independent size constant across the @@ -1961,10 +1991,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 +2002,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 +2013,17 @@ void QWindowsWindow::handleDpiScaledSize(WPARAM wParam, LPARAM lParam, LRESULT * void QWindowsWindow::handleDpiChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) { const UINT dpi = HIWORD(wParam); + const qreal scale = dpiRelativeScale(dpi); setSavedDpi(dpi); + QWindowsThemeCache::clearThemeCache(hwnd); + // Send screen change first, so that the new screen is set during any following resize checkForScreenChanged(QWindowsWindow::FromDpiChange); + if (!IsZoomed(hwnd)) + m_data.restoreGeometry.setSize(m_data.restoreGeometry.size() * scale); + // We get WM_DPICHANGED in one of two situations: // // 1. The DPI change is a "spontaneous" DPI change as a result of e.g. @@ -2010,13 +2046,22 @@ void QWindowsWindow::handleDpiChanged(HWND hwnd, WPARAM wParam, LPARAM lParam) SetWindowPos(hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, prcNewWindow->right - prcNewWindow->left, prcNewWindow->bottom - prcNewWindow->top, SWP_NOZORDER | SWP_NOACTIVATE); + // If the window does not have a frame, WM_MOVE and WM_SIZE won't be + // called which prevents the content from being scaled appropriately + // after a DPI change. + if (shouldOmitFrameAdjustment(m_data.flags, m_data.hwnd)) + handleGeometryChange(); } + + // Re-apply mask now that we have a new DPI, which have resulted in + // a new scale factor. + setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window())); } void QWindowsWindow::handleDpiChangedAfterParent(HWND hwnd) { const UINT dpi = GetDpiForWindow(hwnd); - const qreal scale = qreal(dpi) / qreal(savedDpi()); + const qreal scale = dpiRelativeScale(dpi); setSavedDpi(dpi); checkForScreenChanged(QWindowsWindow::FromDpiChange); @@ -2220,10 +2265,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 +2280,7 @@ void QWindowsWindow::handleGeometryChange() { const QRect previousGeometry = m_data.geometry; m_data.geometry = geometry_sys(); + updateFullFrameMargins(); QWindowSystemInterface::handleGeometryChange(window(), m_data.geometry); // QTBUG-32121: OpenGL/normal windows (with exception of ANGLE // which we no longer support in Qt 6) do not receive expose @@ -2252,6 +2298,9 @@ void QWindowsWindow::handleGeometryChange() if (testFlag(SynchronousGeometryChangeEvent)) QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); + if (!testFlag(ResizeMoveActive)) + updateRestoreGeometry(); + if (!wasSync) clearFlag(SynchronousGeometryChangeEvent); qCDebug(lcQpaEvents) << __FUNCTION__ << this << window() << m_data.geometry; @@ -2340,23 +2389,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 +2476,14 @@ void QWindowsWindow::handleWindowStateChange(Qt::WindowStates state) handleHidden(); QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); // Tell QQuickWindow to stop rendering now. } else { + if (state & Qt::WindowMaximized) { + WINDOWPLACEMENT windowPlacement{}; + windowPlacement.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(m_data.hwnd, &windowPlacement); + const RECT geometry = RECTfromQRect(m_data.restoreGeometry); + windowPlacement.rcNormalPosition = geometry; + SetWindowPlacement(m_data.hwnd, &windowPlacement); + } // QTBUG-17548: We send expose events when receiving WM_Paint, but for // layered windows and transient children, we won't receive any WM_Paint. QWindow *w = window(); @@ -2464,6 +2507,11 @@ void QWindowsWindow::handleWindowStateChange(Qt::WindowStates state) } } +void QWindowsWindow::updateRestoreGeometry() +{ + m_data.restoreGeometry = normalFrameGeometry(m_data.hwnd); +} + void QWindowsWindow::setWindowState(Qt::WindowStates state) { if (m_data.hwnd) { @@ -2531,26 +2579,26 @@ void QWindowsWindow::setWindowState_sys(Qt::WindowStates newState) if (testFlag(HasBorderInFullScreen)) newStyle |= WS_BORDER; setStyle(newStyle); - // Use geometry of QWindow::screen() within creation or the virtual screen the - // window is in (QTBUG-31166, QTBUG-30724). - const QScreen *screen = window()->screen(); - if (!screen) - screen = QGuiApplication::primaryScreen(); - const QRect r = screen ? QHighDpi::toNativePixels(screen->geometry(), window()) : m_savedFrameGeometry; - + const HMONITOR monitor = MonitorFromWindow(m_data.hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfoW(monitor, &monitorInfo); + const QRect screenGeometry(monitorInfo.rcMonitor.left, monitorInfo.rcMonitor.top, + monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left, + monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top); if (newState & Qt::WindowMinimized) { - setMinimizedGeometry(m_data.hwnd, r); + setMinimizedGeometry(m_data.hwnd, screenGeometry); if (stateChange & Qt::WindowMaximized) setRestoreMaximizedFlag(m_data.hwnd, newState & Qt::WindowMaximized); } else { const UINT swpf = SWP_FRAMECHANGED | SWP_NOACTIVATE; const bool wasSync = testFlag(SynchronousGeometryChangeEvent); setFlag(SynchronousGeometryChangeEvent); - SetWindowPos(m_data.hwnd, HWND_TOP, r.left(), r.top(), r.width(), r.height(), swpf); + SetWindowPos(m_data.hwnd, HWND_TOP, screenGeometry.left(), screenGeometry.top(), screenGeometry.width(), screenGeometry.height(), swpf); if (!wasSync) clearFlag(SynchronousGeometryChangeEvent); clearFlag(MaximizeToFullScreen); - QWindowSystemInterface::handleGeometryChange(window(), r); + QWindowSystemInterface::handleGeometryChange(window(), screenGeometry); QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents); } } else { @@ -2643,7 +2691,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 +2718,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 +2744,6 @@ bool QWindowsWindow::handleGeometryChangingMessage(MSG *message, const QWindow * return false; if (windowPos->flags & SWP_NOSIZE) return false; - const QRect suggestedFrameGeometry(windowPos->x, windowPos->y, - windowPos->cx, windowPos->cy); - const QRect suggestedGeometry = suggestedFrameGeometry - margins; const QRectF correctedGeometryF = QPlatformWindow::closestAcceptableGeometry(qWindow, suggestedGeometry); if (!correctedGeometryF.isValid()) return false; @@ -2713,7 +2765,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 +2784,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 +2836,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 +3287,6 @@ enum : WORD { DwmwaUseImmersiveDarkModeBefore20h1 = 19 }; -static bool queryDarkBorder(HWND hwnd) -{ - BOOL result = FALSE; - const bool ok = - SUCCEEDED(DwmGetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, &result, sizeof(result))) - || SUCCEEDED(DwmGetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeBefore20h1, &result, sizeof(result))); - if (!ok) - 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 +3302,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 +3430,6 @@ void QWindowsWindow::registerTouchWindow() qErrnoWarning("RegisterTouchWindow() failed for window '%s'.", qPrintable(window()->objectName())); } -void QWindowsWindow::aboutToMakeCurrent() -{ -#ifndef QT_NO_OPENGL - // For RasterGLSurface windows, that become OpenGL windows dynamically, it might be - // time to set up some GL specifics. This is particularly important for layered - // windows (WS_EX_LAYERED due to alpha > 0). - const bool isCompositing = qt_window_private(window())->compositing; - if (isCompositing != testFlag(Compositing)) { - if (isCompositing) - setFlag(Compositing); - else - clearFlag(Compositing); - - updateGLWindowSettings(window(), m_data.hwnd, m_data.flags, m_opacity); - } -#endif -} - void QWindowsWindow::setHasBorderInFullScreenStatic(QWindow *window, bool border) { if (QPlatformWindow *handle = window->handle()) diff --git a/src/plugins/platforms/windows/qwindowswindow.h b/src/plugins/platforms/windows/qwindowswindow.h index 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..5892493281 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.cpp @@ -5,6 +5,7 @@ #if QT_CONFIG(accessibility) #include "qwindowsuiaaccessibility.h" +#include "qwindowsuiautomation.h" #include "qwindowsuiamainprovider.h" #include "qwindowsuiautils.h" @@ -14,13 +15,13 @@ #include <QtGui/private/qguiapplication_p.h> #include <QtCore/qt_windows.h> #include <qpa/qplatformintegration.h> -#include <QtGui/private/qwindowsuiawrapper_p.h> #include <QtCore/private/qwinregistry_p.h> QT_BEGIN_NAMESPACE using namespace QWindowsUiAutomation; +using namespace Qt::Literals::StringLiterals; bool QWindowsUiaAccessibility::m_accessibleActive = false; @@ -46,7 +47,7 @@ bool QWindowsUiaAccessibility::handleWmGetObject(HWND hwnd, WPARAM wParam, LPARA if (QWindow *window = QWindowsContext::instance()->findWindow(hwnd)) { if (QAccessibleInterface *accessible = window->accessibleRoot()) { QWindowsUiaMainProvider *provider = QWindowsUiaMainProvider::providerForAccessible(accessible); - *lResult = QWindowsUiaWrapper::instance()->returnRawElementProvider(hwnd, wParam, lParam, provider); + *lResult = UiaReturnRawElementProvider(hwnd, wParam, lParam, provider); return true; } } @@ -78,8 +79,8 @@ static QString alertSound(const QObject *object) static QString soundFileName(const QString &soundName) { - const QString key = QStringLiteral("AppEvents\\Schemes\\Apps\\.Default\\") - + soundName + QStringLiteral("\\.Current"); + const QString key = "AppEvents\\Schemes\\Apps\\.Default\\"_L1 + + soundName + "\\.Current"_L1; return QWinRegistryKey(HKEY_CURRENT_USER, key).stringValue(L""); } @@ -97,11 +98,7 @@ void QWindowsUiaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event if (!event) return; - // Ignore events sent before the first UI Automation - // request or while QAccessible is being activated. - if (!m_accessibleActive) - return; - + // Always handle system sound events switch (event->type()) { case QAccessible::PopupMenuStart: playSystemSound(QStringLiteral("MenuPopup")); @@ -116,19 +113,23 @@ void QWindowsUiaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event break; } - QAccessibleInterface *accessible = event->accessibleInterface(); - if (!isActive() || !accessible || !accessible->isValid()) + // Ignore events sent before the first UI Automation + // request or while QAccessible is being activated. + if (!m_accessibleActive) return; - // Ensures QWindowsUiaWrapper is properly initialized. - if (!QWindowsUiaWrapper::instance()->ready()) + QAccessibleInterface *accessible = event->accessibleInterface(); + if (!isActive() || !accessible || !accessible->isValid()) return; // No need to do anything when nobody is listening. - if (!QWindowsUiaWrapper::instance()->clientsAreListening()) + if (!UiaClientsAreListening()) return; switch (event->type()) { + case QAccessible::Announcement: + QWindowsUiaMainProvider::raiseNotification(static_cast<QAccessibleAnnouncementEvent *>(event)); + break; case QAccessible::Focus: QWindowsUiaMainProvider::notifyFocusChange(event); break; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h index 1813bb4d89..2e8ee585da 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaaccessibility.h @@ -7,6 +7,7 @@ #include <QtGui/qtguiglobal.h> #if QT_CONFIG(accessibility) +#include <QtCore/qt_windows.h> #include "qwindowscontext.h" #include <qpa/qplatformaccessibility.h> diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h index c899b4096e..032679ab10 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiabaseprovider.h @@ -10,8 +10,8 @@ #include <QtGui/qaccessible.h> #include <QtCore/qpointer.h> -#include <qwindowscombase.h> -#include <QtGui/private/qwindowsuiawrapper_p.h> +#include "qwindowsuiautomation.h" +#include <QtCore/private/qcomobject_p.h> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h index 49b37ba295..b384eb521c 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaexpandcollapseprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Expand/Collapse control pattern provider. Used for menu items with submenus. class QWindowsUiaExpandCollapseProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IExpandCollapseProvider> + public QComObject<IExpandCollapseProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaExpandCollapseProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h index b501616966..289a867869 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiagriditemprovider.h @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Grid Item control pattern provider. Used by items within a table/tree. class QWindowsUiaGridItemProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IGridItemProvider> + public QComObject<IGridItemProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaGridItemProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h index b905133f6b..d33bbd0429 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiagridprovider.h @@ -12,8 +12,7 @@ QT_BEGIN_NAMESPACE // Implements the Grid control pattern provider. Used by tables/trees. -class QWindowsUiaGridProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IGridProvider> +class QWindowsUiaGridProvider : public QWindowsUiaBaseProvider, public QComObject<IGridProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaGridProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h index eca7e73039..ec006c673e 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiainvokeprovider.h @@ -12,8 +12,7 @@ QT_BEGIN_NAMESPACE // Implements the Invoke control pattern provider. -class QWindowsUiaInvokeProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IInvokeProvider> +class QWindowsUiaInvokeProvider : public QWindowsUiaBaseProvider, public QComObject<IInvokeProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaInvokeProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp index 16f0a3f9a6..be88ab4ae8 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp @@ -18,7 +18,6 @@ #include "qwindowsuiagriditemprovider.h" #include "qwindowsuiawindowprovider.h" #include "qwindowsuiaexpandcollapseprovider.h" -#include "qwindowscombase.h" #include "qwindowscontext.h" #include "qwindowsuiautils.h" #include "qwindowsuiaprovidercache.h" @@ -27,6 +26,7 @@ #include <QtGui/qaccessible.h> #include <QtGui/qguiapplication.h> #include <QtGui/qwindow.h> +#include <qpa/qplatforminputcontextfactory_p.h> #if !defined(Q_CC_BOR) && !defined (Q_CC_GNU) #include <comdef.h> @@ -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,48 +191,59 @@ void QWindowsUiaMainProvider::notifyTextChange(QAccessibleEvent *event) if (accessible->textInterface()) { if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { if (event->type() == QAccessible::TextSelectionChanged) { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); } else if (event->type() == QAccessible::TextCaretMoved) { if (!accessible->state().readOnly) { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_Text_TextSelectionChangedEventId); } } else { - QWindowsUiaWrapper::instance()->raiseAutomationEvent(provider, UIA_Text_TextChangedEventId); + UiaRaiseAutomationEvent(provider, UIA_Text_TextChangedEventId); } } } } } -HRESULT STDMETHODCALLTYPE QWindowsUiaMainProvider::QueryInterface(REFIID iid, LPVOID *iface) +void QWindowsUiaMainProvider::raiseNotification(QAccessibleAnnouncementEvent *event) { - if (!iface) - return E_INVALIDARG; - *iface = nullptr; - - QAccessibleInterface *accessible = accessibleInterface(); + if (QAccessibleInterface *accessible = event->accessibleInterface()) { + if (QWindowsUiaMainProvider *provider = providerForAccessible(accessible)) { + BSTR message = bStrFromQString(event->message()); + QAccessible::AnnouncementPriority prio = event->priority(); + NotificationProcessing processing = (prio == QAccessible::AnnouncementPriority::Assertive) + ? NotificationProcessing_ImportantAll + : NotificationProcessing_All; + BSTR activityId = bStrFromQString(QString::fromLatin1("")); + UiaRaiseNotificationEvent(provider, NotificationKind_Other, processing, message, activityId); - 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; + ::SysFreeString(message); + ::SysFreeString(activityId); + } + } } -ULONG QWindowsUiaMainProvider::AddRef() +HRESULT STDMETHODCALLTYPE QWindowsUiaMainProvider::QueryInterface(REFIID iid, LPVOID *iface) { - return ++m_ref; + HRESULT result = QComObject::QueryInterface(iid, iface); + + if (SUCCEEDED(result) && iid == __uuidof(IRawElementProviderFragmentRoot)) { + QAccessibleInterface *accessible = accessibleInterface(); + if (accessible && hwndForAccessible(accessible)) { + result = S_OK; + } else { + result = E_NOINTERFACE; + iface = nullptr; + } + } + + return result; } ULONG STDMETHODCALLTYPE QWindowsUiaMainProvider::Release() { 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 +298,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 +366,115 @@ HRESULT QWindowsUiaMainProvider::GetPatternProvider(PATTERNID idPattern, IUnknow return S_OK; } +void QWindowsUiaMainProvider::fillVariantArrayForRelation(QAccessibleInterface* accessible, + QAccessible::Relation relation, VARIANT *pRetVal) +{ + Q_ASSERT(accessible); + + typedef QPair<QAccessibleInterface*, QAccessible::Relation> RelationPair; + const QList<RelationPair> relationInterfaces = accessible->relations(relation); + if (relationInterfaces.empty()) + return; + + SAFEARRAY *elements = SafeArrayCreateVector(VT_UNKNOWN, 0, relationInterfaces.size()); + for (LONG i = 0; i < relationInterfaces.size(); ++i) { + if (QWindowsUiaMainProvider *childProvider = QWindowsUiaMainProvider::providerForAccessible(relationInterfaces.at(i).first)) { + SafeArrayPutElement(elements, &i, static_cast<IRawElementProviderSimple*>(childProvider)); + childProvider->Release(); + } + } + + pRetVal->vt = VT_UNKNOWN | VT_ARRAY; + pRetVal->parray = elements; +} + +void QWindowsUiaMainProvider::setAriaProperties(QAccessibleInterface *accessible, VARIANT *pRetVal) +{ + Q_ASSERT(accessible); + + QAccessibleAttributesInterface *attributesIface = accessible->attributesInterface(); + if (!attributesIface) + return; + + QString ariaString; + const QList<QAccessible::Attribute> attrKeys = attributesIface->attributeKeys(); + for (qsizetype i = 0; i < attrKeys.size(); ++i) { + if (i != 0) + ariaString += QStringLiteral(";"); + const QAccessible::Attribute key = attrKeys.at(i); + const QVariant value = attributesIface->attributeValue(key); + // see "Core Accessibility API Mappings" spec: https://www.w3.org/TR/core-aam-1.2/ + switch (key) { + case QAccessible::Attribute::Custom: + { + // forward custom attributes as-is + Q_ASSERT((value.canConvert<QHash<QString, QString>>())); + const QHash<QString, QString> attrMap = value.value<QHash<QString, QString>>(); + for (auto [name, val] : attrMap.asKeyValueRange()) { + if (name != *attrMap.keyBegin()) + ariaString += QStringLiteral(";"); + ariaString += name + QStringLiteral("=") + val; + } + break; + } + case QAccessible::Attribute::Level: + Q_ASSERT(value.canConvert<int>()); + ariaString += QStringLiteral("level=") + QString::number(value.toInt()); + break; + default: + break; + } + } + + setVariantString(ariaString, pRetVal); +} + +void QWindowsUiaMainProvider::setStyle(QAccessibleInterface *accessible, VARIANT *pRetVal) +{ + Q_ASSERT(accessible); + + QAccessibleAttributesInterface *attributesIface = accessible->attributesInterface(); + if (!attributesIface) + return; + + // currently, only heading styles are implemented here + if (accessible->role() != QAccessible::Role::Heading) + return; + + const QVariant levelVariant = attributesIface->attributeValue(QAccessible::Attribute::Level); + if (!levelVariant.isValid()) + return; + + Q_ASSERT(levelVariant.canConvert<int>()); + // UIA only has styles for heading levels 1-9 + const int level = levelVariant.toInt(); + if (level < 1 || level > 9) + return; + + const int styleId = styleIdForHeadingLevel(level); + setVariantI4(styleId, pRetVal); +} + +int QWindowsUiaMainProvider::styleIdForHeadingLevel(int headingLevel) +{ + // only heading levels 1-9 have a corresponding UIA style ID + Q_ASSERT(headingLevel > 0 && headingLevel <= 9); + + static constexpr int styles[] = { + StyleId_Heading1, + StyleId_Heading2, + StyleId_Heading3, + StyleId_Heading4, + StyleId_Heading5, + StyleId_Heading6, + StyleId_Heading7, + StyleId_Heading8, + StyleId_Heading9, + }; + + return styles[headingLevel - 1]; +} + HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pRetVal) { qCDebug(lcQpaUiAutomation) << __FUNCTION__ << idProp; @@ -392,6 +498,9 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR // Accelerator key. setVariantString(accessible->text(QAccessible::Accelerator), pRetVal); break; + case UIA_AriaPropertiesPropertyId: + setAriaProperties(accessible, pRetVal); + break; case UIA_AutomationIdPropertyId: // Automation ID, which can be used by tools to select a specific control in the UI. setVariantString(automationIdForAccessible(accessible), pRetVal); @@ -403,6 +512,15 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR setVariantString(className, pRetVal); } break; + case UIA_DescribedByPropertyId: + fillVariantArrayForRelation(accessible, QAccessible::DescriptionFor, pRetVal); + break; + case UIA_FlowsFromPropertyId: + fillVariantArrayForRelation(accessible, QAccessible::FlowsTo, pRetVal); + break; + case UIA_FlowsToPropertyId: + fillVariantArrayForRelation(accessible, QAccessible::FlowsFrom, pRetVal); + break; case UIA_FrameworkIdPropertyId: setVariantString(QStringLiteral("Qt"), pRetVal); break; @@ -416,7 +534,7 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR // The native OSK should be disabled if the Qt OSK is in use, // or if disabled via application attribute. - static bool imModuleEmpty = qEnvironmentVariableIsEmpty("QT_IM_MODULE"); + static bool imModuleEmpty = QPlatformInputContextFactory::requested().isEmpty(); bool nativeVKDisabled = QCoreApplication::testAttribute(Qt::AA_DisableNativeVirtualKeyboard); // If we want to disable the native OSK auto-showing @@ -483,6 +601,9 @@ HRESULT QWindowsUiaMainProvider::GetPropertyValue(PROPERTYID idProp, VARIANT *pR setVariantString(name, pRetVal); break; } + case UIA_StyleIdAttributeId: + setStyle(accessible, pRetVal); + break; default: break; } @@ -519,7 +640,7 @@ HRESULT QWindowsUiaMainProvider::get_HostRawElementProvider(IRawElementProviderS // Returns a host provider only for controls associated with a native window handle. Others should return NULL. if (QAccessibleInterface *accessible = accessibleInterface()) { if (HWND hwnd = hwndForAccessible(accessible)) { - return QWindowsUiaWrapper::instance()->hostProviderFromHwnd(hwnd, pRetVal); + return UiaHostProviderFromHwnd(hwnd, pRetVal); } } return S_OK; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h index fb5069f620..dafe877974 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); @@ -36,10 +34,10 @@ public: static void notifyNameChange(QAccessibleEvent *event); static void notifySelectionChange(QAccessibleEvent *event); static void notifyTextChange(QAccessibleEvent *event); + static void raiseNotification(QAccessibleAnnouncementEvent *event); // IUnknown HRESULT STDMETHODCALLTYPE QueryInterface(REFIID id, LPVOID *iface) override; - ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE Release() override; // IRawElementProviderSimple methods @@ -62,7 +60,11 @@ public: private: QString automationIdForAccessible(const QAccessibleInterface *accessible); - ULONG m_ref; + static void fillVariantArrayForRelation(QAccessibleInterface *accessible, QAccessible::Relation relation, VARIANT *pRetVal); + static void setAriaProperties(QAccessibleInterface *accessible, VARIANT *pRetVal); + static void setStyle(QAccessibleInterface *accessible, VARIANT *pRetVal); + /** Returns the UIA style ID for a heading level from 1 to 9. */ + static int styleIdForHeadingLevel(int headingLevel); static QMutex m_mutex; }; 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..6954a881d0 --- /dev/null +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/qtguiglobal.h> +#if QT_CONFIG(accessibility) + +#include "qwindowsuiautomation.h" + +#if defined(__MINGW32__) || defined(__MINGW64__) + +template<typename T, typename... TArg> +struct winapi_func +{ + using func_t = T(WINAPI*)(TArg...); + const func_t func; + const T error_value; +#ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + winapi_func(const char *lib_name, const char *func_name, func_t func_proto, + T error_value = T(__HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND))) : + func(reinterpret_cast<func_t>(GetProcAddress(LoadLibraryA(lib_name), func_name))), + error_value(error_value) + { + std::ignore = func_proto; + } +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + T invoke(TArg... arg) + { + if (!func) + return error_value; + return func(arg...); + } +}; + +#define FN(fn) #fn,fn + +BOOL WINAPI UiaClientsAreListening() +{ + static auto func = winapi_func("uiautomationcore", FN(UiaClientsAreListening), BOOL(false)); + return func.invoke(); +} + +LRESULT WINAPI UiaReturnRawElementProvider( + HWND hwnd, WPARAM wParam, LPARAM lParam, IRawElementProviderSimple *el) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaReturnRawElementProvider)); + return func.invoke(hwnd, wParam, lParam, el); +} + +HRESULT WINAPI UiaHostProviderFromHwnd(HWND hwnd, IRawElementProviderSimple **ppProvider) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaHostProviderFromHwnd)); + return func.invoke(hwnd, ppProvider); +} + +HRESULT WINAPI UiaRaiseAutomationPropertyChangedEvent( + IRawElementProviderSimple *pProvider, PROPERTYID id, VARIANT oldValue, VARIANT newValue) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaRaiseAutomationPropertyChangedEvent)); + return func.invoke(pProvider, id, oldValue, newValue); +} + +HRESULT WINAPI UiaRaiseAutomationEvent(IRawElementProviderSimple *pProvider, EVENTID id) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaRaiseAutomationEvent)); + return func.invoke(pProvider, id); +} + +HRESULT WINAPI UiaRaiseNotificationEvent( + IRawElementProviderSimple *pProvider, NotificationKind notificationKind, + NotificationProcessing notificationProcessing, BSTR displayString, BSTR activityId) +{ + static auto func = winapi_func("uiautomationcore", FN(UiaRaiseNotificationEvent)); + return func.invoke(pProvider, notificationKind, notificationProcessing, displayString, activityId); +} + +#endif // defined(__MINGW32__) || defined(__MINGW64__) + +#endif // QT_CONFIG(accessibility) diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h new file mode 100644 index 0000000000..4eb37bafa0 --- /dev/null +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiautomation.h @@ -0,0 +1,81 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSUIAUTOMATION_H +#define QWINDOWSUIAUTOMATION_H + +#include <QtGui/qtguiglobal.h> +#if QT_CONFIG(accessibility) + +#include <uiautomation.h> + +#if defined(__MINGW32__) || defined(__MINGW64__) + +#define UIA_SelectionPattern2Id 10034 +#define UIA_IsReadOnlyAttributeId 40015 +#define UIA_StrikethroughStyleAttributeId 40026 +#define UIA_StyleIdAttributeId 40034 +#define UIA_CaretPositionAttributeId 40038 + +#define StyleId_Heading1 70001 +#define StyleId_Heading2 70002 +#define StyleId_Heading3 70003 +#define StyleId_Heading4 70004 +#define StyleId_Heading5 70005 +#define StyleId_Heading6 70006 +#define StyleId_Heading7 70007 +#define StyleId_Heading8 70008 +#define StyleId_Heading9 70009 + +enum CaretPosition { + CaretPosition_Unknown = 0, + CaretPosition_EndOfLine = 1, + CaretPosition_BeginningOfLine = 2 +}; + +enum TextDecorationLineStyle { + TextDecorationLineStyle_None = 0, + TextDecorationLineStyle_Single = 1, + TextDecorationLineStyle_WordsOnly = 2, + TextDecorationLineStyle_Double = 3, + TextDecorationLineStyle_Dot = 4, + TextDecorationLineStyle_Dash = 5, + TextDecorationLineStyle_DashDot = 6, + TextDecorationLineStyle_DashDotDot = 7, + TextDecorationLineStyle_Wavy = 8, + TextDecorationLineStyle_ThickSingle = 9, + TextDecorationLineStyle_DoubleWavy = 11, + TextDecorationLineStyle_ThickWavy = 12, + TextDecorationLineStyle_LongDash = 13, + TextDecorationLineStyle_ThickDash = 14, + TextDecorationLineStyle_ThickDashDot = 15, + TextDecorationLineStyle_ThickDashDotDot = 16, + TextDecorationLineStyle_ThickDot = 17, + TextDecorationLineStyle_ThickLongDash = 18, + TextDecorationLineStyle_Other = -1 +}; + +BOOL WINAPI UiaClientsAreListening(); + +#ifndef __ISelectionProvider2_INTERFACE_DEFINED__ +#define __ISelectionProvider2_INTERFACE_DEFINED__ +DEFINE_GUID(IID_ISelectionProvider2, 0x14f68475, 0xee1c, 0x44f6, 0xa8, 0x69, 0xd2, 0x39, 0x38, 0x1f, 0x0f, 0xe7); +MIDL_INTERFACE("14f68475-ee1c-44f6-a869-d239381f0fe7") +ISelectionProvider2 : public ISelectionProvider +{ +public: + virtual HRESULT STDMETHODCALLTYPE get_FirstSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **retVal) = 0; + virtual HRESULT STDMETHODCALLTYPE get_LastSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **retVal) = 0; + virtual HRESULT STDMETHODCALLTYPE get_CurrentSelectedItem(__RPC__deref_out_opt IRawElementProviderSimple **retVal) = 0; + virtual HRESULT STDMETHODCALLTYPE get_ItemCount(__RPC__out int *retVal) = 0; +}; +#ifdef __CRT_UUID_DECL +__CRT_UUID_DECL(ISelectionProvider2, 0x14f68475, 0xee1c, 0x44f6, 0xa8, 0x69, 0xd2, 0x39, 0x38, 0x1f, 0x0f, 0xe7) +#endif +#endif // __ISelectionProvider2_INTERFACE_DEFINED__ + +#endif // defined(__MINGW32__) || defined(__MINGW64__) + +#endif // QT_CONFIG(accessibility) + +#endif // QWINDOWSUIAUTOMATION_H diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h index 2f52019d33..8c0a6b8ee7 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiavalueprovider.h @@ -13,8 +13,7 @@ QT_BEGIN_NAMESPACE // Implements the Value control pattern provider. // Supported for all controls that can return text(QAccessible::Value). -class QWindowsUiaValueProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IValueProvider> +class QWindowsUiaValueProvider : public QWindowsUiaBaseProvider, public QComObject<IValueProvider> { Q_DISABLE_COPY_MOVE(QWindowsUiaValueProvider) public: diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h index e277b3f5c5..89a1655232 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiawindowprovider.h @@ -11,8 +11,7 @@ QT_BEGIN_NAMESPACE -class QWindowsUiaWindowProvider : public QWindowsUiaBaseProvider, - public QWindowsComBase<IWindowProvider> +class QWindowsUiaWindowProvider : public QWindowsUiaBaseProvider, public QComObject<IWindowProvider> { Q_DISABLE_COPY(QWindowsUiaWindowProvider) public: diff --git a/src/plugins/platforms/xcb/CMakeLists.txt b/src/plugins/platforms/xcb/CMakeLists.txt index 17e723d607..96758e7181 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 @@ -165,7 +164,7 @@ endif() qt_internal_add_plugin(QXcbIntegrationPlugin OUTPUT_NAME qxcb PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES xcb + DEFAULT_IF "xcb" IN_LIST QT_QPA_PLATFORMS SOURCES qxcbmain.cpp DEFINES @@ -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/qxcbscreen.cpp b/src/plugins/platforms/xcb/qxcbscreen.cpp index 06f4b66edb..0ce337726e 100644 --- a/src/plugins/platforms/xcb/qxcbscreen.cpp +++ b/src/plugins/platforms/xcb/qxcbscreen.cpp @@ -23,6 +23,7 @@ QXcbVirtualDesktop::QXcbVirtualDesktop(QXcbConnection *connection, xcb_screen_t : QXcbObject(connection) , m_screen(screen) , m_number(number) + , m_xSettings(new QXcbXSettings(this)) { const QByteArray cmAtomName = "_NET_WM_CM_S" + QByteArray::number(m_number); m_net_wm_cm_atom = connection->internAtom(cmAtomName.constData()); @@ -129,10 +130,6 @@ void QXcbVirtualDesktop::setPrimaryScreen(QPlatformScreen *s) QXcbXSettings *QXcbVirtualDesktop::xSettings() const { - if (!m_xSettings) { - QXcbVirtualDesktop *self = const_cast<QXcbVirtualDesktop *>(this); - self->m_xSettings = new QXcbXSettings(self); - } return m_xSettings; } 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; } |