Revision control
Copy as Markdown
// 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
// Qt-Security score:significant reason:default
#include <AppKit/AppKit.h>
#include "qcocoaintegration.h"
#include "qcocoawindow.h"
#include "qcocoabackingstore.h"
#include "qcocoanativeinterface.h"
#include "qcocoamenuloader.h"
#include "qcocoaeventdispatcher.h"
#include "qcocoahelpers.h"
#include "qcocoaapplication.h"
#include "qcocoaapplicationdelegate.h"
#include "qcocoatheme.h"
#include "qcocoainputcontext.h"
#include "qcocoamimetypes.h"
#include "qcocoaaccessibility.h"
#include "qcocoascreen.h"
#if QT_CONFIG(sessionmanager)
# include "qcocoasessionmanager.h"
#endif
#include "qcocoawindowmanager.h"
#include <qpa/qplatforminputcontextfactory_p.h>
#include <qpa/qplatformaccessibility.h>
#include <qpa/qplatforminputcontextfactory_p.h>
#include <qpa/qplatformoffscreensurface.h>
#include <QtCore/qcoreapplication.h>
#include <QtGui/qpointingdevice.h>
#include <QtCore/private/qcore_mac_p.h>
#include <QtGui/private/qcoregraphics_p.h>
#include <QtGui/private/qmacmimeregistry_p.h>
#ifndef QT_NO_OPENGL
# include <QtGui/private/qopenglcontext_p.h>
#endif
#include <QtGui/private/qrhibackingstore_p.h>
#include <QtGui/private/qfontengine_coretext_p.h>
#include <IOKit/graphics/IOGraphicsLib.h>
#include <UniformTypeIdentifiers/UTCoreTypes.h>
#include <inttypes.h>
static void initResources()
{
Q_INIT_RESOURCE(qcocoaresources);
}
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
Q_LOGGING_CATEGORY(lcQpa, "qt.qpa", QtWarningMsg);
static void logVersionInformation()
{
if (!lcQpa().isInfoEnabled())
return;
auto osVersion = QMacVersion::currentRuntime();
auto qtBuildSDK = QMacVersion::buildSDK(QMacVersion::QtLibraries);
auto qtDeploymentTarget = QMacVersion::deploymentTarget(QMacVersion::QtLibraries);
auto appBuildSDK = QMacVersion::buildSDK(QMacVersion::ApplicationBinary);
auto appDeploymentTarget = QMacVersion::deploymentTarget(QMacVersion::ApplicationBinary);
qCInfo(lcQpa, "Loading macOS (Cocoa) platform plugin for Qt " QT_VERSION_STR ", running on macOS %d.%d.%d\n\n" \
" Component SDK version Deployment target \n" \
" ------------- ------------- -------------------\n" \
" Qt " QT_VERSION_STR " %d.%d.%d %d.%d.%d\n" \
" Application %d.%d.%d %d.%d.%d\n",
osVersion.majorVersion(), osVersion.minorVersion(), osVersion.microVersion(),
qtBuildSDK.majorVersion(), qtBuildSDK.minorVersion(), qtBuildSDK.microVersion(),
qtDeploymentTarget.majorVersion(), qtDeploymentTarget.minorVersion(), qtDeploymentTarget.microVersion(),
appBuildSDK.majorVersion(), appBuildSDK.minorVersion(), appBuildSDK.microVersion(),
appDeploymentTarget.majorVersion(), appDeploymentTarget.minorVersion(), appDeploymentTarget.microVersion());
qCInfo(lcQpa) << "Running with Liquid Glass enabled:" << qt_apple_runningWithLiquidGlass();
}
class QCoreTextFontEngine;
class QFontEngineFT;
static QCocoaIntegration::Options parseOptions(const QStringList ¶mList)
{
QCocoaIntegration::Options options;
for (const QString ¶m : paramList) {
#ifndef QT_NO_FREETYPE
if (param == "fontengine=freetype"_L1)
options |= QCocoaIntegration::UseFreeTypeFontEngine;
else
#endif
qWarning() << "Unknown option" << param;
}
return options;
}
QCocoaIntegration *QCocoaIntegration::mInstance = nullptr;
QCocoaIntegration::QCocoaIntegration(const QStringList ¶mList)
: mOptions(parseOptions(paramList))
, mFontDb(nullptr)
#if QT_CONFIG(accessibility)
, mAccessibility(new QCocoaAccessibility)
#endif
#ifndef QT_NO_CLIPBOARD
, mCocoaClipboard(new QCocoaClipboard)
#endif
, mCocoaDrag(new QCocoaDrag)
, mNativeInterface(new QCocoaNativeInterface)
, mKeyboardMapper(new QAppleKeyMapper)
{
logVersionInformation();
if (mInstance)
qWarning("Creating multiple Cocoa platform integrations is not supported");
mInstance = this;
#ifndef QT_NO_FREETYPE
if (mOptions.testFlag(UseFreeTypeFontEngine))
mFontDb.reset(new QCoreTextFontDatabaseEngineFactory<QFontEngineFT>);
else
#endif
mFontDb.reset(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>);
auto icStrs = QPlatformInputContextFactory::requested();
icStrs.isEmpty() ? mInputContext.reset(new QCocoaInputContext)
: mInputContext.reset(QPlatformInputContextFactory::create(icStrs));
initResources();
QMacAutoReleasePool pool;
NSApplication *cocoaApplication = [QNSApplication sharedApplication];
qt_redirectNSApplicationSendEvent();
if (qEnvironmentVariableIsEmpty("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM")) {
// Applications launched from plain executables (without an app
// bundle) are "background" applications that does not take keyboard
// focus or have a dock icon or task switcher entry. Qt Gui apps generally
// wants to be foreground applications so change the process type. (But
// see the function implementation for exceptions.)
qt_mac_transformProccessToForegroundApplication();
}
// Qt 4 also does not set the application delegate, so that behavior
// is matched here.
if (!QCoreApplication::testAttribute(Qt::AA_PluginApplication)) {
// Set app delegate, link to the current delegate (if any)
QCocoaApplicationDelegate *newDelegate = [QCocoaApplicationDelegate sharedDelegate];
[newDelegate setReflectionDelegate:[cocoaApplication delegate]];
[cocoaApplication setDelegate:newDelegate];
// Load the application menu. This menu contains Preferences, Hide, Quit.
QCocoaMenuLoader *qtMenuLoader = [QCocoaMenuLoader sharedMenuLoader];
[cocoaApplication setMenu:[qtMenuLoader menu]];
}
QCocoaScreen::initializeScreens();
QMacMimeRegistry::initializeMimeTypes();
QCocoaMimeTypes::initializeMimeTypes();
QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false);
QWindowSystemInterface::registerInputDevice(new QInputDevice(QString("keyboard"), 0,
QInputDevice::DeviceType::Keyboard, QString(), this));
connect(qGuiApp, &QGuiApplication::focusWindowChanged,
this, &QCocoaIntegration::focusWindowChanged);
// Opening of a native menu should close all popup windows
m_menuTrackingObserver = QMacNotificationObserver(nil,
NSMenuDidBeginTrackingNotification, ^{
QGuiApplicationPrivate::instance()->closeAllPopups();
});
}
QCocoaIntegration::~QCocoaIntegration()
{
mInstance = nullptr;
qt_resetNSApplicationSendEvent();
QMacAutoReleasePool pool;
if (!QCoreApplication::testAttribute(Qt::AA_PluginApplication)) {
// remove the apple event handlers installed by QCocoaApplicationDelegate
QCocoaApplicationDelegate *delegate = [QCocoaApplicationDelegate sharedDelegate];
[delegate removeAppleEventHandlers];
// reset the application delegate
[[NSApplication sharedApplication] setDelegate:nil];
}
// Stop global mouse event and app activation monitoring
QCocoaWindow::removePopupMonitor();
#ifndef QT_NO_CLIPBOARD
// Delete the clipboard integration and destroy mime type converters.
// Deleting the clipboard integration flushes promised pastes using
// the mime converters - the ordering here is important.
delete mCocoaClipboard;
QMacMimeRegistry::destroyMimeTypes();
#endif
QCocoaScreen::cleanupScreens();
}
QCocoaIntegration *QCocoaIntegration::instance()
{
return mInstance;
}
QCocoaIntegration::Options QCocoaIntegration::options() const
{
return mOptions;
}
#if QT_CONFIG(sessionmanager)
QPlatformSessionManager *QCocoaIntegration::createPlatformSessionManager(const QString &id, const QString &key) const
{
return new QCocoaSessionManager(id, key);
}
#endif
bool QCocoaIntegration::hasCapability(QPlatformIntegration::Capability cap) const
{
switch (cap) {
#ifndef QT_NO_OPENGL
case ThreadedOpenGL:
// AppKit expects rendering to happen on the main thread, and we can
// easily end up in situations where rendering on secondary threads
// will result in visual artifacts, bugs, or even deadlocks, when
// layer-backed.
return false;
case OpenGL:
#if defined(Q_PROCESSOR_ARM)
if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSTahoe) {
// Tahoe has issues with software-backed GL, crashing in common operations
static bool isSoftwareContext = []{
QOpenGLContext context;
context.create();
auto *cocoaContext = static_cast<QCocoaGLContext*>(context.handle());
if (cocoaContext->isSoftwareContext()) {
qWarning() << "Detected software OpenGL backend,"
<< "which is known to be broken on"
<< qUtf8Printable(QSysInfo::prettyProductName());
return true;
} else {
return false;
}
}();
return !isSoftwareContext;
}
#endif
Q_FALLTHROUGH();
case BufferQueueingOpenGL:
#endif
case ThreadedPixmaps:
case WindowMasks:
case MultipleWindows:
case ForeignWindows:
case ChildWindows:
case ApplicationState:
case ApplicationIcon:
case BackingStoreStaticContents:
case OffscreenSurface:
return true;
case ScreenWindowGrabbing: {
#if defined(Q_PROCESSOR_X86_64)
// ScreenCaptureKit brings down WindowServer on macOS 14 x86_64 VMs in CI
if (QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSSonoma
&& qEnvironmentVariable("QTEST_ENVIRONMENT") == "ci") {
return false;
}
#endif
return true;
}
case MouseCursorPositioning:
return AXIsProcessTrustedWithOptions(CFDictionaryRef(@{
id(kAXTrustedCheckOptionPrompt): @NO
}));
default:
return QPlatformIntegration::hasCapability(cap);
}
}
QPlatformWindow *QCocoaIntegration::createPlatformWindow(QWindow *window) const
{
return new QCocoaWindow(window);
}
QPlatformWindow *QCocoaIntegration::createForeignWindow(QWindow *window, WId nativeHandle) const
{
return new QCocoaWindow(window, nativeHandle);
}
class QCocoaOffscreenSurface : public QPlatformOffscreenSurface
{
public:
QCocoaOffscreenSurface(QOffscreenSurface *offscreenSurface) : QPlatformOffscreenSurface(offscreenSurface) {}
QSurfaceFormat format() const override
{
Q_ASSERT(offscreenSurface());
return offscreenSurface()->requestedFormat();
}
bool isValid() const override { return true; }
};
QPlatformOffscreenSurface *QCocoaIntegration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const
{
return new QCocoaOffscreenSurface(surface);
}
#ifndef QT_NO_OPENGL
QPlatformOpenGLContext *QCocoaIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const
{
return new QCocoaGLContext(context);
}
QOpenGLContext *QCocoaIntegration::createOpenGLContext(NSOpenGLContext *nativeContext, QOpenGLContext *shareContext) const
{
if (!nativeContext)
return nullptr;
auto *context = new QOpenGLContext;
context->setShareContext(shareContext);
auto *contextPrivate = QOpenGLContextPrivate::get(context);
contextPrivate->adopt(new QCocoaGLContext(nativeContext));
return context;
}
#endif
QPlatformBackingStore *QCocoaIntegration::createPlatformBackingStore(QWindow *window) const
{
QMacAutoReleasePool pool;
QCocoaWindow *platformWindow = static_cast<QCocoaWindow*>(window->handle());
if (!platformWindow) {
qWarning() << window << "must be created before being used with a backingstore";
return nullptr;
}
switch (window->surfaceType()) {
case QSurface::RasterSurface:
return new QCALayerBackingStore(window);
case QSurface::MetalSurface:
case QSurface::OpenGLSurface:
case QSurface::VulkanSurface:
// If the window is a widget window, we know that the QWidgetRepaintManager
// will explicitly use rhiFlush() for the window owning the backingstore,
// and any child window with the same surface format. This means we can
// safely return a QCALayerBackingStore here, to ensure that any plain
// flush() for child windows that don't have a matching surface format
// will still work, by setting the layer's contents property.
if (window->inherits("QWidgetWindow"))
return new QCALayerBackingStore(window);
// Otherwise we return a QRhiBackingStore, that implements flush() in
// terms of rhiFlush().
return new QRhiBackingStore(window);
default:
return nullptr;
}
}
QAbstractEventDispatcher *QCocoaIntegration::createEventDispatcher() const
{
return new QCocoaEventDispatcher;
}
#if QT_CONFIG(vulkan)
QPlatformVulkanInstance *QCocoaIntegration::createPlatformVulkanInstance(QVulkanInstance *instance) const
{
mCocoaVulkanInstance = new QCocoaVulkanInstance(instance);
return mCocoaVulkanInstance;
}
QCocoaVulkanInstance *QCocoaIntegration::getCocoaVulkanInstance() const
{
return mCocoaVulkanInstance;
}
#endif
QCoreTextFontDatabase *QCocoaIntegration::fontDatabase() const
{
return mFontDb.data();
}
QCocoaNativeInterface *QCocoaIntegration::nativeInterface() const
{
return mNativeInterface.data();
}
QPlatformInputContext *QCocoaIntegration::inputContext() const
{
return mInputContext.data();
}
#if QT_CONFIG(accessibility)
QCocoaAccessibility *QCocoaIntegration::accessibility() const
{
return mAccessibility.data();
}
#endif
#ifndef QT_NO_CLIPBOARD
QCocoaClipboard *QCocoaIntegration::clipboard() const
{
return mCocoaClipboard;
}
#endif
QCocoaDrag *QCocoaIntegration::drag() const
{
return mCocoaDrag.data();
}
QStringList QCocoaIntegration::themeNames() const
{
return QStringList(QLatin1StringView(QCocoaTheme::name));
}
QPlatformTheme *QCocoaIntegration::createPlatformTheme(const QString &name) const
{
if (name == QLatin1StringView(QCocoaTheme::name))
return new QCocoaTheme;
return QPlatformIntegration::createPlatformTheme(name);
}
QCocoaServices *QCocoaIntegration::services() const
{
if (mServices.isNull())
mServices.reset(new QCocoaServices);
return mServices.data();
}
QVariant QCocoaIntegration::styleHint(StyleHint hint) const
{
switch (hint) {
case FontSmoothingGamma:
return QCoreTextFontEngine::fontSmoothingGamma();
case ShowShortcutsInContextMenus:
return QVariant(false);
case ReplayMousePressOutsidePopup:
return QVariant(false);
default: break;
}
return QPlatformIntegration::styleHint(hint);
}
QPlatformKeyMapper *QCocoaIntegration::keyMapper() const
{
return mKeyboardMapper.data();
}
void QCocoaIntegration::setApplicationIcon(const QIcon &icon) const
{
if (icon.isNull()) {
NSApp.applicationIconImage = nil;
return;
}
// Request a size that looks good on the highest resolution screen available
// for icon engines that don't have an intrinsic size (like SVG).
const auto dockTitleSize = QSizeF::fromCGSize(NSApp.dockTile.size).toSize();
auto image = icon.pixmap(dockTitleSize, qGuiApp->devicePixelRatio()).toImage();
// The assigned image is scaled by the system to fit into the tile,
// but without taking aspect ratio into account, so let's pad the
// image up front if it's not already square.
image = qt_mac_padToSquareImage(image);
NSApp.applicationIconImage = [NSImage imageFromQImage:image];
}
void QCocoaIntegration::setApplicationBadge(qint64 number)
{
NSApp.dockTile.badgeLabel = number ? [NSString stringWithFormat:@"%" PRId64, number] : nil;
}
void QCocoaIntegration::beep() const
{
NSBeep();
}
void QCocoaIntegration::quit() const
{
qCDebug(lcQpaApplication) << "Terminating application";
[NSApp terminate:nil];
}
void QCocoaIntegration::focusWindowChanged(QWindow *focusWindow)
{
// Don't revert icon just because we lost focus
if (!focusWindow)
return;
static bool hasDefaultApplicationIcon = [](){
NSImage *genericApplicationIcon = [NSWorkspace.sharedWorkspace
iconForContentType:UTTypeApplicationBundle];
NSImage *applicationIcon = [NSImage imageNamed:NSImageNameApplicationIcon];
NSRect rect = NSMakeRect(0, 0, 32, 32);
return [applicationIcon CGImageForProposedRect:&rect context:nil hints:nil]
== [genericApplicationIcon CGImageForProposedRect:&rect context:nil hints:nil];
}();
// Don't let the window icon override an explicit application icon set in the Info.plist
if (!hasDefaultApplicationIcon)
return;
// Or an explicit application icon set on QGuiApplication
if (!qGuiApp->windowIcon().isNull())
return;
setApplicationIcon(focusWindow->icon());
}
QT_END_NAMESPACE
#include "moc_qcocoaintegration.cpp"