diff options
Diffstat (limited to 'src/plugins/platforms/ios')
43 files changed, 1405 insertions, 618 deletions
diff --git a/src/plugins/platforms/ios/CMakeLists.txt b/src/plugins/platforms/ios/CMakeLists.txt index d67239fee5..4cc3efc91e 100644 --- a/src/plugins/platforms/ios/CMakeLists.txt +++ b/src/plugins/platforms/ios/CMakeLists.txt @@ -1,7 +1,5 @@ # Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -# Generated from ios.pro. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QIOSIntegrationPlugin Plugin: @@ -9,13 +7,13 @@ qt_internal_add_plugin(QIOSIntegrationPlugin OUTPUT_NAME qios - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES ios # special case + STATIC # Force static, even in shared builds + DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES ios PLUGIN_TYPE platforms SOURCES plugin.mm qiosapplicationdelegate.h qiosapplicationdelegate.mm qiosapplicationstate.h qiosapplicationstate.mm - qioscontext.h qioscontext.mm qioseventdispatcher.h qioseventdispatcher.mm qiosglobal.h qiosglobal.mm qiosinputcontext.h qiosinputcontext.mm @@ -29,43 +27,62 @@ qt_internal_add_plugin(QIOSIntegrationPlugin qioswindow.h qioswindow.mm quiaccessibilityelement.h quiaccessibilityelement.mm quiview.h quiview.mm + quiwindow.mm quiwindow.h uistrings_p.h uistrings.cpp + NO_PCH_SOURCES + qioscontext.mm # undef QT_NO_FOREACH + qiosintegration.mm # undef QT_NO_FOREACH + qiosplatformaccessibility.mm # undef QT_NO_FOREACH + qiosscreen.mm # undef QT_NO_FOREACH LIBRARIES ${FWAudioToolbox} ${FWFoundation} ${FWMetal} ${FWQuartzCore} ${FWUIKit} + ${FWCoreGraphics} Qt::CorePrivate Qt::GuiPrivate ) -# special case begin qt_disable_apple_app_extension_api_only(QIOSIntegrationPlugin) -# special case end -#### Keys ignored in scope 2:.:.:kernel.pro:<TRUE>: -# OTHER_FILES = "quiview_textinput.mm" "quiview_accessibility.mm" ## Scopes: ##################################################################### +qt_internal_find_apple_system_framework(FWUniformTypeIdentifiers UniformTypeIdentifiers) qt_internal_extend_target(QIOSIntegrationPlugin CONDITION QT_FEATURE_opengl + SOURCES + qioscontext.h qioscontext.mm LIBRARIES Qt::OpenGLPrivate ) -qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION QT_FEATURE_clipboard SOURCES qiosclipboard.h qiosclipboard.mm - qiosdocumentpickercontroller.h qiosdocumentpickercontroller.mm +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS + SOURCES qiosfiledialog.h qiosfiledialog.mm - qiosmenu.h qiosmenu.mm + qiosdocumentpickercontroller.h qiosdocumentpickercontroller.mm + LIBRARIES + ${FWUniformTypeIdentifiers} + ${FWPhotos} +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT TVOS + SOURCES + qioscolordialog.h qioscolordialog.mm + qiosfontdialog.h qiosfontdialog.mm qiosmessagedialog.h qiosmessagedialog.mm +) + +qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT (TVOS OR VISIONOS) + SOURCES + qiosmenu.h qiosmenu.mm qiostextinputoverlay.h qiostextinputoverlay.mm - LIBRARIES - ${FWAssetsLibrary} ) -#### Keys ignored in scope 6:.:.:kernel.pro:NOT TARGET___equals____ss_QT_DEFAULT_QPA_PLUGIN: -# PLUGIN_EXTENDS = "-" add_subdirectory(optional) diff --git a/src/plugins/platforms/ios/optional/CMakeLists.txt b/src/plugins/platforms/ios/optional/CMakeLists.txt index 72f61c7473..a01d7a6441 100644 --- a/src/plugins/platforms/ios/optional/CMakeLists.txt +++ b/src/plugins/platforms/ios/optional/CMakeLists.txt @@ -1,7 +1,5 @@ # Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -# Generated from optional.pro. +# SPDX-License-Identifier: BSD-3-Clause if(IOS) add_subdirectory(nsphotolibrarysupport) diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt index 45f6e588d5..663878bde7 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/CMakeLists.txt @@ -1,7 +1,5 @@ # Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -# Generated from nsphotolibrarysupport.pro. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QIosOptionalPlugin_NSPhotoLibrary Plugin: @@ -9,6 +7,7 @@ qt_internal_add_plugin(QIosOptionalPlugin_NSPhotoLibraryPlugin OUTPUT_NAME qiosnsphotolibrarysupport + STATIC # Force static, even in shared builds PLUGIN_TYPE platforms/darwin CLASS_NAME QIosOptionalPlugin_NSPhotoLibrary DEFAULT_IF FALSE @@ -26,17 +25,7 @@ qt_internal_add_plugin(QIosOptionalPlugin_NSPhotoLibraryPlugin Qt::GuiPrivate ) -# special case begin set_target_properties(QIosOptionalPlugin_NSPhotoLibraryPlugin PROPERTIES DISABLE_PRECOMPILE_HEADERS ON ) -# special case end - - -#### Keys ignored in scope 1:.:.:nsphotolibrarysupport.pro:<TRUE>: -# OTHER_FILES = "plugin.json" -# PLUGIN_EXTENDS = "-" - -## Scopes: -##################################################################### diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h index 90b62af56d..0ad54a9e11 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.h @@ -29,8 +29,8 @@ public: void setFileName(const QString &file) override; #ifndef QT_NO_FILESYSTEMITERATOR - Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override; - Iterator *endEntryList() override; + IteratorUniquePtr beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) override; #endif void setError(QFile::FileError error, const QString &str) { QAbstractFileEngine::setError(error, str); } diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm index 0ca911f68b..f7e112ab81 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileengineassetslibrary.mm @@ -11,6 +11,8 @@ #include <QtCore/qurl.h> #include <QtCore/qset.h> #include <QtCore/qthreadstorage.h> +#include <QtCore/qfileselector.h> +#include <QtCore/qpointer.h> QT_BEGIN_NAMESPACE @@ -256,8 +258,8 @@ public: QIOSAssetEnumerator *m_enumerator; QIOSFileEngineIteratorAssetsLibrary( - QDir::Filters filters, const QStringList &nameFilters) - : QAbstractFileEngineIterator(filters, nameFilters) + const QString &path, QDir::Filters filters, const QStringList &nameFilters) + : QAbstractFileEngineIterator(path, filters, nameFilters) , m_enumerator(new QIOSAssetEnumerator([[[ALAssetsLibrary alloc] init] autorelease], ALAssetsGroupAll)) { } @@ -268,8 +270,11 @@ public: g_iteratorCurrentUrl.setLocalData(QString()); } - QString next() override + bool advance() override { + if (!m_enumerator->hasNext()) + return false; + // Cache the URL that we are about to return, since QDir will immediately create a // new file engine on the file and ask if it exists. Unless we do this, we end up // creating a new ALAsset just to verify its existence, which will be especially @@ -277,12 +282,7 @@ public: ALAsset *asset = m_enumerator->next(); QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString(); g_iteratorCurrentUrl.setLocalData(url); - return url; - } - - bool hasNext() const override - { - return m_enumerator->hasNext(); + return true; } QString currentFileName() const override @@ -344,6 +344,17 @@ QAbstractFileEngine::FileFlags QIOSFileEngineAssetsLibrary::fileFlags(QAbstractF { QAbstractFileEngine::FileFlags flags; const bool isDir = (m_assetUrl == "assets-library://"_L1); + if (!isDir) { + static const QFileSelector fileSelector; + static const auto selectors = fileSelector.allSelectors(); + if (m_assetUrl.startsWith("assets-library://"_L1)) { + for (const auto &selector : selectors) { + if (m_assetUrl.endsWith(selector)) + return flags; + } + } + } + const bool exists = isDir || m_assetUrl == g_iteratorCurrentUrl.localData() || loadAsset(); if (!exists) @@ -427,15 +438,11 @@ void QIOSFileEngineAssetsLibrary::setFileName(const QString &file) #ifndef QT_NO_FILESYSTEMITERATOR -QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::beginEntryList( - QDir::Filters filters, const QStringList &filterNames) -{ - return new QIOSFileEngineIteratorAssetsLibrary(filters, filterNames); -} - -QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::endEntryList() +QAbstractFileEngine::IteratorUniquePtr +QIOSFileEngineAssetsLibrary::beginEntryList( + const QString &path, QDir::Filters filters, const QStringList &filterNames) { - return 0; + return std::make_unique<QIOSFileEngineIteratorAssetsLibrary>(path, filters, filterNames); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h index caa852df9f..dfffbb8990 100644 --- a/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h +++ b/src/plugins/platforms/ios/optional/nsphotolibrarysupport/qiosfileenginefactory.h @@ -12,19 +12,22 @@ QT_BEGIN_NAMESPACE class QIOSFileEngineFactory : public QAbstractFileEngineHandler { + Q_DISABLE_COPY_MOVE(QIOSFileEngineFactory) public: - QAbstractFileEngine* create(const QString &fileName) const + QIOSFileEngineFactory() = default; + + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const { - static QLatin1StringView assetsScheme("assets-library:"); + Q_CONSTINIT static QLatin1StringView assetsScheme("assets-library:"); #ifndef Q_OS_TVOS if (fileName.toLower().startsWith(assetsScheme)) - return new QIOSFileEngineAssetsLibrary(fileName); + return std::make_unique<QIOSFileEngineAssetsLibrary>(fileName); #else Q_UNUSED(fileName); #endif - return 0; + return {}; } }; diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm index a017fef457..c6e5a83874 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm @@ -3,15 +3,21 @@ #include "qiosapplicationdelegate.h" +#include "qiosglobal.h" #include "qiosintegration.h" #include "qiosservices.h" #include "qiosviewcontroller.h" #include "qioswindow.h" +#include "qiosscreen.h" +#include "quiwindow.h" #include <qpa/qplatformintegration.h> #include <QtCore/QtCore> +@interface QIOSWindowSceneDelegate : NSObject<UIWindowSceneDelegate> +@end + @implementation QIOSApplicationDelegate - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *restorableObjects))restorationHandler @@ -50,5 +56,44 @@ return iosServices->handleUrl(QUrl::fromNSURL(url)); } +- (UISceneConfiguration *)application:(UIApplication *)application + configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession + options:(UISceneConnectionOptions *)options +{ + qCDebug(lcQpaWindowScene) << "Configuring scene for" << connectingSceneSession + << "with options" << options; + + auto *sceneConfig = connectingSceneSession.configuration; + sceneConfig.delegateClass = QIOSWindowSceneDelegate.class; + return sceneConfig; +} + @end +@implementation QIOSWindowSceneDelegate + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions +{ + qCDebug(lcQpaWindowScene) << "Connecting" << scene << "to" << session; + + Q_ASSERT([scene isKindOfClass:UIWindowScene.class]); + UIWindowScene *windowScene = static_cast<UIWindowScene*>(scene); + + QUIWindow *window = [[QUIWindow alloc] initWithWindowScene:windowScene]; + + QIOSScreen *screen = [&]{ + for (auto *screen : qGuiApp->screens()) { + auto *platformScreen = static_cast<QIOSScreen*>(screen->handle()); +#if !defined(Q_OS_VISIONOS) + if (platformScreen->uiScreen() == windowScene.screen) +#endif + return platformScreen; + } + Q_UNREACHABLE(); + }(); + + window.rootViewController = [[[QIOSViewController alloc] + initWithWindow:window andScreen:screen] autorelease]; +} + +@end diff --git a/src/plugins/platforms/ios/qiosclipboard.mm b/src/plugins/platforms/ios/qiosclipboard.mm index 554c3ca118..de8ab69dff 100644 --- a/src/plugins/platforms/ios/qiosclipboard.mm +++ b/src/plugins/platforms/ios/qiosclipboard.mm @@ -6,22 +6,11 @@ #ifndef QT_NO_CLIPBOARD #include <QtCore/qurl.h> -#include <QtGui/private/qmacmime_p.h> +#include <QtGui/private/qmacmimeregistry_p.h> +#include <QtGui/qutimimeconverter.h> #include <QtCore/QMimeData> #include <QtGui/QGuiApplication> -@interface UIPasteboard (QUIPasteboard) -+ (instancetype)pasteboardWithQClipboardMode:(QClipboard::Mode)mode; -@end - -@implementation UIPasteboard (QUIPasteboard) -+ (instancetype)pasteboardWithQClipboardMode:(QClipboard::Mode)mode -{ - NSString *name = (mode == QClipboard::Clipboard) ? UIPasteboardNameGeneral : UIPasteboardNameFind; - return [UIPasteboard pasteboardWithName:name create:NO]; -} -@end - // -------------------------------------------------------------------- @interface QUIClipboard : NSObject @@ -30,7 +19,6 @@ @implementation QUIClipboard { QIOSClipboard *m_qiosClipboard; NSInteger m_changeCountClipboard; - NSInteger m_changeCountFindBuffer; } - (instancetype)initWithQIOSClipboard:(QIOSClipboard *)qiosClipboard @@ -38,8 +26,7 @@ self = [super init]; if (self) { m_qiosClipboard = qiosClipboard; - m_changeCountClipboard = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::Clipboard].changeCount; - m_changeCountFindBuffer = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::FindBuffer].changeCount; + m_changeCountClipboard = UIPasteboard.generalPasteboard.changeCount; [[NSNotificationCenter defaultCenter] addObserver:self @@ -76,18 +63,12 @@ - (void)updatePasteboardChanged:(NSNotification *)notification { Q_UNUSED(notification); - NSInteger changeCountClipboard = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::Clipboard].changeCount; - NSInteger changeCountFindBuffer = [UIPasteboard pasteboardWithQClipboardMode:QClipboard::FindBuffer].changeCount; + NSInteger changeCountClipboard = UIPasteboard.generalPasteboard.changeCount; if (m_changeCountClipboard != changeCountClipboard) { m_changeCountClipboard = changeCountClipboard; m_qiosClipboard->emitChanged(QClipboard::Clipboard); } - - if (m_changeCountFindBuffer != changeCountFindBuffer) { - m_changeCountFindBuffer = changeCountFindBuffer; - m_qiosClipboard->emitChanged(QClipboard::FindBuffer); - } } @end @@ -98,25 +79,22 @@ QT_BEGIN_NAMESPACE class QIOSMimeData : public QMimeData { public: - QIOSMimeData(QClipboard::Mode mode) : QMimeData(), m_mode(mode) { } + QIOSMimeData() : QMimeData() { } ~QIOSMimeData() { } QStringList formats() const override; QVariant retrieveData(const QString &mimeType, QMetaType type) const override; - -private: - const QClipboard::Mode m_mode; }; QStringList QIOSMimeData::formats() const { QStringList foundMimeTypes; - UIPasteboard *pb = [UIPasteboard pasteboardWithQClipboardMode:m_mode]; + UIPasteboard *pb = UIPasteboard.generalPasteboard; NSArray<NSString *> *pasteboardTypes = [pb pasteboardTypes]; for (NSUInteger i = 0; i < [pasteboardTypes count]; ++i) { - QString uti = QString::fromNSString([pasteboardTypes objectAtIndex:i]); - QString mimeType = QMacInternalPasteboardMime::flavorToMime(QMacInternalPasteboardMime::MIME_ALL, uti); + const QString uti = QString::fromNSString([pasteboardTypes objectAtIndex:i]); + const QString mimeType = QMacMimeRegistry::flavorToMime(QUtiMimeConverter::HandlerScopeFlag::All, uti); if (!mimeType.isEmpty() && !foundMimeTypes.contains(mimeType)) foundMimeTypes << mimeType; } @@ -126,17 +104,14 @@ QStringList QIOSMimeData::formats() const QVariant QIOSMimeData::retrieveData(const QString &mimeType, QMetaType) const { - UIPasteboard *pb = [UIPasteboard pasteboardWithQClipboardMode:m_mode]; + UIPasteboard *pb = UIPasteboard.generalPasteboard; NSArray<NSString *> *pasteboardTypes = [pb pasteboardTypes]; - foreach (QMacInternalPasteboardMime *converter, - QMacInternalPasteboardMime::all(QMacInternalPasteboardMime::MIME_ALL)) { - if (!converter->canConvert(mimeType, converter->flavorFor(mimeType))) - continue; - + const auto converters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All); + for (QUtiMimeConverter *converter : converters) { for (NSUInteger i = 0; i < [pasteboardTypes count]; ++i) { NSString *availableUtiNSString = [pasteboardTypes objectAtIndex:i]; - QString availableUti = QString::fromNSString(availableUtiNSString); + const QString availableUti = QString::fromNSString(availableUtiNSString); if (!converter->canConvert(mimeType, availableUti)) continue; @@ -166,7 +141,7 @@ QMimeData *QIOSClipboard::mimeData(QClipboard::Mode mode) { Q_ASSERT(supportsMode(mode)); if (!m_mimeData.contains(mode)) - return *m_mimeData.insert(mode, new QIOSMimeData(mode)); + return *m_mimeData.insert(mode, new QIOSMimeData); return m_mimeData[mode]; } @@ -174,7 +149,7 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) { Q_ASSERT(supportsMode(mode)); - UIPasteboard *pb = [UIPasteboard pasteboardWithQClipboardMode:mode]; + UIPasteboard *pb = UIPasteboard.generalPasteboard; if (!mimeData) { pb.items = [NSArray<NSDictionary<NSString *, id> *> array]; return; @@ -183,26 +158,29 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) mimeData->deleteLater(); NSMutableDictionary<NSString *, id> *pbItem = [NSMutableDictionary<NSString *, id> dictionaryWithCapacity:mimeData->formats().size()]; - foreach (const QString &mimeType, mimeData->formats()) { - foreach (QMacInternalPasteboardMime *converter, - QMacInternalPasteboardMime::all(QMacInternalPasteboardMime::MIME_ALL)) { - QString uti = converter->flavorFor(mimeType); - if (uti.isEmpty() || !converter->canConvert(mimeType, uti)) + const auto formats = mimeData->formats(); + for (const QString &mimeType : formats) { + const auto converters = QMacMimeRegistry::all(QUtiMimeConverter::HandlerScopeFlag::All); + for (const QUtiMimeConverter *converter : converters) { + const QString uti = converter->utiForMime(mimeType); + if (uti.isEmpty()) continue; QVariant mimeDataAsVariant; if (mimeData->hasImage()) { mimeDataAsVariant = mimeData->imageData(); } else if (mimeData->hasUrls()) { + const auto urls = mimeData->urls(); QVariantList urlList; - for (QUrl url : mimeData->urls()) + urlList.reserve(urls.size()); + for (const QUrl& url : urls) urlList << url; mimeDataAsVariant = QVariant(urlList); } else { mimeDataAsVariant = QVariant(mimeData->data(mimeType)); } - QByteArray byteArray = converter->convertFromMime(mimeType, mimeDataAsVariant, uti).first(); + QByteArray byteArray = converter->convertFromMime(mimeType, mimeDataAsVariant, uti).constFirst(); NSData *nsData = [NSData dataWithBytes:byteArray.constData() length:byteArray.size()]; [pbItem setValue:nsData forKey:uti.toNSString()]; break; @@ -214,7 +192,7 @@ void QIOSClipboard::setMimeData(QMimeData *mimeData, QClipboard::Mode mode) bool QIOSClipboard::supportsMode(QClipboard::Mode mode) const { - return (mode == QClipboard::Clipboard || mode == QClipboard::FindBuffer); + return mode == QClipboard::Clipboard; } bool QIOSClipboard::ownsMode(QClipboard::Mode mode) const diff --git a/src/plugins/platforms/ios/qioscolordialog.h b/src/plugins/platforms/ios/qioscolordialog.h new file mode 100644 index 0000000000..1af718949b --- /dev/null +++ b/src/plugins/platforms/ios/qioscolordialog.h @@ -0,0 +1,38 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QIOSCOLORDIALOG_H +#define QIOSCOLORDIALOG_H + +#include <QtCore/qeventloop.h> +#include <qpa/qplatformdialoghelper.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(QIOSColorDialogController); + +QT_BEGIN_NAMESPACE + +class QIOSColorDialog : public QPlatformColorDialogHelper +{ +public: + QIOSColorDialog(); + ~QIOSColorDialog(); + + void exec() override; + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + + void setCurrentColor(const QColor&) override; + QColor currentColor() const override; + + void updateColor(const QColor&); + +private: + QEventLoop m_eventLoop; + QIOSColorDialogController *m_viewController; + QColor m_currentColor; +}; + +QT_END_NAMESPACE + +#endif // QIOSCOLORDIALOG_H + diff --git a/src/plugins/platforms/ios/qioscolordialog.mm b/src/plugins/platforms/ios/qioscolordialog.mm new file mode 100644 index 0000000000..6651b1791d --- /dev/null +++ b/src/plugins/platforms/ios/qioscolordialog.mm @@ -0,0 +1,159 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#import <UIKit/UIKit.h> + +#include <QtGui/qwindow.h> +#include <QDebug> + +#include <QtCore/private/qcore_mac_p.h> + +#include "qiosglobal.h" +#include "qioscolordialog.h" +#include "qiosintegration.h" + +@interface QIOSColorDialogController : UIColorPickerViewController <UIColorPickerViewControllerDelegate, + UIAdaptivePresentationControllerDelegate> +@end + +@implementation QIOSColorDialogController { + QIOSColorDialog *m_colorDialog; +} + +- (instancetype)initWithQIOSColorDialog:(QIOSColorDialog *)dialog +{ + if (self = [super init]) { + m_colorDialog = dialog; + self.delegate = self; + self.presentationController.delegate = self; + self.supportsAlpha = dialog->options()->testOption(QColorDialogOptions::ShowAlphaChannel); + } + return self; +} + +- (void)setQColor:(const QColor &)qColor +{ + UIColor *uiColor; + const QColor::Spec spec = qColor.spec(); + if (spec == QColor::Hsv) { + uiColor = [UIColor colorWithHue:qColor.hsvHueF() + saturation:qColor.hsvSaturationF() + brightness:qColor.valueF() + alpha:qColor.alphaF()]; + } else { + uiColor = [UIColor colorWithRed:qColor.redF() + green:qColor.greenF() + blue:qColor.blueF() + alpha:qColor.alphaF()]; + } + self.selectedColor = uiColor; +} + +- (void)updateQColor +{ + UIColor *color = self.selectedColor; + CGFloat red = 0, green = 0, blue = 0, alpha = 0; + + QColor newColor; + if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) + newColor.setRgbF(red, green, blue, alpha); + else + qWarning() << "Incompatible color space"; + + + if (m_colorDialog) { + m_colorDialog->updateColor(newColor); + emit m_colorDialog->currentColorChanged(newColor); + } +} + +// ----------------------UIColorPickerViewControllerDelegate-------------------------- +- (void)colorPickerViewControllerDidSelectColor:(UIColorPickerViewController *)viewController +{ + Q_UNUSED(viewController); + [self updateQColor]; +} + +- (void)colorPickerViewControllerDidFinish:(UIColorPickerViewController *)viewController +{ + Q_UNUSED(viewController); + [self updateQColor]; + emit m_colorDialog->accept(); +} + +// ----------------------UIAdaptivePresentationControllerDelegate-------------------------- +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController +{ + Q_UNUSED(presentationController); + emit m_colorDialog->reject(); +} + +@end + +QIOSColorDialog::QIOSColorDialog() + : m_viewController(nullptr) +{ +} + +QIOSColorDialog::~QIOSColorDialog() +{ + hide(); +} + +void QIOSColorDialog::exec() +{ + m_eventLoop.exec(QEventLoop::DialogExec); +} + +bool QIOSColorDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) +{ + Q_UNUSED(windowFlags); + + if (!m_viewController) { + m_viewController = [[QIOSColorDialogController alloc] initWithQIOSColorDialog:this]; + if (m_currentColor.isValid()) + [m_viewController setQColor:m_currentColor]; + } + + if (windowModality == Qt::ApplicationModal || windowModality == Qt::WindowModal) + m_viewController.modalInPresentation = YES; + + UIWindow *window = presentationWindow(parent); + if (!window) + return false; + + // We can't present from view controller if already presenting + if (window.rootViewController.presentedViewController) + return false; + + [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; + + return true; +} + +void QIOSColorDialog::hide() +{ + [m_viewController dismissViewControllerAnimated:YES completion:nil]; + [m_viewController release]; + m_viewController = nullptr; + m_eventLoop.exit(); +} + +void QIOSColorDialog::setCurrentColor(const QColor &color) +{ + updateColor(color); + if (m_viewController) + [m_viewController setQColor:color]; +} + +QColor QIOSColorDialog::currentColor() const +{ + return m_currentColor; +} + +void QIOSColorDialog::updateColor(const QColor &color) +{ + m_currentColor = color; +} + + diff --git a/src/plugins/platforms/ios/qioscontext.mm b/src/plugins/platforms/ios/qioscontext.mm index 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.h b/src/plugins/platforms/ios/qiosdocumentpickercontroller.h index 9182a699b3..f0b7472539 100644 --- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.h +++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #import <UIKit/UIKit.h> +#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> #include "qiosfiledialog.h" diff --git a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm index 09934dda0e..fca0432426 100644 --- a/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm +++ b/src/plugins/platforms/ios/qiosdocumentpickercontroller.mm @@ -12,27 +12,37 @@ - (instancetype)initWithQIOSFileDialog:(QIOSFileDialog *)fileDialog { - NSMutableArray <NSString *> *docTypes = [[[NSMutableArray alloc] init] autorelease]; - UIDocumentPickerMode importMode; - switch (fileDialog->options()->fileMode()) { - case QFileDialogOptions::AnyFile: - case QFileDialogOptions::ExistingFile: - case QFileDialogOptions::ExistingFiles: - [docTypes addObject:(__bridge NSString *)kUTTypeContent]; - [docTypes addObject:(__bridge NSString *)kUTTypeItem]; - [docTypes addObject:(__bridge NSString *)kUTTypeData]; - importMode = UIDocumentPickerModeImport; - break; - case QFileDialogOptions::Directory: - case QFileDialogOptions::DirectoryOnly: - // Directory picking is not supported because it requires - // special handling not possible with the current QFilePicker - // implementation. - - Q_UNREACHABLE(); + NSMutableArray <UTType *> *docTypes = [[[NSMutableArray alloc] init] autorelease]; + + QStringList nameFilters = fileDialog->options()->nameFilters(); + if (!nameFilters.isEmpty() && (fileDialog->options()->fileMode() != QFileDialogOptions::Directory + || fileDialog->options()->fileMode() != QFileDialogOptions::DirectoryOnly)) + { + QStringList results; + for (const QString &filter : nameFilters) + results.append(QPlatformFileDialogHelper::cleanFilterList(filter)); + + docTypes = [self computeAllowedFileTypes:results]; + } + + if (!docTypes.count) { + switch (fileDialog->options()->fileMode()) { + case QFileDialogOptions::AnyFile: + case QFileDialogOptions::ExistingFile: + case QFileDialogOptions::ExistingFiles: + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeContent]]; + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeItem]]; + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeData]]; + break; + // Showing files is not supported in Directory mode in iOS + case QFileDialogOptions::Directory: + case QFileDialogOptions::DirectoryOnly: + [docTypes addObject:[UTType typeWithIdentifier:(__bridge NSString *)UTTypeFolder]]; + break; + } } - if (self = [super initWithDocumentTypes:docTypes inMode:importMode]) { + if (self = [super initForOpeningContentTypes:docTypes]) { m_fileDialog = fileDialog; self.modalPresentationStyle = UIModalPresentationFormSheet; self.delegate = self; @@ -78,4 +88,28 @@ emit m_fileDialog->reject(); } +- (NSMutableArray<UTType*>*)computeAllowedFileTypes:(QStringList)filters +{ + QStringList fileTypes; + for (const QString &filter : filters) { + if (filter == (QLatin1String("*"))) + continue; + + if (filter.contains(u'?')) + continue; + + if (filter.count(u'*') != 1) + continue; + + auto extensions = filter.split('.', Qt::SkipEmptyParts); + fileTypes += extensions.last(); + } + + NSMutableArray<UTType *> *result = [NSMutableArray<UTType *> arrayWithCapacity:fileTypes.size()]; + for (const QString &string : fileTypes) + [result addObject:[UTType typeWithFilenameExtension:string.toNSString()]]; + + return result; +} + @end diff --git a/src/plugins/platforms/ios/qiosfiledialog.h b/src/plugins/platforms/ios/qiosfiledialog.h index 12a3af7181..f00c154c03 100644 --- a/src/plugins/platforms/ios/qiosfiledialog.h +++ b/src/plugins/platforms/ios/qiosfiledialog.h @@ -39,6 +39,7 @@ private: bool showImagePickerDialog(QWindow *parent); bool showNativeDocumentPickerDialog(QWindow *parent); + void showImagePickerDialog_helper(QWindow *parent); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosfiledialog.mm b/src/plugins/platforms/ios/qiosfiledialog.mm index 64e9445c43..cf9580c17e 100644 --- a/src/plugins/platforms/ios/qiosfiledialog.mm +++ b/src/plugins/platforms/ios/qiosfiledialog.mm @@ -3,17 +3,22 @@ #import <UIKit/UIKit.h> +#import <Photos/Photos.h> + #include <QtCore/qstandardpaths.h> #include <QtGui/qwindow.h> #include <QDebug> #include <QtCore/private/qcore_mac_p.h> +#include "qiosglobal.h" #include "qiosfiledialog.h" #include "qiosintegration.h" #include "qiosoptionalplugininterface.h" #include "qiosdocumentpickercontroller.h" +#include <QtCore/qpointer.h> + using namespace Qt::StringLiterals; QIOSFileDialog::QIOSFileDialog() @@ -36,11 +41,15 @@ bool QIOSFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window Q_UNUSED(windowFlags); Q_UNUSED(windowModality); - bool acceptOpen = options()->acceptMode() == QFileDialogOptions::AcceptOpen; - QString directory = options()->initialDirectory().toLocalFile(); + const bool acceptOpen = options()->acceptMode() == QFileDialogOptions::AcceptOpen; + const auto initialDir = options()->initialDirectory(); + const QString directory = initialDir.toLocalFile(); + // We manually add assets-library:// to the list of paths, + // when converted to QUrl, it becames a scheme. + const QString scheme = initialDir.scheme(); if (acceptOpen) { - if (directory.startsWith("assets-library:"_L1)) + if (directory.startsWith("assets-library:"_L1) || scheme == "assets-library"_L1) return showImagePickerDialog(parent); else return showNativeDocumentPickerDialog(parent); @@ -49,6 +58,12 @@ bool QIOSFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality window return false; } +void QIOSFileDialog::showImagePickerDialog_helper(QWindow *parent) +{ + UIWindow *window = presentationWindow(parent); + [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; +} + bool QIOSFileDialog::showImagePickerDialog(QWindow *parent) { if (!m_viewController) { @@ -67,9 +82,38 @@ bool QIOSFileDialog::showImagePickerDialog(QWindow *parent) return false; } - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window - : qt_apple_sharedApplication().keyWindow; - [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; + // "Old style" authorization (deprecated, but we have to work with AssetsLibrary anyway). + // + // From the documentation: + // "The authorizationStatus and requestAuthorization: methods aren’t compatible with the + // limited library and return PHAuthorizationStatusAuthorized when the user authorizes your + // app for limited access only." + // + // This is good enough for us. + + const auto authStatus = [PHPhotoLibrary authorizationStatus]; + if (authStatus == PHAuthorizationStatusAuthorized) { + showImagePickerDialog_helper(parent); + } else if (authStatus == PHAuthorizationStatusNotDetermined) { + QPointer<QWindow> winGuard(parent); + QPointer<QIOSFileDialog> thisGuard(this); + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + if (thisGuard && winGuard) + thisGuard->showImagePickerDialog_helper(winGuard); + + } else if (thisGuard) { + emit thisGuard->reject(); + } + }); + }]; + } else { + // Treat 'Limited' (we don't know how to deal with anyway) and 'Denied' as errors. + // FIXME: logging category? + qWarning() << "QIOSFileDialog: insufficient permission, cannot pick images"; + return false; + } return true; } @@ -77,14 +121,9 @@ bool QIOSFileDialog::showImagePickerDialog(QWindow *parent) bool QIOSFileDialog::showNativeDocumentPickerDialog(QWindow *parent) { #ifndef Q_OS_TVOS - if (options()->fileMode() == QFileDialogOptions::Directory || - options()->fileMode() == QFileDialogOptions::DirectoryOnly) - return false; - m_viewController = [[QIOSDocumentPickerController alloc] initWithQIOSFileDialog:this]; - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window - : qt_apple_sharedApplication().keyWindow; + UIWindow *window = presentationWindow(parent); [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; return true; diff --git a/src/plugins/platforms/ios/qiosfontdialog.h b/src/plugins/platforms/ios/qiosfontdialog.h new file mode 100644 index 0000000000..f0a92d0d6f --- /dev/null +++ b/src/plugins/platforms/ios/qiosfontdialog.h @@ -0,0 +1,41 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QIOSFONTDIALOG_H +#define QIOSFONTDIALOG_H + +#include <QtCore/qeventloop.h> +#include <qpa/qplatformdialoghelper.h> + +@interface QIOSFontDialogController : UIFontPickerViewController <UIFontPickerViewControllerDelegate, + UIAdaptivePresentationControllerDelegate> +@end + +QT_BEGIN_NAMESPACE + +class QIOSFontDialog : public QPlatformFontDialogHelper +{ +public: + QIOSFontDialog(); + ~QIOSFontDialog(); + + void exec() override; + + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + + void setCurrentFont(const QFont &) override; + QFont currentFont() const override; + + void updateCurrentFont(const QFont &); + +private: + QEventLoop m_eventLoop; + QIOSFontDialogController *m_viewController; + QFont m_currentFont; + +}; + +QT_END_NAMESPACE + +#endif // QIOSFONTDIALOG_H diff --git a/src/plugins/platforms/ios/qiosfontdialog.mm b/src/plugins/platforms/ios/qiosfontdialog.mm new file mode 100644 index 0000000000..25d0197195 --- /dev/null +++ b/src/plugins/platforms/ios/qiosfontdialog.mm @@ -0,0 +1,190 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#import <UIKit/UIKit.h> + +#include <QtGui/qwindow.h> +#include <QtGui/qfontdatabase.h> +#include <QDebug> + +#include <QtCore/private/qcore_mac_p.h> +#include <QtGui/private/qfont_p.h> +#include <QtGui/private/qfontengine_p.h> + +#include "qiosglobal.h" +#include "qiosfontdialog.h" +#include "qiosintegration.h" + +@implementation QIOSFontDialogController { + QIOSFontDialog *m_fontDialog; +} + +- (instancetype)initWithQIOSFontDialog:(QIOSFontDialog *)dialog +{ + UIFontPickerViewControllerConfiguration *configuration = [[UIFontPickerViewControllerConfiguration alloc] init]; + if (dialog->options()->testOption(QFontDialogOptions::MonospacedFonts)) { + UIFontDescriptorSymbolicTraits traits = {}; + traits |= UIFontDescriptorTraitMonoSpace; + configuration.filteredTraits = traits; + } + configuration.includeFaces = YES; + if (self = [super initWithConfiguration:configuration]) { + m_fontDialog = dialog; + self.delegate = self; + self.presentationController.delegate = self; + } + [configuration release]; + return self; +} + +- (void)setQFont:(const QFont &)font +{ + QFontInfo fontInfo(font); + auto family = fontInfo.family().toNSString(); + auto size = fontInfo.pointSize(); + + NSDictionary *dictionary = @{ + static_cast<NSString *>(UIFontDescriptorFamilyAttribute): family, + static_cast<NSString *>(UIFontDescriptorSizeAttribute): [NSNumber numberWithInt:size] + }; + UIFontDescriptor *fd = [UIFontDescriptor fontDescriptorWithFontAttributes:dictionary]; + + UIFontDescriptorSymbolicTraits traits = 0; + if (font.style() == QFont::StyleItalic) + traits |= UIFontDescriptorTraitItalic; + if (font.weight() == QFont::Bold) + traits |= UIFontDescriptorTraitBold; + fd = [fd fontDescriptorWithSymbolicTraits:traits]; + + self.selectedFontDescriptor = fd; +} + +- (void)updateQFont +{ + UIFontDescriptor *font = self.selectedFontDescriptor; + if (!font) + return; + + NSDictionary *attributes = font.fontAttributes; + UIFontDescriptorSymbolicTraits traits = font.symbolicTraits; + + QFont newFont; + int size = qRound(font.pointSize); + QString family = QString::fromNSString([attributes objectForKey:UIFontDescriptorFamilyAttribute]); + if (family.isEmpty()) { + // If includeFaces is true, then the font descriptor won't + // have the UIFontDescriptorFamilyAttribute key set so we + // need to create a UIFont to get the font family + UIFont *f = [UIFont fontWithDescriptor:font size:size]; + family = QString::fromNSString(f.familyName); + } + + QString style; + if ((traits & (UIFontDescriptorTraitItalic | UIFontDescriptorTraitBold)) == (UIFontDescriptorTraitItalic | UIFontDescriptorTraitBold)) + style = "Bold Italic"; + else if (traits & UIFontDescriptorTraitItalic) + style = "Italic"; + else if (traits & UIFontDescriptorTraitBold) + style = "Bold"; + + newFont = QFontDatabase::font(family, style, size); + + if (m_fontDialog) { + m_fontDialog->updateCurrentFont(newFont); + emit m_fontDialog->currentFontChanged(newFont); + } +} + +// ----------------------UIFontPickerViewControllerDelegate-------------------------- +- (void)fontPickerViewControllerDidPickFont:(UIFontPickerViewController *)viewController +{ + [self updateQFont]; + emit m_fontDialog->accept(); +} + +- (void)fontPickerViewControllerDidCancel:(UIFontPickerViewController *)viewController +{ + Q_UNUSED(viewController); + emit m_fontDialog->reject(); +} + +// ----------------------UIAdaptivePresentationControllerDelegate-------------------------- +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController +{ + Q_UNUSED(presentationController); + emit m_fontDialog->reject(); +} + +@end + +QIOSFontDialog::QIOSFontDialog() + : m_viewController(nullptr) +{ +} + +QIOSFontDialog::~QIOSFontDialog() +{ + hide(); +} + +void QIOSFontDialog::exec() +{ + m_eventLoop.exec(QEventLoop::DialogExec); +} + +bool QIOSFontDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) +{ + Q_UNUSED(windowFlags); + Q_UNUSED(windowModality); + + if (!m_viewController) { + m_viewController = [[QIOSFontDialogController alloc] initWithQIOSFontDialog:this]; + [m_viewController setQFont:m_currentFont]; + } + + if (windowModality == Qt::ApplicationModal || windowModality == Qt::WindowModal) + m_viewController.modalInPresentation = YES; + + UIWindow *window = presentationWindow(parent); + if (!window) + return false; + + // We can't present from view controller if already presenting + if (window.rootViewController.presentedViewController) + return false; + + [window.rootViewController presentViewController:m_viewController animated:YES completion:nil]; + + return true; +} + +void QIOSFontDialog::hide() +{ + [m_viewController dismissViewControllerAnimated:YES completion:nil]; + [m_viewController release]; + m_viewController = nullptr; + if (m_eventLoop.isRunning()) + m_eventLoop.exit(); +} + +void QIOSFontDialog::setCurrentFont(const QFont &font) +{ + if (m_currentFont == font) + return; + + m_currentFont = font; + if (m_viewController) + [m_viewController setQFont:font]; +} + +QFont QIOSFontDialog::currentFont() const +{ + return m_currentFont; +} + +void QIOSFontDialog::updateCurrentFont(const QFont &font) +{ + m_currentFont = font; +} + + diff --git a/src/plugins/platforms/ios/qiosglobal.h b/src/plugins/platforms/ios/qiosglobal.h index 022fa88c24..9428487a00 100644 --- a/src/plugins/platforms/ios/qiosglobal.h +++ b/src/plugins/platforms/ios/qiosglobal.h @@ -14,6 +14,7 @@ QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(lcQpaApplication); Q_DECLARE_LOGGING_CATEGORY(lcQpaInputMethods); Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow); +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindowScene); #if !defined(QT_NO_DEBUG) #define qImDebug \ @@ -26,6 +27,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow); class QPlatformScreen; bool isQtApplication(); +bool isRunningOnVisionOS(); #ifndef Q_OS_TVOS Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientation); @@ -34,10 +36,15 @@ UIDeviceOrientation fromQtScreenOrientation(Qt::ScreenOrientation qtOrientation) int infoPlistValue(NSString* key, int defaultValue); +class QWindow; +class QScreen; +UIWindow *presentationWindow(QWindow *); +UIView *rootViewForScreen(QScreen *); + QT_END_NAMESPACE @interface UIResponder (QtFirstResponder) -+ (id)currentFirstResponder; ++ (id)qt_currentFirstResponder; @end QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosglobal.mm b/src/plugins/platforms/ios/qiosglobal.mm index 0a6bc5bb62..25ccf2961b 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -13,6 +13,7 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcQpaApplication, "qt.qpa.application"); Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window"); +Q_LOGGING_CATEGORY(lcQpaWindowScene, "qt.qpa.window.scene"); bool isQtApplication() { @@ -29,6 +30,15 @@ bool isQtApplication() return isQt; } +bool isRunningOnVisionOS() +{ + static bool result = []{ + // This class is documented to only be available on visionOS + return NSClassFromString(@"UIWindowSceneGeometryPreferencesVision"); + }(); + return result; +} + #ifndef Q_OS_TVOS Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientation) { @@ -85,6 +95,47 @@ int infoPlistValue(NSString* key, int defaultValue) return value ? [value intValue] : defaultValue; } +UIWindow *presentationWindow(QWindow *window) +{ + UIWindow *uiWindow = window ? reinterpret_cast<UIView *>(window->winId()).window : nullptr; + if (!uiWindow) { + auto *scenes = [qt_apple_sharedApplication().connectedScenes allObjects]; + if (scenes.count > 0) { + auto *windowScene = static_cast<UIWindowScene*>(scenes[0]); + uiWindow = windowScene.keyWindow; + if (!uiWindow && windowScene.windows.count) + uiWindow = windowScene.windows[0]; + } + } + return uiWindow; +} + +UIView *rootViewForScreen(QScreen *screen) +{ + const auto *iosScreen = static_cast<QIOSScreen *>(screen->handle()); + for (UIScene *scene in [qt_apple_sharedApplication().connectedScenes allObjects]) { + if (![scene isKindOfClass:UIWindowScene.class]) + continue; + + auto *windowScene = static_cast<UIWindowScene*>(scene); + +#if !defined(Q_OS_VISIONOS) + if (windowScene.screen != iosScreen->uiScreen()) + continue; +#else + Q_UNUSED(iosScreen); +#endif + + UIWindow *uiWindow = windowScene.keyWindow; + if (!uiWindow && windowScene.windows.count) + uiWindow = windowScene.windows[0]; + + return uiWindow.rootViewController.view; + } + + return nullptr; +} + QT_END_NAMESPACE // ------------------------------------------------------------------------- @@ -119,7 +170,7 @@ QT_END_NAMESPACE @implementation UIResponder (QtFirstResponder) -+ (id)currentFirstResponder ++ (id)qt_currentFirstResponder { if (qt_apple_isApplicationExtension()) { qWarning() << "can't get first responder in application extensions!"; diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index 4c4b213ba2..5716ad041e 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.mm +++ b/src/plugins/platforms/ios/qiosinputcontext.mm @@ -18,6 +18,8 @@ #include <QGuiApplication> #include <QtGui/private/qwindow_p.h> +#include <QtCore/qpointer.h> + // ------------------------------------------------------------------------- static QUIView *focusView() @@ -119,12 +121,12 @@ static QUIView *focusView() { [self keyboardWillOrDidChange:notification]; - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; if (![firstResponder isKindOfClass:[QIOSTextInputResponder class]]) return; // Enable hide-keyboard gesture - self.enabled = YES; + self.enabled = m_context->isInputPanelVisible(); m_context->scrollToCursor(); } @@ -174,7 +176,11 @@ static QUIView *focusView() { [super touchesBegan:touches withEvent:event]; - Q_ASSERT(m_context->isInputPanelVisible()); + if (!m_context->isInputPanelVisible()) { + qImDebug("keyboard was hidden by sliding it down, disabling hide-keyboard gesture"); + self.enabled = NO; + return; + } if ([touches count] != 1) self.state = UIGestureRecognizerStateFailed; @@ -228,7 +234,7 @@ static QUIView *focusView() if (self.state == UIGestureRecognizerStateBegan) { qImDebug("hide keyboard gesture was triggered"); - UIResponder *firstResponder = [UIResponder currentFirstResponder]; + UIResponder *firstResponder = [UIResponder qt_currentFirstResponder]; Q_ASSERT([firstResponder isKindOfClass:[QIOSTextInputResponder class]]); [firstResponder resignFirstResponder]; } @@ -297,11 +303,7 @@ QIOSInputContext::QIOSInputContext() , m_keyboardHideGesture([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this]) , m_textResponder(0) { - if (isQtApplication()) { - QIOSScreen *iosScreen = static_cast<QIOSScreen*>(QGuiApplication::primaryScreen()->handle()); - [iosScreen->uiWindow() addGestureRecognizer:m_keyboardHideGesture]; - } - + Q_ASSERT(!qGuiApp->focusWindow()); connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged); } @@ -346,7 +348,7 @@ void QIOSInputContext::clearCurrentFocusObject() void QIOSInputContext::updateKeyboardState(NSNotification *notification) { -#ifdef Q_OS_TVOS +#if defined(Q_OS_TVOS) || defined(Q_OS_VISIONOS) Q_UNUSED(notification); #else static CGRect currentKeyboardRect = CGRectZero; @@ -366,7 +368,7 @@ void QIOSInputContext::updateKeyboardState(NSNotification *notification) // The isInputPanelVisible() property is based on whether or not the virtual keyboard // is visible on screen, and does not follow the logic of the iOS WillShow and WillHide // notifications which are not emitted for undocked keyboards, and are buggy when dealing - // with input-accesosory-views. The reason for using frameEnd here (the future state), + // with input-accessory-views. The reason for using frameEnd here (the future state), // instead of the current state reflected in frameBegin, is that QInputMethod::isVisible() // is documented to reflect the future state in the case of animated transitions. m_keyboardState.keyboardVisible = CGRectIntersectsRect(frameEnd, [UIScreen mainScreen].bounds); @@ -436,6 +438,7 @@ UIView *QIOSInputContext::scrollableRootView() void QIOSInputContext::scrollToCursor() { +#if !defined(Q_OS_VISIONOS) if (!isQtApplication()) return; @@ -492,6 +495,7 @@ void QIOSInputContext::scrollToCursor() } else { scroll(0); } +#endif } void QIOSInputContext::scroll(int y) @@ -603,12 +607,15 @@ void QIOSInputContext::setFocusObject(QObject *focusObject) void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) { - Q_UNUSED(focusWindow); - qImDebug() << "new focus window =" << focusWindow; reset(); + if (isQtApplication()) { + [m_keyboardHideGesture.view removeGestureRecognizer:m_keyboardHideGesture]; + [focusView().window addGestureRecognizer:m_keyboardHideGesture]; + } + // The keyboard rectangle depend on the focus window, so // we need to re-evaluate the keyboard state. updateKeyboardState(); diff --git a/src/plugins/platforms/ios/qiosintegration.h b/src/plugins/platforms/ios/qiosintegration.h index 6830a5b455..2c7d33cc94 100644 --- a/src/plugins/platforms/ios/qiosintegration.h +++ b/src/plugins/platforms/ios/qiosintegration.h @@ -11,7 +11,8 @@ #include <QtCore/private/qfactoryloader_p.h> #include "qiosapplicationstate.h" -#ifndef Q_OS_TVOS + +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) #include "qiostextinputoverlay.h" #endif @@ -31,15 +32,21 @@ public: bool hasCapability(Capability cap) const override; QPlatformWindow *createPlatformWindow(QWindow *window) const override; + QPlatformWindow *createForeignWindow(QWindow *window, WId nativeHandle) const override; QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override; +#if QT_CONFIG(opengl) QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const override; +#endif + QPlatformOffscreenSurface *createPlatformOffscreenSurface(QOffscreenSurface *surface) const override; QPlatformFontDatabase *fontDatabase() const override; -#ifndef QT_NO_CLIPBOARD + +#if QT_CONFIG(clipboard) QPlatformClipboard *clipboard() const override; #endif + QPlatformInputContext *inputContext() const override; QPlatformServices *services() const override; @@ -58,6 +65,8 @@ public: void beep() const override; + void setApplicationBadge(qint64 number) override; + static QIOSIntegration *instance(); // -- QPlatformNativeInterface -- @@ -70,7 +79,7 @@ public: private: QPlatformFontDatabase *m_fontDatabase; -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) QPlatformClipboard *m_clipboard; #endif QPlatformInputContext *m_inputContext; @@ -78,7 +87,7 @@ private: QIOSServices *m_platformServices; mutable QPlatformAccessibility *m_accessibility; QFactoryLoader *m_optionalPlugins; -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QIOSTextInputOverlay m_textInputOverlay; #endif }; diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index 84caccc267..7cd21f83f6 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -1,14 +1,15 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qiosintegration.h" #include "qioseventdispatcher.h" #include "qiosglobal.h" #include "qioswindow.h" #include "qiosscreen.h" #include "qiosplatformaccessibility.h" -#include "qioscontext.h" -#ifndef Q_OS_TVOS +#if QT_CONFIG(clipboard) #include "qiosclipboard.h" #endif #include "qiosinputcontext.h" @@ -24,10 +25,15 @@ #include <qpa/qplatformoffscreensurface.h> #include <QtGui/private/qcoretextfontdatabase_p.h> -#include <QtGui/private/qmacmime_p.h> +#include <QtGui/private/qmacmimeregistry_p.h> +#include <QtGui/qutimimeconverter.h> #include <QDir> #include <QOperatingSystemVersion> +#if QT_CONFIG(opengl) +#include "qioscontext.h" +#endif + #import <AudioToolbox/AudioServices.h> #include <QtDebug> @@ -45,7 +51,7 @@ QIOSIntegration *QIOSIntegration::instance() QIOSIntegration::QIOSIntegration() : m_fontDatabase(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>) -#if !defined(Q_OS_TVOS) && !defined(QT_NO_CLIPBOARD) +#if QT_CONFIG(clipboard) , m_clipboard(new QIOSClipboard) #endif , m_inputContext(0) @@ -66,6 +72,10 @@ QIOSIntegration::QIOSIntegration() void QIOSIntegration::initialize() { +#if defined(Q_OS_VISIONOS) + // Qt requires a screen, so let's give it a dummy one + QWindowSystemInterface::handleScreenAdded(new QIOSScreen); +#else UIScreen *mainScreen = [UIScreen mainScreen]; NSMutableArray<UIScreen *> *screens = [[[UIScreen screens] mutableCopy] autorelease]; if (![screens containsObject:mainScreen]) { @@ -75,6 +85,7 @@ void QIOSIntegration::initialize() for (UIScreen *screen in screens) QWindowSystemInterface::handleScreenAdded(new QIOSScreen(screen)); +#endif // Depends on a primary screen being present m_inputContext = new QIOSInputContext; @@ -82,14 +93,16 @@ void QIOSIntegration::initialize() m_touchDevice = new QPointingDevice; m_touchDevice->setType(QInputDevice::DeviceType::TouchScreen); QPointingDevice::Capabilities touchCapabilities = QPointingDevice::Capability::Position | QPointingDevice::Capability::NormalizedPosition; +#if !defined(Q_OS_VISIONOS) if (mainScreen.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) touchCapabilities |= QPointingDevice::Capability::Pressure; +#endif m_touchDevice->setCapabilities(touchCapabilities); QWindowSystemInterface::registerInputDevice(m_touchDevice); #if QT_CONFIG(tabletevent) QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); #endif - QMacInternalPasteboardMime::initializeMimeTypes(); + QMacMimeRegistry::initializeMimeTypes(); qsizetype size = QList<QPluginParsedMetaData>(m_optionalPlugins->metaData()).size(); for (qsizetype i = 0; i < size; ++i) @@ -101,11 +114,12 @@ QIOSIntegration::~QIOSIntegration() delete m_fontDatabase; m_fontDatabase = 0; -#if !defined(Q_OS_TVOS) && !defined(QT_NO_CLIPBOARD) +#if QT_CONFIG(clipboard) delete m_clipboard; m_clipboard = 0; #endif - QMacInternalPasteboardMime::destroyMimeTypes(); + + QMacMimeRegistry::destroyMimeTypes(); delete m_inputContext; m_inputContext = 0; @@ -126,11 +140,15 @@ QIOSIntegration::~QIOSIntegration() bool QIOSIntegration::hasCapability(Capability cap) const { switch (cap) { +#if QT_CONFIG(opengl) case BufferQueueingOpenGL: return true; case OpenGL: case ThreadedOpenGL: return true; + case RasterGLSurface: + return true; +#endif case ThreadedPixmaps: return true; case MultipleWindows: @@ -139,7 +157,7 @@ bool QIOSIntegration::hasCapability(Capability cap) const return false; case ApplicationState: return true; - case RasterGLSurface: + case ForeignWindows: return true; default: return QPlatformIntegration::hasCapability(cap); @@ -151,16 +169,23 @@ QPlatformWindow *QIOSIntegration::createPlatformWindow(QWindow *window) const return new QIOSWindow(window); } +QPlatformWindow *QIOSIntegration::createForeignWindow(QWindow *window, WId nativeHandle) const +{ + return new QIOSWindow(window, nativeHandle); +} + QPlatformBackingStore *QIOSIntegration::createPlatformBackingStore(QWindow *window) const { return new QRhiBackingStore(window); } +#if QT_CONFIG(opengl) // Used when the QWindow's surface type is set by the client to QSurface::OpenGLSurface QPlatformOpenGLContext *QIOSIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { return new QIOSContext(context); } +#endif class QIOSOffscreenSurface : public QPlatformOffscreenSurface { @@ -190,14 +215,10 @@ QPlatformFontDatabase * QIOSIntegration::fontDatabase() const return m_fontDatabase; } -#ifndef QT_NO_CLIPBOARD +#if QT_CONFIG(clipboard) QPlatformClipboard *QIOSIntegration::clipboard() const { -#ifndef Q_OS_TVOS return m_clipboard; -#else - return QPlatformIntegration::clipboard(); -#endif } #endif @@ -268,6 +289,11 @@ void QIOSIntegration::beep() const #endif } +void QIOSIntegration::setApplicationBadge(qint64 number) +{ + UIApplication.sharedApplication.applicationIconBadgeNumber = number; +} + // --------------------------------------------------------- void *QIOSIntegration::nativeResourceForWindow(const QByteArray &resource, QWindow *window) diff --git a/src/plugins/platforms/ios/qiosmenu.h b/src/plugins/platforms/ios/qiosmenu.h index 1822c08b1a..b0c8e7e10c 100644 --- a/src/plugins/platforms/ios/qiosmenu.h +++ b/src/plugins/platforms/ios/qiosmenu.h @@ -11,6 +11,8 @@ #import "quiview.h" +#include <QtCore/qpointer.h> + class QIOSMenu; @class QUIMenuController; @class QUIPickerView; diff --git a/src/plugins/platforms/ios/qiosmenu.mm b/src/plugins/platforms/ios/qiosmenu.mm index e547cb0641..227ad2c7f5 100644 --- a/src/plugins/platforms/ios/qiosmenu.mm +++ b/src/plugins/platforms/ios/qiosmenu.mm @@ -491,7 +491,7 @@ QIOSMenuItemList QIOSMenu::filterFirstResponderActions(const QIOSMenuItemList &m // In case of QIOSTextResponder, edit actions will be converted to key events that ends up // triggering the shortcuts of the filtered menu items. QIOSMenuItemList filteredMenuItems; - UIResponder *responder = [UIResponder currentFirstResponder]; + UIResponder *responder = [UIResponder qt_currentFirstResponder]; for (int i = 0; i < menuItems.count(); ++i) { QIOSMenuItem *menuItem = menuItems.at(i); @@ -502,8 +502,8 @@ QIOSMenuItemList QIOSMenu::filterFirstResponderActions(const QIOSMenuItemList &m || (shortcut == QKeySequence::Paste && [responder canPerformAction:@selector(paste:) withSender:nil]) || (shortcut == QKeySequence::Delete && [responder canPerformAction:@selector(delete:) withSender:nil]) || (shortcut == QKeySequence::SelectAll && [responder canPerformAction:@selector(selectAll:) withSender:nil]) - || (shortcut == QKeySequence::Undo && [responder canPerformAction:@selector(undo:) withSender:nil]) - || (shortcut == QKeySequence::Redo && [responder canPerformAction:@selector(redo:) withSender:nil]) + || (shortcut == QKeySequence::Undo && [responder canPerformAction:@selector(undo) withSender:nil]) + || (shortcut == QKeySequence::Redo && [responder canPerformAction:@selector(redo) withSender:nil]) || (shortcut == QKeySequence::Bold && [responder canPerformAction:@selector(toggleBoldface:) withSender:nil]) || (shortcut == QKeySequence::Italic && [responder canPerformAction:@selector(toggleItalics:) withSender:nil]) || (shortcut == QKeySequence::Underline && [responder canPerformAction:@selector(toggleUnderline:) withSender:nil])) { diff --git a/src/plugins/platforms/ios/qiosmessagedialog.mm b/src/plugins/platforms/ios/qiosmessagedialog.mm index fd14e699e0..7fbd5d8729 100644 --- a/src/plugins/platforms/ios/qiosmessagedialog.mm +++ b/src/plugins/platforms/ios/qiosmessagedialog.mm @@ -11,6 +11,7 @@ #include "qiosglobal.h" #include "quiview.h" +#include "qiosscreen.h" #include "qiosmessagedialog.h" using namespace Qt::StringLiterals; @@ -29,7 +30,7 @@ inline QString QIOSMessageDialog::messageTextPlain() { // Concatenate text fragments, and remove HTML tags const QSharedPointer<QMessageDialogOptions> &opt = options(); - const QString &lineShift = QStringLiteral("\n\n"); + constexpr auto lineShift = "\n\n"_L1; const QString &informativeText = opt->informativeText(); const QString &detailedText = opt->detailedText(); @@ -39,7 +40,7 @@ inline QString QIOSMessageDialog::messageTextPlain() if (!detailedText.isEmpty()) text += lineShift + detailedText; - text.replace("<p>"_L1, QStringLiteral("\n"), Qt::CaseInsensitive); + text.replace("<p>"_L1, "\n"_L1, Qt::CaseInsensitive); text.remove(QRegularExpression(QStringLiteral("<[^>]*>"))); return text; @@ -91,6 +92,9 @@ bool QIOSMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality win || windowModality == Qt::NonModal) // We can only do modal dialogs return false; + if (!options()->checkBoxLabel().isNull()) + return false; // Can't support + m_alertController = [[UIAlertController alertControllerWithTitle:options()->windowTitle().toNSString() message:messageTextPlain().toNSString() @@ -112,7 +116,18 @@ bool QIOSMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality win [m_alertController addAction:createAction(NoButton)]; } - UIWindow *window = parent ? reinterpret_cast<UIView *>(parent->winId()).window : qt_apple_sharedApplication().keyWindow; + UIWindow *window = presentationWindow(parent); + if (!window) + return false; + + if (window.hidden) { + // With a window hidden, an attempt to present view controller + // below fails with a warning, that a view "is not a part of + // any view hierarchy". The UIWindow is initially hidden, + // as unhiding it is what hides the splash screen. + window.hidden = NO; + } + [window.rootViewController presentViewController:m_alertController animated:YES completion:nil]; return true; } diff --git a/src/plugins/platforms/ios/qiosplatformaccessibility.mm b/src/plugins/platforms/ios/qiosplatformaccessibility.mm index d54b7db57a..eb18ee637e 100644 --- a/src/plugins/platforms/ios/qiosplatformaccessibility.mm +++ b/src/plugins/platforms/ios/qiosplatformaccessibility.mm @@ -1,12 +1,15 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qiosplatformaccessibility.h" #if QT_CONFIG(accessibility) #include <QtGui/QtGui> #include "qioswindow.h" +#include "quiaccessibilityelement.h" QIOSPlatformAccessibility::QIOSPlatformAccessibility() {} @@ -25,8 +28,6 @@ void invalidateCache(QAccessibleInterface *iface) // This will invalidate everything regardless of what window the // interface belonged to. We might want to revisit this strategy later. // (Therefore this function still takes the interface as argument) - // It is also responsible for the bug that focus gets temporary lost - // when items get added or removed from the screen foreach (QWindow *win, QGuiApplication::topLevelWindows()) { if (win && win->handle()) { QT_PREPEND_NAMESPACE(QIOSWindow) *window = static_cast<QT_PREPEND_NAMESPACE(QIOSWindow) *>(win->handle()); @@ -38,14 +39,35 @@ void invalidateCache(QAccessibleInterface *iface) void QIOSPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) { - if (!isActive() || !event->accessibleInterface()) + auto *accessibleInterface = event->accessibleInterface(); + if (!isActive() || !accessibleInterface) return; switch (event->type()) { + case QAccessible::Focus: { + auto *element = [QMacAccessibilityElement elementWithId:event->uniqueId()]; + Q_ASSERT(element); + // There's no NSAccessibilityFocusedUIElementChangedNotification, like we have on + // macOS. Instead, the documentation for UIAccessibilityLayoutChangedNotification + // specifies that the optional argument to UIAccessibilityPostNotification is the + // accessibility element for VoiceOver to move to after processing the notification. + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, element); + break; + } case QAccessible::ObjectCreated: case QAccessible::ObjectShow: case QAccessible::ObjectHide: case QAccessible::ObjectDestroyed: - invalidateCache(event->accessibleInterface()); + invalidateCache(accessibleInterface); + switch (accessibleInterface->role()) { + case QAccessible::Window: + case QAccessible::Dialog: + // Bigger changes to the UI require a full reset of VoiceOver + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + break; + default: + // While smaller changes can be handled by re-reading the layout + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } break; default: break; diff --git a/src/plugins/platforms/ios/qiosscreen.h b/src/plugins/platforms/ios/qiosscreen.h index 21afb90c55..dd69428390 100644 --- a/src/plugins/platforms/ios/qiosscreen.h +++ b/src/plugins/platforms/ios/qiosscreen.h @@ -10,10 +10,6 @@ @class QIOSOrientationListener; -@interface QUIWindow : UIWindow -@property (nonatomic, readonly) BOOL sendingEvent; -@end - QT_BEGIN_NAMESPACE class QIOSScreen : public QObject, public QPlatformScreen @@ -21,7 +17,11 @@ class QIOSScreen : public QObject, public QPlatformScreen Q_OBJECT public: +#if !defined(Q_OS_VISIONOS) QIOSScreen(UIScreen *screen); +#else + QIOSScreen(); +#endif ~QIOSScreen(); QString name() const override; @@ -40,8 +40,9 @@ public: QPixmap grabWindow(WId window, int x, int y, int width, int height) const override; +#if !defined(Q_OS_VISIONOS) UIScreen *uiScreen() const; - UIWindow *uiWindow() const; +#endif void setUpdatesPaused(bool); @@ -50,15 +51,17 @@ public: private: void deliverUpdateRequests() const; - UIScreen *m_uiScreen; - UIWindow *m_uiWindow; +#if !defined(Q_OS_VISIONOS) + UIScreen *m_uiScreen = nullptr; +#endif QRect m_geometry; QRect m_availableGeometry; int m_depth; +#if !defined(Q_OS_VISIONOS) uint m_physicalDpi; +#endif QSizeF m_physicalSize; - QIOSOrientationListener *m_orientationListener; - CADisplayLink *m_displayLink; + CADisplayLink *m_displayLink = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosscreen.mm b/src/plugins/platforms/ios/qiosscreen.mm index ce0dedc88d..7559979f33 100644 --- a/src/plugins/platforms/ios/qiosscreen.mm +++ b/src/plugins/platforms/ios/qiosscreen.mm @@ -1,6 +1,8 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qiosglobal.h" #include "qiosintegration.h" #include "qiosscreen.h" @@ -10,11 +12,13 @@ #include "qiosviewcontroller.h" #include "quiview.h" #include "qiostheme.h" +#include "quiwindow.h" #include <QtCore/private/qcore_mac_p.h> #include <QtGui/qpointingdevice.h> #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qguiapplication_p.h> #include <private/qcoregraphics_p.h> #include <qpa/qwindowsysteminterface.h> @@ -43,6 +47,7 @@ typedef void (^DisplayLinkBlock)(CADisplayLink *displayLink); // ------------------------------------------------------------------------- +#if !defined(Q_OS_VISIONOS) static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) { foreach (QScreen *screen, QGuiApplication::screens()) { @@ -72,14 +77,17 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) + (void)screenConnected:(NSNotification*)notification { - Q_ASSERT_X(QIOSIntegration::instance(), Q_FUNC_INFO, - "Screen connected before QIOSIntegration creation"); + if (!QIOSIntegration::instance()) + return; // Will be added when QIOSIntegration is created QWindowSystemInterface::handleScreenAdded(new QIOSScreen([notification object])); } + (void)screenDisconnected:(NSNotification*)notification { + if (!QIOSIntegration::instance()) + return; + QIOSScreen *screen = qtPlatformScreenFor([notification object]); Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen disconnected that we didn't know about"); @@ -88,6 +96,9 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) + (void)screenModeChanged:(NSNotification*)notification { + if (!QIOSIntegration::instance()) + return; + QIOSScreen *screen = qtPlatformScreenFor([notification object]); Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen changed that we didn't know about"); @@ -96,96 +107,7 @@ static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen) @end -// ------------------------------------------------------------------------- - -@interface QIOSOrientationListener : NSObject -@end - -@implementation QIOSOrientationListener { - QIOSScreen *m_screen; -} - -- (instancetype)initWithQIOSScreen:(QIOSScreen *)screen -{ - self = [super init]; - if (self) { - m_screen = screen; -#ifndef Q_OS_TVOS - [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(orientationChanged:) - name:@"UIDeviceOrientationDidChangeNotification" object:nil]; -#endif - } - return self; -} - -- (void)dealloc -{ -#ifndef Q_OS_TVOS - [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:@"UIDeviceOrientationDidChangeNotification" object:nil]; -#endif - [super dealloc]; -} - -- (void)orientationChanged:(NSNotification *)notification -{ - Q_UNUSED(notification); - m_screen->updateProperties(); -} - -@end - -@interface UIScreen (Compatibility) -@property (nonatomic, readonly) CGRect qt_applicationFrame; -@end - -@implementation UIScreen (Compatibility) -- (CGRect)qt_applicationFrame -{ -#ifdef Q_OS_IOS - return self.applicationFrame; -#else - return self.bounds; -#endif -} -@end - -// ------------------------------------------------------------------------- - -@implementation QUIWindow - -- (instancetype)initWithFrame:(CGRect)frame -{ - if ((self = [super initWithFrame:frame])) - self->_sendingEvent = NO; - - return self; -} - -- (void)sendEvent:(UIEvent *)event -{ - QScopedValueRollback<BOOL> sendingEvent(self->_sendingEvent, YES); - [super sendEvent:event]; -} - -- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection -{ - [super traitCollectionDidChange:previousTraitCollection]; - - if (self.screen == UIScreen.mainScreen) { - if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) { - QIOSTheme::initializeSystemPalette(); - QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); - } - } -} - -@end +#endif // !defined(Q_OS_VISIONOS) // ------------------------------------------------------------------------- @@ -193,6 +115,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; +#if !defined(Q_OS_VISIONOS) /*! Returns the model identifier of the device. */ @@ -212,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(); @@ -251,44 +176,30 @@ QIOSScreen::QIOSScreen(UIScreen *screen) m_physicalDpi = 96; } - if (!qt_apple_isApplicationExtension()) { - for (UIWindow *existingWindow in qt_apple_sharedApplication().windows) { - if (existingWindow.screen == m_uiScreen) { - m_uiWindow = [m_uiWindow retain]; - break; - } - } - - if (!m_uiWindow) { - // Create a window and associated view-controller that we can use - m_uiWindow = [[QUIWindow alloc] initWithFrame:[m_uiScreen bounds]]; - m_uiWindow.rootViewController = [[[QIOSViewController alloc] initWithQIOSScreen:this] autorelease]; - } - } - - m_orientationListener = [[QIOSOrientationListener alloc] initWithQIOSScreen:this]; - - updateProperties(); - m_displayLink = [m_uiScreen displayLinkWithBlock:^(CADisplayLink *) { deliverUpdateRequests(); }]; m_displayLink.paused = YES; // Enabled when clients call QWindow::requestUpdate() [m_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; + +#endif // !defined(Q_OS_VISIONOS)) + + updateProperties(); } QIOSScreen::~QIOSScreen() { [m_displayLink invalidate]; - - [m_orientationListener release]; - [m_uiWindow release]; } QString QIOSScreen::name() const { +#if defined(Q_OS_VISIONOS) + return {}; +#else if (m_uiScreen == [UIScreen mainScreen]) return QString::fromNSString([UIDevice currentDevice].model) + " built-in display"_L1; else return "External display"_L1; +#endif } void QIOSScreen::updateProperties() @@ -296,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() ? @@ -347,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 @@ -431,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; @@ -452,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 @@ -491,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(); @@ -518,15 +401,12 @@ QPixmap QIOSScreen::grabWindow(WId window, int x, int y, int width, int height) return QPixmap::fromImage(qt_mac_toQImage(screenshot.CGImage)); } +#if !defined(Q_OS_VISIONOS) UIScreen *QIOSScreen::uiScreen() const { return m_uiScreen; } - -UIWindow *QIOSScreen::uiWindow() const -{ - return m_uiWindow; -} +#endif QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiostextinputoverlay.mm b/src/plugins/platforms/ios/qiostextinputoverlay.mm index 512ab77bd2..01046334a1 100644 --- a/src/plugins/platforms/ios/qiostextinputoverlay.mm +++ b/src/plugins/platforms/ios/qiostextinputoverlay.mm @@ -947,7 +947,17 @@ static void executeBlockWithoutAnimation(Block block) int cursorPosOnRelease = QPlatformInputContext::queryFocusObject(Qt::ImCursorPosition, touchPos).toInt(); if (cursorPosOnRelease == _cursorPosOnPress) { + // We've recognized a gesture to open the menu, but we don't know + // whether the user tapped a control that was overlaid our input + // area, since we don't do any granular hit-testing in touchesBegan. + // To ensure that the gesture doesn't eat touch events that should + // have reached another UI control we report the gesture as failed + // here, and then manually show the menu at the next runloop pass. _menuShouldBeVisible = true; + self.state = UIGestureRecognizerStateFailed; + dispatch_async(dispatch_get_main_queue(), ^{ + QIOSTextInputOverlay::s_editMenu.visible = _menuShouldBeVisible; + }); } else { // The menu is hidden, and the cursor will change position once // Qt receive the touch release. We therefore fail so that we @@ -996,19 +1006,26 @@ QIOSTextInputOverlay::~QIOSTextInputOverlay() void QIOSTextInputOverlay::updateFocusObject() { + // Destroy old recognizers since they were created with + // dependencies to the old focus object (focus view). if (m_cursorRecognizer) { - // Destroy old recognizers since they were created with - // dependencies to the old focus object (focus view). m_cursorRecognizer.enabled = NO; - m_selectionRecognizer.enabled = NO; - m_openMenuOnTapRecognizer.enabled = NO; [m_cursorRecognizer release]; - [m_selectionRecognizer release]; - [m_openMenuOnTapRecognizer release]; - [s_editMenu release]; m_cursorRecognizer = nullptr; + } + if (m_selectionRecognizer) { + m_selectionRecognizer.enabled = NO; + [m_selectionRecognizer release]; m_selectionRecognizer = nullptr; + } + if (m_openMenuOnTapRecognizer) { + m_openMenuOnTapRecognizer.enabled = NO; + [m_openMenuOnTapRecognizer release]; m_openMenuOnTapRecognizer = nullptr; + } + + if (s_editMenu) { + [s_editMenu release]; s_editMenu = nullptr; } diff --git a/src/plugins/platforms/ios/qiostextresponder.mm b/src/plugins/platforms/ios/qiostextresponder.mm index 8069ddb601..5231a3adde 100644 --- a/src/plugins/platforms/ios/qiostextresponder.mm +++ b/src/plugins/platforms/ios/qiostextresponder.mm @@ -164,7 +164,7 @@ { FirstResponderCandidate firstResponderCandidate(self); - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; if (![super becomeFirstResponder]) { qImDebug() << self << "was not allowed to become first responder"; @@ -178,7 +178,7 @@ - (BOOL)resignFirstResponder { - qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; + qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder]; // Don't allow activation events of the window that we're doing text on behalf on // to steal responder. @@ -196,11 +196,11 @@ // a regular responder transfer to another window. In the former case, iOS // will set the new first-responder to our next-responder, and in the latter // case we'll have an active responder candidate. - if (![UIResponder currentFirstResponder] && !FirstResponderCandidate::currentCandidate()) { + if (![UIResponder qt_currentFirstResponder] && !FirstResponderCandidate::currentCandidate()) { // No first responder set anymore, sync this with Qt by clearing the // focus object. m_inputContext->clearCurrentFocusObject(); - } else if ([UIResponder currentFirstResponder] == [self nextResponder]) { + } else if ([UIResponder qt_currentFirstResponder] == [self nextResponder]) { // We have resigned the keyboard, and transferred first responder back to the parent view Q_ASSERT(!FirstResponderCandidate::currentCandidate()); if ([self currentImeState:Qt::ImEnabled].toBool()) { @@ -222,8 +222,12 @@ - (UIResponder*)nextResponder { - return qApp->focusWindow() ? - reinterpret_cast<QUIView *>(qApp->focusWindow()->handle()->winId()) : 0; + // Make sure we have a handle/platform window before getting the winId(). + // In the dtor of QIOSWindow the platform window is set to null before calling + // removeFromSuperview which will end up calling nextResponder. That means it's + // possible that we can get here while the window is being torn down. + return (qApp->focusWindow() && qApp->focusWindow()->handle()) ? + reinterpret_cast<QUIView *>(qApp->focusWindow()->handle()->winId()) : 0; } // ------------------------------------------------------------------------- @@ -397,7 +401,7 @@ if (UIView *accessoryView = static_cast<UIView *>(platformData.value(kImePlatformDataInputAccessoryView).value<void *>())) self.inputAccessoryView = [[[WrapperView alloc] initWithView:accessoryView] autorelease]; -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) if (platformData.value(kImePlatformDataHideShortcutsBar).toBool()) { // According to the docs, leadingBarButtonGroups/trailingBarButtonGroups should be set to nil to hide the shortcuts bar. // However, starting with iOS 10, the API has been surrounded with NS_ASSUME_NONNULL, which contradicts this and causes @@ -898,7 +902,7 @@ QInputMethodEvent e(m_markedText, attrs); [self sendEventToFocusObject:e]; } - QRectF startRect = QPlatformInputContext::cursorRectangle();; + QRectF startRect = QPlatformInputContext::cursorRectangle(); attrs = QList<QInputMethodEvent::Attribute>(); attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.location + r.length, 0, 0); @@ -906,7 +910,7 @@ QInputMethodEvent e(m_markedText, attrs); [self sendEventToFocusObject:e]; } - QRectF endRect = QPlatformInputContext::cursorRectangle();; + QRectF endRect = QPlatformInputContext::cursorRectangle(); if (cursorPos != int(r.location + r.length) || cursorPos != anchorPos) { attrs = QList<QInputMethodEvent::Attribute>(); @@ -944,20 +948,20 @@ [self sendEventToFocusObject:e]; } -- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(UITextRange *)range +- (void)setBaseWritingDirection:(NSWritingDirection)writingDirection forRange:(UITextRange *)range { Q_UNUSED(writingDirection); Q_UNUSED(range); // Writing direction is handled by QLocale } -- (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction +- (NSWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction { Q_UNUSED(position); Q_UNUSED(direction); if (QLocale::system().textDirection() == Qt::RightToLeft) - return UITextWritingDirectionRightToLeft; - return UITextWritingDirectionLeftToRight; + return NSWritingDirectionRightToLeft; + return NSWritingDirectionLeftToRight; } - (UITextRange *)characterRangeByExtendingPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction diff --git a/src/plugins/platforms/ios/qiostheme.h b/src/plugins/platforms/ios/qiostheme.h index ea5169800b..f0a404a61a 100644 --- a/src/plugins/platforms/ios/qiostheme.h +++ b/src/plugins/platforms/ios/qiostheme.h @@ -21,15 +21,18 @@ public: const QPalette *palette(Palette type = SystemPalette) const override; QVariant themeHint(ThemeHint hint) const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QPlatformMenuItem* createPlatformMenuItem() const override; QPlatformMenu* createPlatformMenu() const override; +#endif bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; static const char *name; diff --git a/src/plugins/platforms/ios/qiostheme.mm b/src/plugins/platforms/ios/qiostheme.mm index 02143bd7a2..3853de9cf1 100644 --- a/src/plugins/platforms/ios/qiostheme.mm +++ b/src/plugins/platforms/ios/qiostheme.mm @@ -11,16 +11,24 @@ #include <QtGui/private/qcoregraphics_p.h> #include <QtGui/private/qcoretextfontdatabase_p.h> +#include <QtGui/private/qappleiconengine_p.h> #include <QtGui/private/qguiapplication_p.h> #include <qpa/qplatformintegration.h> #include <UIKit/UIFont.h> #include <UIKit/UIInterface.h> -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) #include "qiosmenu.h" +#endif + +#if !defined(Q_OS_TVOS) #include "qiosfiledialog.h" +#include "qioscolordialog.h" +#include "qiosfontdialog.h" #include "qiosmessagedialog.h" +#include "qiosscreen.h" +#include "quiwindow.h" #endif QT_BEGIN_NAMESPACE @@ -66,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 @@ -75,29 +86,25 @@ const QPalette *QIOSTheme::palette(QPlatformTheme::Palette type) const return 0; } +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QPlatformMenuItem* QIOSTheme::createPlatformMenuItem() const { -#ifdef Q_OS_TVOS - return 0; -#else - return new QIOSMenuItem(); -#endif + return new QIOSMenuItem; } QPlatformMenu* QIOSTheme::createPlatformMenu() const { -#ifdef Q_OS_TVOS - return 0; -#else - return new QIOSMenu(); -#endif + return new QIOSMenu; } +#endif bool QIOSTheme::usePlatformNativeDialog(QPlatformTheme::DialogType type) const { switch (type) { case FileDialog: case MessageDialog: + case ColorDialog: + case FontDialog: return !qt_apple_isApplicationExtension(); default: return false; @@ -114,6 +121,12 @@ QPlatformDialogHelper *QIOSTheme::createPlatformDialogHelper(QPlatformTheme::Dia case MessageDialog: return new QIOSMessageDialog(); break; + case ColorDialog: + return new QIOSColorDialog(); + break; + case FontDialog: + return new QIOSFontDialog(); + break; #endif default: return 0; @@ -132,15 +145,30 @@ QVariant QIOSTheme::themeHint(ThemeHint hint) const } } -QPlatformTheme::Appearance QIOSTheme::appearance() const +Qt::ColorScheme QIOSTheme::colorScheme() const { - if (UIWindow *window = qt_apple_sharedApplication().windows.lastObject) { - return window.rootViewController.traitCollection.userInterfaceStyle - == UIUserInterfaceStyleDark - ? QPlatformTheme::Appearance::Dark - : QPlatformTheme::Appearance::Light; +#if defined(Q_OS_VISIONOS) + // On visionOS the concept of light or dark mode does not + // apply, as the UI is constantly changing based on what + // the lighting conditions are outside the headset, but + // the OS reports itself as always being in dark mode. + return Qt::ColorScheme::Dark; +#else + // Set the appearance based on the QUIWindow + // Fallback to the UIScreen if no window is created yet + UIUserInterfaceStyle appearance = UIScreen.mainScreen.traitCollection.userInterfaceStyle; + NSArray<UIWindow *> *windows = qt_apple_sharedApplication().windows; + for (UIWindow *window in windows) { + if ([window isKindOfClass:[QUIWindow class]]) { + appearance = static_cast<QUIWindow*>(window).traitCollection.userInterfaceStyle; + break; + } } - return QPlatformTheme::Appearance::Unknown; + + return appearance == UIUserInterfaceStyleDark + ? Qt::ColorScheme::Dark + : Qt::ColorScheme::Light; +#endif } const QFont *QIOSTheme::font(Font type) const @@ -150,4 +178,9 @@ const QFont *QIOSTheme::font(Font type) const return coreTextFontDatabase->themeFont(type); } +QIconEngine *QIOSTheme::createIconEngine(const QString &iconName) const +{ + return new QAppleIconEngine(iconName); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosviewcontroller.h b/src/plugins/platforms/ios/qiosviewcontroller.h index 237cd57edf..1f8da41ba4 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.h +++ b/src/plugins/platforms/ios/qiosviewcontroller.h @@ -12,14 +12,12 @@ QT_END_NAMESPACE @interface QIOSViewController : UIViewController -- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen; +- (instancetype)initWithWindow:(UIWindow*)window andScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen; - (void)updateProperties; - (NSArray*)keyCommands; - (void)handleShortcut:(UIKeyCommand*)keyCommand; #ifndef Q_OS_TVOS -@property (nonatomic, assign) UIInterfaceOrientation lockedOrientation; - // UIViewController @property (nonatomic, assign) BOOL prefersStatusBarHidden; @property (nonatomic, assign) UIStatusBarAnimation preferredStatusBarUpdateAnimation; diff --git a/src/plugins/platforms/ios/qiosviewcontroller.mm b/src/plugins/platforms/ios/qiosviewcontroller.mm index c4e8968232..436d1e7bed 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.mm +++ b/src/plugins/platforms/ios/qiosviewcontroller.mm @@ -21,9 +21,12 @@ #include "qioswindow.h" #include "quiview.h" +#include <QtCore/qpointer.h> + // ------------------------------------------------------------------------- @interface QIOSViewController () +@property (nonatomic, assign) UIWindow *window; @property (nonatomic, assign) QPointer<QT_PREPEND_NAMESPACE(QIOSScreen)> platformScreen; @property (nonatomic, assign) BOOL changingOrientation; @end @@ -88,27 +91,30 @@ { Q_UNUSED(subview); - QT_PREPEND_NAMESPACE(QIOSScreen) *screen = self.qtViewController.platformScreen; - - // The 'window' property of our view is not valid until the window - // has been shown, so we have to access it through the QIOSScreen. - UIWindow *uiWindow = screen->uiWindow(); + // Track UIWindow via explicit property on QIOSViewController, + // as the window property of our own view is not valid until + // the window has been shown (below). + UIWindow *uiWindow = self.qtViewController.window; if (uiWindow.hidden) { - // Associate UIWindow to screen and show it the first time a QWindow - // is mapped to the screen. For external screens this means disabling - // mirroring mode and presenting alternate content on the screen. - uiWindow.screen = screen->uiScreen(); + // Show the UIWindow the first time a QWindow is mapped to the screen. + // For the main screen this hides the launch screen, while for external + // screens this disables mirroring of the main screen, so the external + // screen can be used for alternate content. uiWindow.hidden = NO; } } +#if !defined(Q_OS_VISIONOS) - (void)willRemoveSubview:(UIView *)subview { Q_UNUSED(subview); - Q_ASSERT(self.window); UIWindow *uiWindow = self.window; + // uiWindow can be null when closing from the ios "app manager" and the app is + // showing a native window like UIDocumentBrowserViewController + if (!uiWindow) + return; if (uiWindow.screen != [UIScreen mainScreen] && self.subviews.count == 1) { // We're about to remove the last view of an external screen, so go back @@ -116,10 +122,10 @@ // to ensure that we don't try to layout the view that's being removed. dispatch_async(dispatch_get_main_queue(), ^{ uiWindow.hidden = YES; - uiWindow.screen = [UIScreen mainScreen]; }); } } +#endif - (void)layoutSubviews { @@ -225,15 +231,14 @@ @synthesize preferredStatusBarStyle; #endif -- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen +- (instancetype)initWithWindow:(UIWindow*)window andScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen { if (self = [self init]) { + self.window = window; self.platformScreen = screen; self.changingOrientation = NO; #ifndef Q_OS_TVOS - self.lockedOrientation = UIInterfaceOrientationUnknown; - // Status bar may be initially hidden at startup through Info.plist self.prefersStatusBarHidden = infoPlistValue(@"UIStatusBarHidden", false); self.preferredStatusBarUpdateAnimation = UIStatusBarAnimationNone; @@ -280,7 +285,7 @@ Q_ASSERT(!qt_apple_isApplicationExtension()); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(willChangeStatusBarFrame:) name:UIApplicationWillChangeStatusBarFrameNotification @@ -290,6 +295,15 @@ name:UIApplicationDidChangeStatusBarOrientationNotification object:qt_apple_sharedApplication()]; #endif + + // Make sure any top level windows that have already been created + // for this screen are reparented into our desktop manager view. + for (auto *window : qGuiApp->topLevelWindows()) { + if (window->screen()->handle() != self.platformScreen) + continue; + if (auto *platformWindow = window->handle()) + platformWindow->setParent(nullptr); + } } - (void)viewDidUnload @@ -300,26 +314,6 @@ // ------------------------------------------------------------------------- -- (BOOL)shouldAutorotate -{ -#ifndef Q_OS_TVOS - return self.platformScreen && self.platformScreen->uiScreen() == [UIScreen mainScreen] && !self.lockedOrientation; -#else - return NO; -#endif -} - -- (NSUInteger)supportedInterfaceOrientations -{ - // As documented by Apple in the iOS 6.0 release notes, setStatusBarOrientation:animated: - // only works if the supportedInterfaceOrientations of the view controller is 0, making - // us responsible for ensuring that the status bar orientation is consistent. We enter - // this mode when auto-rotation is disabled due to an explicit content orientation being - // set on the focus window. Note that this is counter to what the documentation for - // supportedInterfaceOrientations says, which states that the method should not return 0. - return [self shouldAutorotate] ? UIInterfaceOrientationMaskAll : 0; -} - - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration { self.changingOrientation = YES; @@ -334,6 +328,7 @@ [super didRotateFromInterfaceOrientation:orientation]; } +#if !defined(Q_OS_VISIONOS) - (void)willChangeStatusBarFrame:(NSNotification*)notification { Q_UNUSED(notification); @@ -377,6 +372,7 @@ [self.view setNeedsLayout]; } +#endif - (void)viewWillLayoutSubviews { @@ -397,10 +393,12 @@ if (!self.platformScreen || !self.platformScreen->screen()) return; +#if !defined(Q_OS_VISIONOS) // For now we only care about the main screen, as both the statusbar // visibility and orientation is only appropriate for the main screen. if (self.platformScreen->uiScreen() != [UIScreen mainScreen]) return; +#endif // Prevent recursion caused by updating the status bar appearance (position // or visibility), which in turn may cause a layout of our subviews, and @@ -427,7 +425,7 @@ // All decisions are based on the top level window focusWindow = qt_window_private(focusWindow)->topLevelWindow(); -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) // -------------- Status bar style and visbility --------------- @@ -447,51 +445,6 @@ [self setNeedsStatusBarAppearanceUpdate]; [self.view setNeedsLayout]; } - - - // -------------- Content orientation --------------- - - UIApplication *uiApplication = qt_apple_sharedApplication(); - - static BOOL kAnimateContentOrientationChanges = YES; - - Qt::ScreenOrientation contentOrientation = focusWindow->contentOrientation(); - if (contentOrientation != Qt::PrimaryOrientation) { - // An explicit content orientation has been reported for the focus window, - // so we keep the status bar in sync with content orientation. This will ensure - // that the task bar (and associated gestures) are also rotated accordingly. - - if (!self.lockedOrientation) { - // We are moving from Qt::PrimaryOrientation to an explicit orientation, - // so we need to store the current statusbar orientation, as we need it - // later when mapping screen coordinates for QScreen and for returning - // to Qt::PrimaryOrientation. - self.lockedOrientation = uiApplication.statusBarOrientation; - } - - [uiApplication setStatusBarOrientation: - UIInterfaceOrientation(fromQtScreenOrientation(contentOrientation)) - animated:kAnimateContentOrientationChanges]; - - } else { - // The content orientation is set to Qt::PrimaryOrientation, meaning - // that auto-rotation should be enabled. But we may be coming out of - // a state of locked orientation, which needs some cleanup before we - // can enable auto-rotation again. - if (self.lockedOrientation) { - // First we need to restore the statusbar to what it was at the - // time of locking the orientation, otherwise iOS will be very - // confused when it starts doing auto-rotation again. - [uiApplication setStatusBarOrientation:self.lockedOrientation - animated:kAnimateContentOrientationChanges]; - - // Then we can re-enable auto-rotation - self.lockedOrientation = UIInterfaceOrientationUnknown; - - // And finally let iOS rotate the root view to match the device orientation - [UIViewController attemptRotationToDeviceOrientation]; - } - } #endif } diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 988269a855..88afee80c3 100644 --- a/src/plugins/platforms/ios/qioswindow.h +++ b/src/plugins/platforms/ios/qioswindow.h @@ -21,14 +21,13 @@ class QIOSWindow : public QObject, public QPlatformWindow Q_OBJECT public: - explicit QIOSWindow(QWindow *window); + explicit QIOSWindow(QWindow *window, WId nativeHandle = 0); ~QIOSWindow(); void setGeometry(const QRect &rect) override; void setWindowState(Qt::WindowStates state) override; void setParent(const QPlatformWindow *window) override; - void handleContentOrientationChange(Qt::ScreenOrientation orientation) override; void setVisible(bool visible) override; void setOpacity(qreal level) override; @@ -56,13 +55,20 @@ public: void requestUpdate() override; + void setMask(const QRegion ®ion) override; + +#if QT_CONFIG(opengl) CAEAGLLayer *eaglLayer() const; +#endif + + bool isForeignWindow() const override; + UIView *view() const; private: void applicationStateChanged(Qt::ApplicationState state); void applyGeometry(const QRect &rect); - QUIView *m_view; + UIView *m_view; QRect m_normalGeometry; int m_windowLevel; @@ -78,6 +84,8 @@ private: QDebug operator<<(QDebug debug, const QIOSWindow *window); #endif +QT_MANGLE_NAMESPACE(QUIView) *quiview_cast(UIView *view); + QT_END_NAMESPACE #endif // QIOSWINDOW_H diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index e88f5ab3d0..6a1080e238 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -4,7 +4,6 @@ #include "qioswindow.h" #include "qiosapplicationdelegate.h" -#include "qioscontext.h" #include "qiosglobal.h" #include "qiosintegration.h" #include "qiosscreen.h" @@ -12,11 +11,17 @@ #include "quiview.h" #include "qiosinputcontext.h" +#include <QtCore/private/qcore_mac_p.h> + #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <qpa/qplatformintegration.h> +#if QT_CONFIG(opengl) #import <QuartzCore/CAEAGLLayer.h> -#ifdef Q_OS_IOS +#endif + +#if QT_CONFIG(metal) #import <QuartzCore/CAMetalLayer.h> #endif @@ -24,34 +29,48 @@ QT_BEGIN_NAMESPACE -QIOSWindow::QIOSWindow(QWindow *window) +enum { + defaultWindowWidth = 160, + defaultWindowHeight = 160 +}; + +QIOSWindow::QIOSWindow(QWindow *window, WId nativeHandle) : QPlatformWindow(window) , m_windowLevel(0) { -#ifdef Q_OS_IOS - if (window->surfaceType() == QSurface::RasterSurface) - window->setSurfaceType(QSurface::MetalSurface); + if (nativeHandle) { + m_view = reinterpret_cast<UIView *>(nativeHandle); + [m_view retain]; + } else { +#if QT_CONFIG(metal) + if (window->surfaceType() == QSurface::RasterSurface) + window->setSurfaceType(QSurface::MetalSurface); - if (window->surfaceType() == QSurface::MetalSurface) - m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; - else + if (window->surfaceType() == QSurface::MetalSurface) + m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; + else #endif - m_view = [[QUIView alloc] initWithQIOSWindow:this]; + m_view = [[QUIView alloc] initWithQIOSWindow:this]; + } connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QIOSWindow::applicationStateChanged); - setParent(QPlatformWindow::parent()); + if (QPlatformWindow::parent()) + setParent(QPlatformWindow::parent()); - // Resolve default window geometry in case it was not set before creating the - // platform window. This picks up eg. minimum-size if set, and defaults to - // the "maxmized" geometry (even though we're not in that window state). - // FIXME: Detect if we apply a maximized geometry and send a window state - // change event in that case. - m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), - screen()->availableGeometry().width(), screen()->availableGeometry().height()); + if (!isForeignWindow()) { + // Resolve default window geometry in case it was not set before creating the + // platform window. This picks up eg. minimum-size if set. + m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), + defaultWindowWidth, defaultWindowHeight); - setWindowState(window->windowStates()); - setOpacity(window->opacity()); + setWindowState(window->windowStates()); + setOpacity(window->opacity()); + setMask(QHighDpi::toNativeLocalRegion(window->mask(), window)); + } else { + // Pick up essential foreign window state + QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect()); + } Qt::ScreenOrientation initialOrientation = window->contentOrientation(); if (initialOrientation != Qt::PrimaryOrientation) { @@ -73,8 +92,15 @@ QIOSWindow::~QIOSWindow() [m_view touchesCancelled:[NSSet set] withEvent:0]; clearAccessibleCache(); - m_view.platformWindow = 0; - [m_view removeFromSuperview]; + + quiview_cast(m_view).platformWindow = nullptr; + + // Remove from superview, unless we're a foreign window without a + // Qt window parent, in which case the foreign window is used as + // a window container for a Qt UI hierarchy inside a native UI. + if (!(isForeignWindow() && !QPlatformWindow::parent())) + [m_view removeFromSuperview]; + [m_view release]; } @@ -113,7 +139,7 @@ void QIOSWindow::setVisible(bool visible) if (visible && shouldAutoActivateWindow()) { if (!window()->property("_q_showWithoutActivating").toBool()) requestActivateWindow(); - } else if (!visible && [m_view isActiveWindow]) { + } else if (!visible && [quiview_cast(m_view) isActiveWindow]) { // Our window was active/focus window but now hidden, so relinquish // focus to the next possible window in the stack. NSArray<UIView *> *subviews = m_view.viewController.view.subviews; @@ -202,7 +228,7 @@ void QIOSWindow::applyGeometry(const QRect &rect) QMargins QIOSWindow::safeAreaMargins() const { - UIEdgeInsets safeAreaInsets = m_view.qt_safeAreaInsets; + UIEdgeInsets safeAreaInsets = m_view.safeAreaInsets; return QMargins(safeAreaInsets.left, safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom); } @@ -226,22 +252,45 @@ void QIOSWindow::setWindowState(Qt::WindowStates state) if (state & Qt::WindowMinimized) { applyGeometry(QRect()); } else if (state & (Qt::WindowFullScreen | Qt::WindowMaximized)) { - // When an application is in split-view mode, the UIScreen still has the - // same geometry, but the UIWindow is resized to the area reserved for the - // application. We use this to constrain the geometry used when applying the - // fullscreen or maximized window states. Note that we do not do this - // in applyGeometry(), as we don't want to artificially limit window - // placement "outside" of the screen bounds if that's what the user wants. - QRect uiWindowBounds = QRectF::fromCGRect(m_view.window.bounds).toRect(); - QRect fullscreenGeometry = screen()->geometry().intersected(uiWindowBounds); - QRect maximizedGeometry = window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint ? - fullscreenGeometry : screen()->availableGeometry().intersected(uiWindowBounds); + if (NSProcessInfo.processInfo.iOSAppOnMac) { + // iOS apps running as "Designed for iPad" on macOS do not match + // our current window management implementation where a single + // UIWindow is tied to a single screen. And even if we're on the + // right screen, the UIScreen does not account for the 77% scale + // of the UIUserInterfaceIdiomPad environment, so we can't use + // it to clamp the window geometry. Instead just use the UIWindow + // directly, which represents our "screen". + applyGeometry(uiWindowBounds); + } else if (isRunningOnVisionOS()) { + // On visionOS there is no concept of a screen, and hence no concept of + // screen-relative system UI that we should keep top level windows away + // from, so don't apply the UIWindow safe area insets to the screen. + applyGeometry(uiWindowBounds); + } else { + QRect fullscreenGeometry = screen()->geometry(); + QRect maximizedGeometry = fullscreenGeometry; + +#if !defined(Q_OS_VISIONOS) + if (!(window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint)) { + // If the safe area margins reflect the screen's outer edges, + // then reduce the maximized geometry accordingly. Otherwise + // leave it as is, and assume the client will take the safe + // are margins into account explicitly. + UIScreen *uiScreen = m_view.window.windowScene.screen; + UIEdgeInsets safeAreaInsets = m_view.window.safeAreaInsets; + if (m_view.window.bounds.size.width == uiScreen.bounds.size.width) + maximizedGeometry.adjust(safeAreaInsets.left, 0, -safeAreaInsets.right, 0); + if (m_view.window.bounds.size.height == uiScreen.bounds.size.height) + maximizedGeometry.adjust(0, safeAreaInsets.top, 0, -safeAreaInsets.bottom); + } +#endif - if (state & Qt::WindowFullScreen) - applyGeometry(fullscreenGeometry); - else - applyGeometry(maximizedGeometry); + if (state & Qt::WindowFullScreen) + applyGeometry(fullscreenGeometry.intersected(uiWindowBounds)); + else + applyGeometry(maximizedGeometry.intersected(uiWindowBounds)); + } } else { applyGeometry(m_normalGeometry); } @@ -249,10 +298,16 @@ void QIOSWindow::setWindowState(Qt::WindowStates state) void QIOSWindow::setParent(const QPlatformWindow *parentWindow) { - UIView *parentView = parentWindow ? reinterpret_cast<UIView *>(parentWindow->winId()) - : isQtApplication() ? static_cast<QIOSScreen *>(screen())->uiWindow().rootViewController.view : 0; - - [parentView addSubview:m_view]; + UIView *superview = nullptr; + if (parentWindow) + superview = reinterpret_cast<UIView *>(parentWindow->winId()); + else if (isQtApplication() && !isForeignWindow()) + superview = rootViewForScreen(window()->screen()); + + if (superview) + [superview addSubview:m_view]; + else if (quiview_cast(m_view.superview)) + [m_view removeFromSuperview]; } void QIOSWindow::requestActivateWindow() @@ -263,7 +318,6 @@ void QIOSWindow::requestActivateWindow() if (blockedByModal()) return; - Q_ASSERT(m_view.window); [m_view.window makeKeyWindow]; [m_view becomeFirstResponder]; @@ -273,8 +327,6 @@ void QIOSWindow::requestActivateWindow() void QIOSWindow::raiseOrLower(bool raise) { - // Re-insert m_view at the correct index among its sibling views - // (QWindows) according to their current m_windowLevel: if (!isQtApplication()) return; @@ -282,17 +334,27 @@ void QIOSWindow::raiseOrLower(bool raise) if (subviews.count == 1) return; - for (int i = int(subviews.count) - 1; i >= 0; --i) { - UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); - if (view.hidden || view == m_view || !view.qwindow) - continue; - int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; - if (m_windowLevel > level || (raise && m_windowLevel == level)) { - [m_view.superview insertSubview:m_view aboveSubview:view]; - return; + if (m_view.superview == m_view.qtViewController.view) { + // We're a top level window, so we need to take window + // levels into account. + for (int i = int(subviews.count) - 1; i >= 0; --i) { + UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); + if (view.hidden || view == m_view || !view.qwindow) + continue; + int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; + if (m_windowLevel > level || (raise && m_windowLevel == level)) { + [m_view.superview insertSubview:m_view aboveSubview:view]; + return; + } } + [m_view.superview insertSubview:m_view atIndex:0]; + } else { + // Child window, or embedded into a non-Qt view controller + if (raise) + [m_view.superview bringSubviewToFront:m_view]; + else + [m_view.superview sendSubviewToBack:m_view]; } - [m_view.superview insertSubview:m_view atIndex:0]; } void QIOSWindow::updateWindowLevel() @@ -321,20 +383,13 @@ void QIOSWindow::updateWindowLevel() m_windowLevel = qMax(transientParentWindow->m_windowLevel, m_windowLevel); } -void QIOSWindow::handleContentOrientationChange(Qt::ScreenOrientation orientation) -{ - // Update the QWindow representation straight away, so that - // we can update the statusbar orientation based on the new - // content orientation. - qt_window_private(window())->contentOrientation = orientation; - - [m_view.qtViewController updateProperties]; -} - void QIOSWindow::applicationStateChanged(Qt::ApplicationState) { + if (isForeignWindow()) + return; + if (window()->isExposed() != isExposed()) - [m_view sendUpdatedExposeEvent]; + [quiview_cast(m_view) sendUpdatedExposeEvent]; } qreal QIOSWindow::devicePixelRatio() const @@ -344,7 +399,10 @@ qreal QIOSWindow::devicePixelRatio() const void QIOSWindow::clearAccessibleCache() { - [m_view clearAccessibleCache]; + if (isForeignWindow()) + return; + + [quiview_cast(m_view) clearAccessibleCache]; } void QIOSWindow::requestUpdate() @@ -352,11 +410,27 @@ void QIOSWindow::requestUpdate() static_cast<QIOSScreen *>(screen())->setUpdatesPaused(false); } +void QIOSWindow::setMask(const QRegion ®ion) +{ + if (!region.isEmpty()) { + QCFType<CGMutablePathRef> maskPath = CGPathCreateMutable(); + for (const QRect &r : region) + CGPathAddRect(maskPath, nullptr, r.toCGRect()); + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.path = maskPath; + m_view.layer.mask = maskLayer; + } else { + m_view.layer.mask = nil; + } +} + +#if QT_CONFIG(opengl) CAEAGLLayer *QIOSWindow::eaglLayer() const { Q_ASSERT([m_view.layer isKindOfClass:[CAEAGLLayer class]]); return static_cast<CAEAGLLayer *>(m_view.layer); } +#endif #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug debug, const QIOSWindow *window) @@ -371,6 +445,37 @@ QDebug operator<<(QDebug debug, const QIOSWindow *window) } #endif // !QT_NO_DEBUG_STREAM +/*! + Returns the view cast to a QUIview if possible. + + If the view is not a QUIview, nil is returned, which is safe to + send messages to, effectively making [quiview_cast(view) message] + a no-op. + + For extra verbosity and clearer code, please consider checking + that the platform window is not a foreign window before using + this cast, via QPlatformWindow::isForeignWindow(). + + Do not use this method solely to check for foreign windows, as + that will make the code harder to read for people not working + primarily on iOS, who do not know the difference between the + UIView and QUIView cases. +*/ +QUIView *quiview_cast(UIView *view) +{ + return qt_objc_cast<QUIView *>(view); +} + +bool QIOSWindow::isForeignWindow() const +{ + return ![m_view isKindOfClass:QUIView.class]; +} + +UIView *QIOSWindow::view() const +{ + return m_view; +} + QT_END_NAMESPACE #include "moc_qioswindow.cpp" diff --git a/src/plugins/platforms/ios/quiaccessibilityelement.h b/src/plugins/platforms/ios/quiaccessibilityelement.h index e78fef6d30..8580325436 100644 --- a/src/plugins/platforms/ios/quiaccessibilityelement.h +++ b/src/plugins/platforms/ios/quiaccessibilityelement.h @@ -14,7 +14,7 @@ @property (readonly) QAccessible::Id axid; - (instancetype)initWithId:(QAccessible::Id)anId withAccessibilityContainer:(id)view; -+ (instancetype)elementWithId:(QAccessible::Id)anId withAccessibilityContainer:(id)view; ++ (instancetype)elementWithId:(QAccessible::Id)anId; @end diff --git a/src/plugins/platforms/ios/quiaccessibilityelement.mm b/src/plugins/platforms/ios/quiaccessibilityelement.mm index 08e366f32b..39b2cb8a50 100644 --- a/src/plugins/platforms/ios/quiaccessibilityelement.mm +++ b/src/plugins/platforms/ios/quiaccessibilityelement.mm @@ -8,6 +8,7 @@ #include "private/qaccessiblecache_p.h" #include "private/qcore_mac_p.h" #include "uistrings_p.h" +#include "qioswindow.h" QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); @@ -23,7 +24,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); return self; } -+ (instancetype)elementWithId:(QAccessible::Id)anId withAccessibilityContainer:(id)view ++ (instancetype)elementWithId:(QAccessible::Id)anId { Q_ASSERT(anId); if (!anId) @@ -33,9 +34,17 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); QMacAccessibilityElement *element = cache->elementForId(anId); if (!element) { - Q_ASSERT(QAccessible::accessibleInterface(anId)); - element = [[self alloc] initWithId:anId withAccessibilityContainer:view]; - cache->insertElement(anId, element); + auto *a11yInterface = QAccessible::accessibleInterface(anId); + Q_ASSERT(a11yInterface); + auto *window = a11yInterface->window(); + if (window && window->handle()) { + auto *platformWindow = static_cast<QIOSWindow*>(window->handle()); + element = [[self alloc] initWithId:anId withAccessibilityContainer:platformWindow->view()]; + cache->insertElement(anId, element); + } else { + qWarning() << "Could not create a11y element for" << window + << "with platform window" << (window ? window->handle() : nullptr); + } } return element; } diff --git a/src/plugins/platforms/ios/quiview.h b/src/plugins/platforms/ios/quiview.h index 6d1d19fd3d..7899ec6e0e 100644 --- a/src/plugins/platforms/ios/quiview.h +++ b/src/plugins/platforms/ios/quiview.h @@ -32,10 +32,9 @@ QT_END_NAMESPACE - (QWindow *)qwindow; - (UIViewController *)viewController; - (QIOSViewController*)qtViewController; -@property (nonatomic, readonly) UIEdgeInsets qt_safeAreaInsets; @end -#ifdef Q_OS_IOS +#if QT_CONFIG(metal) @interface QUIMetalView : QUIView @end #endif diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm index 63dfab674c..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 @@ -82,7 +82,10 @@ inline ulong getTimeStamp(UIEvent *event) + (Class)layerClass { +#if QT_CONFIG(opengl) return [CAEAGLLayer class]; +#endif + return [super layerClass]; } - (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window @@ -105,6 +108,17 @@ inline ulong getTimeStamp(UIEvent *event) m_lastScrollDelta = CGPointZero; m_lastScrollCursorPos = CGPointZero; [self addGestureRecognizer:m_scrollGestureRecognizer]; + + if ([self.layer isKindOfClass:CAMetalLayer.class]) { + QWindow *window = self.platformWindow->window(); + if (QColorSpace colorSpace = window->format().colorSpace(); colorSpace.isValid()) { + QCFType<CFDataRef> iccData = colorSpace.iccProfile().toCFData(); + QCFType<CGColorSpaceRef> cgColorSpace = CGColorSpaceCreateWithICCData(iccData); + CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(self.layer); + metalLayer.colorspace = cgColorSpace; + qCDebug(lcQpaWindow) << "Set" << self << "color space to" << metalLayer.colorspace; + } + } } return self; @@ -113,6 +127,7 @@ inline ulong getTimeStamp(UIEvent *event) - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { +#if QT_CONFIG(opengl) if ([self.layer isKindOfClass:[CAEAGLLayer class]]) { // Set up EAGL layer CAEAGLLayer *eaglLayer = static_cast<CAEAGLLayer *>(self.layer); @@ -122,6 +137,7 @@ inline ulong getTimeStamp(UIEvent *event) kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 }; } +#endif if (isQtApplication()) self.hidden = YES; @@ -182,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 @@ -191,6 +208,7 @@ inline ulong getTimeStamp(UIEvent *event) // FIXME: Allow the scale factor to be customized through QSurfaceFormat. } +#endif - (void)didAddSubview:(UIView *)subview { @@ -248,6 +266,9 @@ inline ulong getTimeStamp(UIEvent *event) Q_UNUSED(layer); Q_ASSERT(layer == self.layer); + if (!self.platformWindow) + return; + [self sendUpdatedExposeEvent]; } @@ -289,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"; @@ -300,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"; @@ -328,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; @@ -337,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; } @@ -351,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; @@ -502,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 @@ -544,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 @@ -579,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); @@ -615,13 +635,33 @@ inline ulong getTimeStamp(UIEvent *event) return Qt::Key_unknown; } -- (bool)processPresses:(NSSet *)presses withType:(QEvent::Type)type { +- (bool)isControlKey:(Qt::Key)key +{ + switch (key) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Left: + case Qt::Key_Right: + return true; + default: + break; + } + + return false; +} + +- (bool)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type +{ // Presses on Menu button will generate a Menu key event. By default, not handling // this event will cause the application to return to Headboard (tvOS launcher). // When handling the event (for example, as a back button), both press and // release events must be handled accordingly. + if (!qApp->focusWindow()) + return false; + + bool eventHandled = false; + const bool imEnabled = QIOSInputContext::instance()->inputMethodAccepted(); - bool handled = false; for (UIPress* press in presses) { Qt::KeyboardModifiers qtModifiers = Qt::NoModifier; if (@available(ios 13.4, *)) @@ -630,26 +670,15 @@ inline ulong getTimeStamp(UIEvent *event) int key = [self mapPressTypeToKey:press withModifiers:qtModifiers text:text]; if (key == Qt::Key_unknown) continue; - if (QWindowSystemInterface::handleKeyEvent(self.platformWindow->window(), type, key, - qtModifiers, text)) { - handled = true; - } - } - - return handled; -} + if (imEnabled && ![self isControlKey:Qt::Key(key)]) + continue; -- (BOOL)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type -{ - bool handlePress = false; - if (qApp->focusWindow()) { - QInputMethodQueryEvent queryEvent(Qt::ImEnabled); - if (qApp->focusObject() && QCoreApplication::sendEvent(qApp->focusObject(), &queryEvent)) - handlePress = queryEvent.value(Qt::ImEnabled).toBool(); - if (!handlePress && [self processPresses:presses withType:type]) - return true; + bool keyHandled = QWindowSystemInterface::handleKeyEvent( + self.platformWindow->window(), type, key, qtModifiers, text); + eventHandled = eventHandled || keyHandled; } - return false; + + return eventHandled; } - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event @@ -661,20 +690,20 @@ inline ulong getTimeStamp(UIEvent *event) - (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event { if (![self handlePresses:presses eventType:QEvent::KeyPress]) - [super pressesBegan:presses withEvent:event]; + [super pressesChanged:presses withEvent:event]; [super pressesChanged:presses withEvent:event]; } - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event { if (![self handlePresses:presses eventType:QEvent::KeyRelease]) - [super pressesBegan:presses withEvent:event]; + [super pressesEnded:presses withEvent:event]; [super pressesEnded:presses withEvent:event]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { -#ifndef Q_OS_TVOS +#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) // Check first if QIOSMenu should handle the action before continuing up the responder chain return [QIOSMenu::menuActionTarget() targetForAction:action withSender:sender] != 0; #else @@ -687,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; @@ -803,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 bb27d832d1..04e1f8cfb3 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -23,9 +23,11 @@ if (!iface || iface->state().invisible) return; - [self createAccessibleElement: iface]; for (int i = 0; i < iface->childCount(); ++i) [self createAccessibleContainer: iface->child(i)]; + + // The container element must go last, so that it underlays all its children + [self createAccessibleElement:iface]; } - (void)initAccessibility @@ -52,7 +54,6 @@ - (void)clearAccessibleCache { [m_accessibleElements removeAllObjects]; - UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, @""); } // this is a container, returning yes here means the functions below will never be called diff --git a/src/plugins/platforms/ios/quiwindow.h b/src/plugins/platforms/ios/quiwindow.h new file mode 100644 index 0000000000..b5587411e4 --- /dev/null +++ b/src/plugins/platforms/ios/quiwindow.h @@ -0,0 +1,13 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QUIWINDOW_H +#define QUIWINDOW_H + +#include <UIKit/UIWindow.h> + +@interface QUIWindow : UIWindow +@property (nonatomic, readonly) BOOL sendingEvent; +@end + +#endif // QUIWINDOW_H diff --git a/src/plugins/platforms/ios/quiwindow.mm b/src/plugins/platforms/ios/quiwindow.mm new file mode 100644 index 0000000000..7c910b6d9e --- /dev/null +++ b/src/plugins/platforms/ios/quiwindow.mm @@ -0,0 +1,56 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "quiwindow.h" + +#include "qiostheme.h" + +#include <QtCore/qscopedvaluerollback.h> + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> + +#include <UIKit/UIKit.h> + +@implementation QUIWindow + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) + self->_sendingEvent = NO; + + return self; +} + +- (void)sendEvent:(UIEvent *)event +{ + QScopedValueRollback<BOOL> sendingEvent(self->_sendingEvent, YES); + [super sendEvent:event]; +} + +#if !defined(Q_OS_VISIONOS) +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection +{ + [super traitCollectionDidChange:previousTraitCollection]; + + if (!qGuiApp) + return; + + Qt::ColorScheme colorScheme = self.traitCollection.userInterfaceStyle + == UIUserInterfaceStyleDark + ? Qt::ColorScheme::Dark + : Qt::ColorScheme::Light; + + if (self.screen == UIScreen.mainScreen) { + // Check if the current userInterfaceStyle reports a different appearance than + // the platformTheme's appearance. We might have set that one based on the UIScreen + if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle + || QGuiApplicationPrivate::platformTheme()->colorScheme() != colorScheme) { + QIOSTheme::initializeSystemPalette(); + QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>(); + } + } +} +#endif + +@end |