diff options
Diffstat (limited to 'src/plugins/platforms/ios')
-rw-r--r-- | src/plugins/platforms/ios/CMakeLists.txt | 19 | ||||
-rw-r--r-- | src/plugins/platforms/ios/SwiftIntegration.cmake | 78 | ||||
-rw-r--r-- | src/plugins/platforms/ios/module.modulemap | 4 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosapplication.swift | 198 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosapplicationdelegate.h | 5 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qioseventdispatcher.h | 2 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qioseventdispatcher.mm | 21 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosglobal.mm | 20 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosintegration.h | 31 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosintegration.mm | 50 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosscreen.mm | 5 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qioswindow.h | 3 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qioswindow.mm | 43 | ||||
-rw-r--r-- | src/plugins/platforms/ios/quiaccessibilityelement.mm | 13 | ||||
-rw-r--r-- | src/plugins/platforms/ios/quiview.mm | 65 |
15 files changed, 483 insertions, 74 deletions
diff --git a/src/plugins/platforms/ios/CMakeLists.txt b/src/plugins/platforms/ios/CMakeLists.txt index 4cc3efc91e..51c1b52cf3 100644 --- a/src/plugins/platforms/ios/CMakeLists.txt +++ b/src/plugins/platforms/ios/CMakeLists.txt @@ -5,10 +5,23 @@ ## QIOSIntegrationPlugin Plugin: ##################################################################### +if(VISIONOS) + include(SwiftIntegration.cmake) + + qt_install(TARGETS QIOSIntegrationPluginSwift + EXPORT "${INSTALL_CMAKE_NAMESPACE}QIOSIntegrationPluginTargets" + DESTINATION "${INSTALL_LIBDIR}" + ) + qt_internal_add_targets_to_additional_targets_export_file( + TARGETS QIOSIntegrationPluginSwift + EXPORT_NAME_PREFIX "${INSTALL_CMAKE_NAMESPACE}QIOSIntegrationPlugin" + ) +endif() + qt_internal_add_plugin(QIOSIntegrationPlugin OUTPUT_NAME qios STATIC # Force static, even in shared builds - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES ios + DEFAULT_IF "ios" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms SOURCES plugin.mm @@ -86,3 +99,7 @@ qt_internal_extend_target(QIOSIntegrationPlugin CONDITION NOT (TVOS OR VISIONOS) ) add_subdirectory(optional) + +if(VISIONOS) + target_link_libraries(QIOSIntegrationPlugin PRIVATE QIOSIntegrationPluginSwift) +endif() diff --git a/src/plugins/platforms/ios/SwiftIntegration.cmake b/src/plugins/platforms/ios/SwiftIntegration.cmake new file mode 100644 index 0000000000..d52edb3ad2 --- /dev/null +++ b/src/plugins/platforms/ios/SwiftIntegration.cmake @@ -0,0 +1,78 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +set(CMAKE_Swift_COMPILER_TARGET arm64-apple-xros) +if($CACHE{CMAKE_OSX_SYSROOT} MATCHES "^[a-z]+simulator$") + set(CMAKE_Swift_COMPILER_TARGET "${CMAKE_Swift_COMPILER_TARGET}-simulator") +endif() + +cmake_policy(SET CMP0157 NEW) +enable_language(Swift) + +# Verify that we have a new enough compiler +if("${CMAKE_Swift_COMPILER_VERSION}" VERSION_LESS 5.9) + message(FATAL_ERROR "Swift 5.9 required for C++ interoperability") +endif() + +get_target_property(QT_CORE_INCLUDES Qt6::Core INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_GUI_INCLUDES Qt6::Gui INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_CORE_PRIVATE_INCLUDES Qt6::CorePrivate INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(QT_GUI_PRIVATE_INCLUDES Qt6::GuiPrivate INTERFACE_INCLUDE_DIRECTORIES) + +set(target QIOSIntegrationPluginSwift) +# Swift library +set(SWIFT_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/qiosapplication.swift" +) +add_library(${target} STATIC ${SWIFT_SOURCES}) +set_target_properties(${target} PROPERTIES + Swift_MODULE_NAME ${target}) +target_include_directories(${target} PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + "${QT_CORE_INCLUDES}" + "${QT_GUI_INCLUDES}" + "${QT_CORE_PRIVATE_INCLUDES}" + "${QT_GUI_PRIVATE_INCLUDES}" + +) +target_compile_options(${target} PUBLIC + $<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default> + $<$<COMPILE_LANGUAGE:Swift>:-Xcc -std=c++17>) + +# Swift to C++ bridging header +set(SWIFT_BRIDGING_HEADER "${CMAKE_CURRENT_BINARY_DIR}/qiosswiftintegration.h") +list(TRANSFORM QT_CORE_INCLUDES PREPEND "-I") +list(TRANSFORM QT_GUI_INCLUDES PREPEND "-I") +list(TRANSFORM QT_CORE_PRIVATE_INCLUDES PREPEND "-I") +list(TRANSFORM QT_GUI_PRIVATE_INCLUDES PREPEND "-I") +add_custom_command( + COMMAND + ${CMAKE_Swift_COMPILER} -frontend -typecheck + ${SWIFT_SOURCES} + -I ${CMAKE_CURRENT_SOURCE_DIR} + ${QT_CORE_INCLUDES} + ${QT_GUI_INCLUDES} + ${QT_CORE_PRIVATE_INCLUDES} + ${QT_GUI_PRIVATE_INCLUDES} + -sdk ${CMAKE_OSX_SYSROOT} + -module-name ${target} + -cxx-interoperability-mode=default + -Xcc -std=c++17 + -emit-clang-header-path "${SWIFT_BRIDGING_HEADER}" + -target ${CMAKE_Swift_COMPILER_TARGET} + OUTPUT + "${SWIFT_BRIDGING_HEADER}" + DEPENDS + ${SWIFT_SOURCES} + ) + +set(header_target "${target}Header") +add_custom_target(${header_target} + DEPENDS "${SWIFT_BRIDGING_HEADER}" +) +# Make sure the "'__bridge_transfer' casts have no effect when not using ARC" +# warning doesn't break warnings-are-error builds. +target_compile_options(${target} INTERFACE + -Wno-error=arc-bridge-casts-disallowed-in-nonarc) + +add_dependencies(${target} ${header_target}) diff --git a/src/plugins/platforms/ios/module.modulemap b/src/plugins/platforms/ios/module.modulemap new file mode 100644 index 0000000000..af42b3e1f5 --- /dev/null +++ b/src/plugins/platforms/ios/module.modulemap @@ -0,0 +1,4 @@ +module QIOSIntegrationPlugin { + header "qiosapplicationdelegate.h" + header "qiosintegration.h" +} diff --git a/src/plugins/platforms/ios/qiosapplication.swift b/src/plugins/platforms/ios/qiosapplication.swift new file mode 100644 index 0000000000..9eb172a896 --- /dev/null +++ b/src/plugins/platforms/ios/qiosapplication.swift @@ -0,0 +1,198 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import SwiftUI +import CompositorServices +import QIOSIntegrationPlugin +import RealityKit + +struct QIOSSwiftApplication: App { + @UIApplicationDelegateAdaptor private var appDelegate: QIOSApplicationDelegate + + var body: some SwiftUI.Scene { + WindowGroup() { + ImmersiveSpaceControlView() + } + + ImmersiveSpace(id: "QIOSImmersiveSpace") { + CompositorLayer(configuration: QIOSLayerConfiguration()) { layerRenderer in + QIOSIntegration.instance().renderCompositorLayer(layerRenderer) + + // Handle any events in the scene. + layerRenderer.onSpatialEvent = { eventCollection in + QIOSIntegration.instance().handleSpatialEvents(jsonStringFromEventCollection(eventCollection)) + } + } + } + // CompositorLayer immersive spaces are always full, and should not need + // to set the immersion style, but lacking this we get a warning in the + // console about not being able to "configure an immersive space with + // selected style 'AutomaticImmersionStyle' since it is not in the list + // of supported styles for this type of content: 'FullImmersionStyle'." + .immersionStyle(selection: .constant(.full), in: .full) + } +} + +public struct QIOSLayerConfiguration: CompositorLayerConfiguration { + public func makeConfiguration(capabilities: LayerRenderer.Capabilities, + configuration: inout LayerRenderer.Configuration) { + // Use reflection to pull out underlying C handles + // FIXME: Use proper bridging APIs when available + let capabilitiesMirror = Mirror(reflecting: capabilities) + let configurationMirror = Mirror(reflecting: configuration) + QIOSIntegration.instance().configureCompositorLayer( + capabilitiesMirror.descendant("c_capabilities") as? cp_layer_renderer_capabilities_t, + configurationMirror.descendant("box", "value") as? cp_layer_renderer_configuration_t + ) + } +} + +public func runSwiftAppMain() { + QIOSSwiftApplication.main() +} + +public class ImmersiveState: ObservableObject { + static let shared = ImmersiveState() + @Published var showImmersiveSpace: Bool = false +} + +struct ImmersiveSpaceControlView: View { + @ObservedObject private var immersiveState = ImmersiveState.shared + + @Environment(\.openImmersiveSpace) var openImmersiveSpace + @Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace + + var body: some View { + VStack {} + .onChange(of: immersiveState.showImmersiveSpace) { _, newValue in + Task { + if newValue { + await openImmersiveSpace(id: "QIOSImmersiveSpace") + } else { + await dismissImmersiveSpace() + } + } + } + } +} + +public class ImmersiveSpaceManager : NSObject { + @objc public static func openImmersiveSpace() { + ImmersiveState.shared.showImmersiveSpace = true + } + + @objc public static func dismissImmersiveSpace() { + ImmersiveState.shared.showImmersiveSpace = false + } +} + +extension SpatialEventCollection.Event.Kind: Encodable { + enum CodingKeys: String, CodingKey { + case touch + case directPinch + case indirectPinch + case pointer + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .touch: + try container.encode("touch") + case .directPinch: + try container.encode("directPinch") + case .indirectPinch: + try container.encode("indirectPinch") + case .pointer: + try container.encode("pointer") + @unknown default: + try container.encode("unknown") + } + } +} +extension SpatialEventCollection.Event.Phase: Encodable { + enum CodingKeys: String, CodingKey { + case active + case ending + case cancled + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .active: + try container.encode("active") + case .ended: + try container.encode("ended") + case .cancelled: + try container.encode("canceled") + @unknown default: + try container.encode("unknown") + } + } +} +extension SpatialEventCollection.Event.InputDevicePose: Encodable { + enum CodingKeys: String, CodingKey { + case altitude + case azimuth + case pose3D + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(altitude.radians, forKey: .altitude) + try container.encode(azimuth.radians, forKey: .azimuth) + try container.encode(pose3D, forKey: .pose3D) + } +} + +extension SpatialEventCollection.Event: Encodable { + enum CodingKeys: String, CodingKey { + case id + case timestamp + case kind + case location + case phase + case modifierKeys + case inputDevicePose + case location3D + case selectionRay + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id.hashValue, forKey: .id) + try container.encode(timestamp, forKey: .timestamp) + try container.encode(kind, forKey: .kind) + try container.encode(location, forKey: .location) + try container.encode(phase, forKey: .phase) + try container.encode(modifierKeys.rawValue, forKey: .modifierKeys) + try container.encode(inputDevicePose, forKey: .inputDevicePose) + try container.encode(location3D, forKey: .location3D) + try container.encode(selectionRay, forKey: .selectionRay) + } +} + +extension SpatialEventCollection: Encodable { + enum CodingKeys: String, CodingKey { + case events + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Array(self), forKey: .events) + } +} + +func jsonStringFromEventCollection(_ eventCollection: SpatialEventCollection) -> String { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + + do { + let jsonData = try encoder.encode(eventCollection) + return String(data: jsonData, encoding: .utf8) ?? "{}" + } catch { + print("Failed to encode event collection: \(error)") + return "{}" + } +} diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.h b/src/plugins/platforms/ios/qiosapplicationdelegate.h index 39bb9fdedb..7e12d64cbf 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.h +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.h @@ -1,6 +1,9 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QIOSAPPLICATIONDELEGATE_H +#define QIOSAPPLICATIONDELEGATE_H + #import <UIKit/UIKit.h> #import <QtGui/QtGui> @@ -8,3 +11,5 @@ @interface QIOSApplicationDelegate : UIResponder <UIApplicationDelegate> @end + +#endif // QIOSAPPLICATIONDELEGATE_H diff --git a/src/plugins/platforms/ios/qioseventdispatcher.h b/src/plugins/platforms/ios/qioseventdispatcher.h index b40024ec19..5eee0556f5 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.h +++ b/src/plugins/platforms/ios/qioseventdispatcher.h @@ -16,6 +16,8 @@ public: static QIOSEventDispatcher* create(); bool processPostedEvents() override; + static bool isQtApplication(); + protected: explicit QIOSEventDispatcher(QObject *parent = nullptr); }; diff --git a/src/plugins/platforms/ios/qioseventdispatcher.mm b/src/plugins/platforms/ios/qioseventdispatcher.mm index 24d9d88294..710a834bfd 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.mm +++ b/src/plugins/platforms/ios/qioseventdispatcher.mm @@ -5,6 +5,10 @@ #include "qiosapplicationdelegate.h" #include "qiosglobal.h" +#if defined(Q_OS_VISIONOS) +#include "qiosswiftintegration.h" +#endif + #include <QtCore/qprocessordetection.h> #include <QtCore/private/qcoreapplication_p.h> #include <QtCore/private/qthread_p.h> @@ -173,12 +177,16 @@ namespace QAppleLogActivity UIApplicationMain; QAppleLogActivity applicationDidFinishLaunching; } logActivity; + + static bool s_isQtApplication = false; } using namespace QT_PREPEND_NAMESPACE(QtPrivate); extern "C" int qt_main_wrapper(int argc, char *argv[]) { + s_isQtApplication = true; + @autoreleasepool { size_t defaultStackSize = 512 * kBytesPerKiloByte; // Same as secondary threads @@ -202,8 +210,16 @@ extern "C" int qt_main_wrapper(int argc, char *argv[]) logActivity.UIApplicationMain = QT_APPLE_LOG_ACTIVITY( lcEventDispatcher().isDebugEnabled(), "UIApplicationMain").enter(); +#if defined(Q_OS_VISIONOS) + Q_UNUSED(argc); + Q_UNUSED(argv); + qCDebug(lcEventDispatcher) << "Starting Swift app"; + QIOSIntegrationPluginSwift::runSwiftAppMain(); + Q_UNREACHABLE(); +#else qCDebug(lcEventDispatcher) << "Running UIApplicationMain"; return UIApplicationMain(argc, argv, nil, NSStringFromClass([QIOSApplicationDelegate class])); +#endif } } @@ -424,6 +440,11 @@ QIOSEventDispatcher::QIOSEventDispatcher(QObject *parent) QWindowSystemInterface::setSynchronousWindowSystemEvents(true); } +bool QIOSEventDispatcher::isQtApplication() +{ + return s_isQtApplication; +} + /*! Override of the CoreFoundation posted events runloop source callback so that we can send window system (QPA) events in addition to sending diff --git a/src/plugins/platforms/ios/qiosglobal.mm b/src/plugins/platforms/ios/qiosglobal.mm index 25ccf2961b..1722e09aaa 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -5,6 +5,8 @@ #include "qiosapplicationdelegate.h" #include "qiosviewcontroller.h" #include "qiosscreen.h" +#include "quiwindow.h" +#include "qioseventdispatcher.h" #include <QtCore/private/qcore_mac_p.h> @@ -17,17 +19,13 @@ Q_LOGGING_CATEGORY(lcQpaWindowScene, "qt.qpa.window.scene"); bool isQtApplication() { - if (qt_apple_isApplicationExtension()) - return false; - // Returns \c true if the plugin is in full control of the whole application. This means // that we control the application delegate and the top view controller, and can take // actions that impacts all parts of the application. The opposite means that we are // embedded inside a native iOS application, and should be more focused on playing along // with native UIControls, and less inclined to change structures that lies outside the // scope of our QWindows/UIViews. - static bool isQt = ([qt_apple_sharedApplication().delegate isKindOfClass:[QIOSApplicationDelegate class]]); - return isQt; + return QIOSEventDispatcher::isQtApplication(); } bool isRunningOnVisionOS() @@ -126,9 +124,15 @@ UIView *rootViewForScreen(QScreen *screen) Q_UNUSED(iosScreen); #endif - UIWindow *uiWindow = windowScene.keyWindow; - if (!uiWindow && windowScene.windows.count) - uiWindow = windowScene.windows[0]; + UIWindow *uiWindow = qt_objc_cast<QUIWindow*>(windowScene.keyWindow); + if (!uiWindow) { + for (UIWindow *win in windowScene.windows) { + if (qt_objc_cast<QUIWindow*>(win)) { + uiWindow = win; + break; + } + } + } return uiWindow.rootViewController.view; } diff --git a/src/plugins/platforms/ios/qiosintegration.h b/src/plugins/platforms/ios/qiosintegration.h index 2c7d33cc94..6c2014d048 100644 --- a/src/plugins/platforms/ios/qiosintegration.h +++ b/src/plugins/platforms/ios/qiosintegration.h @@ -16,11 +16,24 @@ #include "qiostextinputoverlay.h" #endif +#if defined(Q_OS_VISIONOS) +#include <swift/bridging> +#endif + QT_BEGIN_NAMESPACE +using namespace QNativeInterface; + class QIOSServices; -class QIOSIntegration : public QPlatformNativeInterface, public QPlatformIntegration +class +#if defined(Q_OS_VISIONOS) + SWIFT_IMMORTAL_REFERENCE +#endif +QIOSIntegration : public QPlatformNativeInterface, public QPlatformIntegration +#if defined(Q_OS_VISIONOS) + , public QVisionOSApplication +#endif { Q_OBJECT public: @@ -77,6 +90,18 @@ public: QIOSApplicationState applicationState; +#if defined(Q_OS_VISIONOS) + void openImmersiveSpace() override; + void dismissImmersiveSpace() override; + + using CompositorLayer = QVisionOSApplication::ImmersiveSpaceCompositorLayer; + void setImmersiveSpaceCompositorLayer(CompositorLayer *layer) override; + + void configureCompositorLayer(cp_layer_renderer_capabilities_t, cp_layer_renderer_configuration_t); + void renderCompositorLayer(cp_layer_renderer_t); + void handleSpatialEvents(const char *jsonString); +#endif + private: QPlatformFontDatabase *m_fontDatabase; #if QT_CONFIG(clipboard) @@ -90,6 +115,10 @@ private: #if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS) QIOSTextInputOverlay m_textInputOverlay; #endif + +#if defined(Q_OS_VISIONOS) + CompositorLayer *m_immersiveSpaceCompositorLayer = nullptr; +#endif }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index 7cd21f83f6..76173ce830 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -17,6 +17,10 @@ #include "qiosservices.h" #include "qiosoptionalplugininterface.h" +#if defined(Q_OS_VISIONOS) +#include "qiosswiftintegration.h" +#endif + #include <QtGui/qpointingdevice.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qrhibackingstore_p.h> @@ -296,6 +300,52 @@ void QIOSIntegration::setApplicationBadge(qint64 number) // --------------------------------------------------------- +#if defined(Q_OS_VISIONOS) +void QIOSIntegration::openImmersiveSpace() +{ + [ImmersiveSpaceManager openImmersiveSpace]; +} + +void QIOSIntegration::dismissImmersiveSpace() +{ + [ImmersiveSpaceManager dismissImmersiveSpace]; +} + +void QIOSIntegration::setImmersiveSpaceCompositorLayer(CompositorLayer *layer) +{ + m_immersiveSpaceCompositorLayer = layer; +} + +void QIOSIntegration::configureCompositorLayer(cp_layer_renderer_capabilities_t capabilities, + cp_layer_renderer_configuration_t configuration) +{ + if (m_immersiveSpaceCompositorLayer) + m_immersiveSpaceCompositorLayer->configure(capabilities, configuration); +} + +void QIOSIntegration::renderCompositorLayer(cp_layer_renderer_t renderer) +{ + if (m_immersiveSpaceCompositorLayer) + m_immersiveSpaceCompositorLayer->render(renderer); +} + +void QIOSIntegration::handleSpatialEvents(const char *jsonString) +{ + if (m_immersiveSpaceCompositorLayer) { + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(QByteArray(jsonString), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Error parsing JSON: " << error.errorString(); + return; + } + m_immersiveSpaceCompositorLayer->handleSpatialEvents(doc.object()); + } +} + +#endif + +// --------------------------------------------------------- + void *QIOSIntegration::nativeResourceForWindow(const QByteArray &resource, QWindow *window) { if (!window || !window->handle()) diff --git a/src/plugins/platforms/ios/qiosscreen.mm b/src/plugins/platforms/ios/qiosscreen.mm index 7559979f33..8f3081e276 100644 --- a/src/plugins/platforms/ios/qiosscreen.mm +++ b/src/plugins/platforms/ios/qiosscreen.mm @@ -322,7 +322,10 @@ QDpi QIOSScreen::logicalBaseDpi() const qreal QIOSScreen::devicePixelRatio() const { #if defined(Q_OS_VISIONOS) - return 2.0; // Based on what iPad app reports + // Based on what iPad app reports, and what Apple + // documents to be the default scale factor on + // visionOS, and the minimum scale for assets. + return 2.0; #else return [m_uiScreen scale]; #endif diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 88afee80c3..86bcc111d3 100644 --- a/src/plugins/platforms/ios/qioswindow.h +++ b/src/plugins/platforms/ios/qioswindow.h @@ -71,10 +71,9 @@ private: UIView *m_view; QRect m_normalGeometry; - int m_windowLevel; void raiseOrLower(bool raise); - void updateWindowLevel(); + int windowLevel() const; bool blockedByModal(); friend class QIOSScreen; diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index f461a5f55b..7cd3d5f0b0 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -36,7 +36,6 @@ enum { QIOSWindow::QIOSWindow(QWindow *window, WId nativeHandle) : QPlatformWindow(window) - , m_windowLevel(0) { if (nativeHandle) { m_view = reinterpret_cast<UIView *>(nativeHandle); @@ -127,11 +126,6 @@ void QIOSWindow::setVisible(bool visible) if (!isQtApplication() || !window()->isTopLevel()) return; - // Since iOS doesn't do window management the way a Qt application - // expects, we need to raise and activate windows ourselves: - if (visible) - updateWindowLevel(); - if (blockedByModal()) { if (visible) raise(); @@ -343,8 +337,8 @@ void QIOSWindow::raiseOrLower(bool raise) 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)) { + int level = static_cast<QIOSWindow *>(view.qwindow->handle())->windowLevel(); + if (windowLevel() > level || (raise && windowLevel() == level)) { [m_view.superview insertSubview:m_view aboveSubview:view]; return; } @@ -359,30 +353,34 @@ void QIOSWindow::raiseOrLower(bool raise) } } -void QIOSWindow::updateWindowLevel() +int QIOSWindow::windowLevel() const { Qt::WindowType type = window()->type(); + int level = 0; + if (type == Qt::ToolTip) - m_windowLevel = 120; + level = 120; else if (window()->flags() & Qt::WindowStaysOnTopHint) - m_windowLevel = 100; + level = 100; else if (window()->isModal()) - m_windowLevel = 40; + level = 40; else if (type == Qt::Popup) - m_windowLevel = 30; + level = 30; else if (type == Qt::SplashScreen) - m_windowLevel = 20; + level = 20; else if (type == Qt::Tool) - m_windowLevel = 10; + level = 10; else - m_windowLevel = 0; + level = 0; - // A window should be in at least the same m_windowLevel as its parent: + // A window should be in at least the same window level as its parent QWindow *transientParent = window()->transientParent(); QIOSWindow *transientParentWindow = transientParent ? static_cast<QIOSWindow *>(transientParent->handle()) : 0; if (transientParentWindow) - m_windowLevel = qMax(transientParentWindow->m_windowLevel, m_windowLevel); + level = qMax(transientParentWindow->windowLevel(), level); + + return level; } void QIOSWindow::applicationStateChanged(Qt::ApplicationState) @@ -396,6 +394,15 @@ void QIOSWindow::applicationStateChanged(Qt::ApplicationState) qreal QIOSWindow::devicePixelRatio() const { +#if !defined(Q_OS_VISIONOS) + // If the view has not yet been added to a screen, it will not + // pick up its device pixel ratio, so we need to do so manually + // based on the screen we think the window will be added to. + if (!m_view.window.windowScene.screen) + return screen()->devicePixelRatio(); +#endif + + // Otherwise we can rely on the content scale factor return m_view.contentScaleFactor; } diff --git a/src/plugins/platforms/ios/quiaccessibilityelement.mm b/src/plugins/platforms/ios/quiaccessibilityelement.mm index 39b2cb8a50..fa54f61967 100644 --- a/src/plugins/platforms/ios/quiaccessibilityelement.mm +++ b/src/plugins/platforms/ios/quiaccessibilityelement.mm @@ -10,6 +10,8 @@ #include "uistrings_p.h" #include "qioswindow.h" +#include <QtGui/private/qaccessiblebridgeutils_p.h> + QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); @implementation QMacAccessibilityElement @@ -70,6 +72,17 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QMacAccessibilityElement); return iface->text(QAccessible::Name).toNSString(); } + +- (NSString*)accessibilityIdentifier +{ + QAccessibleInterface *iface = QAccessible::accessibleInterface(self.axid); + if (!iface) { + qWarning() << "invalid accessible interface for: " << self.axid; + return @""; + } + return QAccessibleBridgeUtils::accessibleId(iface).toNSString(); +} + - (NSString*)accessibilityHint { QAccessibleInterface *iface = QAccessible::accessibleInterface(self.axid); diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm index d5808db305..b31e1a043d 100644 --- a/src/plugins/platforms/ios/quiview.mm +++ b/src/plugins/platforms/ios/quiview.mm @@ -92,7 +92,16 @@ inline ulong getTimeStamp(UIEvent *event) { if (self = [self initWithFrame:window->geometry().toCGRect()]) { self.platformWindow = window; + + if (isQtApplication()) + self.hidden = YES; + m_accessibleElements = [[NSMutableArray<UIAccessibilityElement *> alloc] init]; + +#ifndef Q_OS_TVOS + self.multipleTouchEnabled = YES; +#endif + m_scrollGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleScroll:)]; @@ -109,6 +118,7 @@ inline ulong getTimeStamp(UIEvent *event) m_lastScrollCursorPos = CGPointZero; [self addGestureRecognizer:m_scrollGestureRecognizer]; + // Set up layer if ([self.layer isKindOfClass:CAMetalLayer.class]) { QWindow *window = self.platformWindow->window(); if (QColorSpace colorSpace = window->format().colorSpace(); colorSpace.isValid()) { @@ -119,17 +129,8 @@ inline ulong getTimeStamp(UIEvent *event) qCDebug(lcQpaWindow) << "Set" << self << "color space to" << metalLayer.colorspace; } } - } - - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if ((self = [super initWithFrame:frame])) { #if QT_CONFIG(opengl) - if ([self.layer isKindOfClass:[CAEAGLLayer class]]) { - // Set up EAGL layer + else if ([self.layer isKindOfClass:[CAEAGLLayer class]]) { CAEAGLLayer *eaglLayer = static_cast<CAEAGLLayer *>(self.layer); eaglLayer.opaque = TRUE; eaglLayer.drawableProperties = @{ @@ -139,37 +140,13 @@ inline ulong getTimeStamp(UIEvent *event) } #endif - if (isQtApplication()) - self.hidden = YES; - -#ifndef Q_OS_TVOS - self.multipleTouchEnabled = YES; +#if defined(Q_OS_VISIONOS) + // Although the "Drawing sharp layer-based content in visionOS" docs + // claim that by default a CALayer rasterizes at a 2x scale this does + // not seem to be the case in practice. So we explicitly set the view's + // scale factor based on the screen, where we hard-code it to 2.0. + self.contentScaleFactor = self.platformWindow->screen()->devicePixelRatio(); #endif - - if (qEnvironmentVariableIntValue("QT_IOS_DEBUG_WINDOW_MANAGEMENT")) { - static CGFloat hue = 0.0; - CGFloat lastHue = hue; - for (CGFloat diff = 0; diff < 0.1 || diff > 0.9; diff = fabs(hue - lastHue)) - hue = drand48(); - - #define colorWithBrightness(br) \ - [UIColor colorWithHue:hue saturation:0.5 brightness:br alpha:1.0].CGColor - - self.layer.borderColor = colorWithBrightness(1.0); - self.layer.borderWidth = 1.0; - } - - if (qEnvironmentVariableIsSet("QT_IOS_DEBUG_WINDOW_SAFE_AREAS")) { - UIView *safeAreaOverlay = [[UIView alloc] initWithFrame:CGRectZero]; - [safeAreaOverlay setBackgroundColor:[UIColor colorWithRed:0.3 green:0.7 blue:0.9 alpha:0.3]]; - [self addSubview:safeAreaOverlay]; - - safeAreaOverlay.translatesAutoresizingMaskIntoConstraints = NO; - [safeAreaOverlay.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES; - [safeAreaOverlay.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor].active = YES; - [safeAreaOverlay.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor].active = YES; - [safeAreaOverlay.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES; - } } return self; @@ -356,9 +333,11 @@ inline ulong getTimeStamp(UIEvent *event) qImDebug() << self << "resigned first responder"; - UIResponder *newResponder = FirstResponderCandidate::currentCandidate(); - if ([self responderShouldTriggerWindowDeactivation:newResponder]) - QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason); + if (qGuiApp) { + UIResponder *newResponder = FirstResponderCandidate::currentCandidate(); + if ([self responderShouldTriggerWindowDeactivation:newResponder]) + QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason); + } return YES; } |