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 <QuartzCore/QuartzCore.h>
#include "qcocoawindow.h"
#include "qcocoaintegration.h"
#include "qcocoascreen.h"
#include "qnswindowdelegate.h"
#include "qcocoaeventdispatcher.h"
#ifndef QT_NO_OPENGL
#include "qcocoaglcontext.h"
#endif
#include "qcocoahelpers.h"
#include "qcocoanativeinterface.h"
#include "qcocoaaccessibilityelement.h"
#include "qnsview.h"
#include "qnswindow.h"
#include <QtCore/qfileinfo.h>
#include <QtCore/private/qcore_mac_p.h>
#include <qwindow.h>
#include <private/qwindow_p.h>
#include <QtGui/private/qpointingdevice_p.h>
#include <QtGui/private/qeventpoint_p.h>
#include <qpa/qwindowsysteminterface.h>
#include <qpa/qplatformscreen.h>
#include <QtGui/qaccessible.h>
#include <QtGui/private/qcoregraphics_p.h>
#include <QtGui/private/qhighdpiscaling_p.h>
#include <QtGui/private/qmetallayer_p.h>
#include <QDebug>
#include <vector>
QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QNSWindowController, NSWindowController
- (instancetype)initWithCocoaWindow:(QCocoaWindow *)platformWindow;
)
@implementation QNSWindowController {
QPointer<QCocoaWindow> m_platformWindow;
}
- (instancetype)initWithCocoaWindow:(QCocoaWindow *)platformWindow
{
// We need a nib name, even if we don't use a NIB, otherwise
// Cocoa is not going to call our loadWindow override.
if ((self = [super initWithWindowNibName:@"QNSWindowController"]))
m_platformWindow = platformWindow;
return self;
}
- (void)loadWindow
{
QMacAutoReleasePool pool;
self.window = [m_platformWindow->createNSWindow() autorelease];
}
- (void)dealloc
{
qCDebug(lcQpaWindow) << "Disposing of" << self.window << "for" << m_platformWindow;
[self.window close];
self.window = nil;
[super dealloc];
}
@end
QT_BEGIN_NAMESPACE
enum {
defaultWindowWidth = 160,
defaultWindowHeight = 160
};
Q_LOGGING_CATEGORY(lcCocoaNotifications, "qt.qpa.cocoa.notifications");
static void qRegisterNotificationCallbacks()
{
static const QLatin1StringView notificationHandlerPrefix(Q_NOTIFICATION_PREFIX);
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
const QMetaObject *metaObject = QMetaType(qRegisterMetaType<QCocoaWindow*>()).metaObject();
Q_ASSERT(metaObject);
for (int i = 0; i < metaObject->methodCount(); ++i) {
QMetaMethod method = metaObject->method(i);
const QString methodTag = QString::fromLatin1(method.tag());
if (!methodTag.startsWith(notificationHandlerPrefix))
continue;
const QString notificationName = methodTag.mid(notificationHandlerPrefix.size());
[center addObserverForName:notificationName.toNSString() object:nil queue:nil
usingBlock:^(NSNotification *notification) {
QVarLengthArray<QCocoaWindow *, 32> cocoaWindows;
if ([notification.object isKindOfClass:[NSWindow class]]) {
NSWindow *nsWindow = notification.object;
for (const QWindow *window : QGuiApplication::allWindows()) {
if (QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()))
if (cocoaWindow->nativeWindow() == nsWindow)
cocoaWindows += cocoaWindow;
}
} else if ([notification.object isKindOfClass:[NSView class]]) {
if (QNSView *qnsView = qnsview_cast(notification.object))
cocoaWindows += qnsView.platformWindow;
} else {
qCWarning(lcCocoaNotifications) << "Unhandled notification"
<< notification.name << "for" << notification.object;
return;
}
if (lcCocoaNotifications().isDebugEnabled() && !cocoaWindows.isEmpty()) {
QVector<QCocoaWindow *> debugWindows;
for (QCocoaWindow *cocoaWindow : cocoaWindows)
debugWindows += cocoaWindow;
qCDebug(lcCocoaNotifications) << "Forwarding" << qPrintable(notificationName) <<
"to" << debugWindows;
}
// FIXME: Could be a foreign window, look up by iterating top level QWindows
for (QCocoaWindow *cocoaWindow : cocoaWindows) {
if (!method.invoke(cocoaWindow, Qt::DirectConnection)) {
qCWarning(lcQpaWindow) << "Failed to invoke NSNotification callback for"
<< notification.name << "on" << cocoaWindow;
}
}
}];
}
}
Q_CONSTRUCTOR_FUNCTION(qRegisterNotificationCallbacks)
const int QCocoaWindow::NoAlertRequest = -1;
QPointer<QCocoaWindow> QCocoaWindow::s_windowUnderMouse;
QCocoaWindow::QCocoaWindow(QWindow *win, WId nativeHandle) : QPlatformWindow(win)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::QCocoaWindow" << window();
if (nativeHandle) {
m_view = reinterpret_cast<NSView *>(nativeHandle);
[m_view retain];
}
}
void QCocoaWindow::initialize()
{
qCDebug(lcQpaWindow) << "QCocoaWindow::initialize" << window();
QMacAutoReleasePool pool;
if (!m_view)
m_view = [[QNSView alloc] initWithCocoaWindow:this];
if (!isForeignWindow()) {
// Compute the initial geometry based on the geometry set on the
// QWindow, with automatic positioning and sizing, if the position
// or size has been left unset.
auto initialGeometry = QPlatformWindow::initialGeometry(window(),
windowGeometry(), defaultWindowWidth, defaultWindowHeight);
// Note: The initial geometry does not incorporate whether the
// positionPolicy includes the frame or not. It's up to us to
// account for that below.
if (QPlatformWindow::parent() || isSubWindow(window())) {
// If we're not a top level we need to establish the superview
// relationship first, before we can set the geometry, so that we
// know whether the superview is flipped or not when setting the
// geometry.
recreateWindowIfNeeded();
setGeometry(initialGeometry);
} else {
if (window()->flags() & Qt::ExpandedClientAreaHint) {
// Make sure the expanded client area hint actually expands the
// client size to incorporate the frame size, had it been there.
QRect frameGeometryWithFrame = QCocoaScreen::mapFromNative(
[NSWindow frameRectForContentRect:QCocoaScreen::mapToNative(initialGeometry)
styleMask:windowStyleMask(window()->flags() & ~Qt::ExpandedClientAreaHint)]).toRect();
if (qt_window_private(window())->positionPolicy == QWindowPrivate::WindowFrameExclusive)
initialGeometry = frameGeometryWithFrame;
else
initialGeometry.setSize(frameGeometryWithFrame.size());
}
// If we're a top level window we need to create the NSWindow
// first, so that we know the frame margins of the window. But
// since the geometry will be applied during setContentView we
// must persist the initial geometry here, so that it's picked
// up that that point.
QPlatformWindow::setGeometry(initialGeometry);
recreateWindowIfNeeded();
// We don't need to set the initial geometry again here. And we
// must also be careful to not setGeometry with the newly adopted
// geometry, as that is now effecively WindowFrameExclusive, while
// the QWindow might still be set to WindowFrameInclusive.
}
setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window()));
m_safeAreaInsetsObserver = QMacKeyValueObserver(
m_view, @"safeAreaInsets", [this] {
// Defer to next runloop pass, so that any changes to the
// margins during resizing have settled down.
QMetaObject::invokeMethod(this, [this]{
updateSafeAreaMarginsIfNeeded();
}, Qt::QueuedConnection);
}, NSKeyValueObservingOptionNew);
} else {
// Reparent to superview if needed
recreateWindowIfNeeded();
// Pick up essential foreign window state
QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect());
}
m_initialized = true;
}
const NSNotificationName QCocoaWindowWillReleaseQNSViewNotification = @"QCocoaWindowWillReleaseQNSViewNotification";
QCocoaWindow::~QCocoaWindow()
{
qCDebug(lcQpaWindow) << "QCocoaWindow::~QCocoaWindow" << window();
QMacAutoReleasePool pool;
if (window()->flags() & Qt::ExpandedClientAreaHint) {
// We expanded the client area during initialization, so we need
// to adjust the size back now, so that recreating the window again
// will not keep growing it as we re-expand the size.
auto readjustedGeometry = geometry();
QRect contentRectWithFrame = QCocoaScreen::mapFromNative(
[NSWindow contentRectForFrameRect:QCocoaScreen::mapToNative(geometry())
styleMask:windowStyleMask(window()->flags() & ~Qt::ExpandedClientAreaHint)]).toRect();
readjustedGeometry = contentRectWithFrame;
setGeometry(readjustedGeometry, QWindowPrivate::PositionPolicy::WindowFrameExclusive);
}
// Remove from superview as long as we're not a foreign
// window used only to contain Qt windows (ie no parent)
if (!isForeignWindow() || QPlatformWindow::parent())
[m_view removeFromSuperview];
#if QT_CONFIG(accessibility)
if (isForeignWindow())
clearAccessibleParent();
#endif
m_safeAreaInsetsObserver = {};
// Make sure to disconnect observer in all case if view is valid
// to avoid notifications received when deleting when using Qt::AA_NativeWindows attribute
if (!isForeignWindow())
[[NSNotificationCenter defaultCenter] removeObserver:m_view];
#if QT_CONFIG(vulkan)
if (QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance()) {
auto vulcanInstance = cocoaIntegration->getCocoaVulkanInstance();
if (vulcanInstance)
vulcanInstance->destroySurface(m_vulkanSurface);
}
#endif
// Must send notification before calling release, as doing it from
// [QNSView dealloc] would mean that any weak references to the view
// would already return nil.
[NSNotificationCenter.defaultCenter
postNotificationName:QCocoaWindowWillReleaseQNSViewNotification
object:m_view];
[m_view release];
// Disposing of the view and window should have resulted in an
// expose event with isExposed=false, but just in case we try
// to stop the display link here as well.
static_cast<QCocoaScreen *>(screen())->maybeStopDisplayLink();
}
QSurfaceFormat QCocoaWindow::format() const
{
auto format = window()->requestedFormat();
if (auto *view = qnsview_cast(m_view); view.colorSpace) {
auto colorSpace = QColorSpace::fromIccProfile(QByteArray::fromNSData(view.colorSpace.ICCProfileData));
if (!colorSpace.isValid()) {
qCWarning(lcQpaWindow) << "Failed to parse ICC profile for" << view.colorSpace
<< "with ICC data" << view.colorSpace.ICCProfileData;
}
format.setColorSpace(colorSpace);
}
return format;
}
bool QCocoaWindow::isForeignWindow() const
{
return ![m_view isKindOfClass:[QNSView class]];
}
QRect QCocoaWindow::geometry() const
{
return QPlatformWindow::geometry();
}
QRect QCocoaWindow::actualGeometry() const
{
if (isContentView() && !isEmbedded()) {
// Content views are positioned at (0, 0) in the window, so we resolve via the window
CGRect contentRect = [m_view.window contentRectForFrameRect:m_view.window.frame];
// The result above is in native screen coordinates, so remap to the Qt coordinate system
return QCocoaScreen::mapFromNative(contentRect).toRect();
} else {
return QCocoaWindow::mapFromNative(m_view.frame, m_view.superview).toRect();
}
}
/*!
\brief the geometry of the window as it will appear when shown as
a normal (not maximized or full screen) top-level window.
For child windows this property always holds an empty rectangle.
\sa QWidget::normalGeometry()
*/
QRect QCocoaWindow::normalGeometry() const
{
if (!isContentView())
return QRect();
// We only persist the normal the geometry when going into
// fullscreen and maximized states. For all other cases we
// can just report the geometry as is.
if (!(windowState() & (Qt::WindowFullScreen | Qt::WindowMaximized)))
return geometry();
return m_normalGeometry;
}
void QCocoaWindow::updateNormalGeometry()
{
if (!isContentView())
return;
if (windowState() != Qt::WindowNoState)
return;
m_normalGeometry = geometry();
}
void QCocoaWindow::setGeometry(const QRect &rect)
{
QScopedValueRollback inSetGeometry(m_inSetGeometry, true);
setGeometry(rect, qt_window_private(window())->positionPolicy);
}
void QCocoaWindow::setGeometry(const QRect &rectIn, QWindowPrivate::PositionPolicy positionPolicy)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::setGeometry" << window() << rectIn << positionPolicy;
QMacAutoReleasePool pool;
QRect rect = rectIn;
if (positionPolicy == QWindowPrivate::WindowFrameInclusive) {
// This means it is a call from QWindow::setFramePosition(), so the coordinates
// include the frame (size is still the contents rectangle). As the functionality
// below operates purely in content positions, we need to remove the frame margins.
const QMargins margins = frameMargins();
rect.moveTopLeft(rect.topLeft() + QPoint(margins.left(), margins.top()));
qCDebug(lcQpaWindow) << "Adjusted content geometry to" << rect << "by removing frame margins" << margins;
}
QPlatformWindow::setGeometry(rect);
const QRect originalGeometry = actualGeometry();
if (isContentView()) {
if (isEmbedded()) {
// Sizing or moving the content view doesn't make sense when
// we are embedded, so report the current geometry as is.
QCocoaWindow::handleGeometryChange();
} else {
NSRect bounds = QCocoaScreen::mapToNative(rect);
[m_view.window setFrame:[m_view.window frameRectForContentRect:bounds] display:YES animate:NO];
}
} else {
m_view.frame = QCocoaWindow::mapToNative(rect, m_view.superview);
}
if (actualGeometry() == originalGeometry) {
// The requested geometry change was rejected by the OS. This can
// happen when trying to move a window past the top of the screen,
// which is not allowed. Normally, when the OS adjusts the geometry
// we requested, it will be updated via windowDidMove/Resize, but
// if the rejected change ends up the same as the old geometry these
// callbacks won't be called, so we have to update manually here.
qCDebug(lcQpaWindow) << "Native geometry didn't change. Reporting geometry change manually.";
handleGeometryChange();
}
}
QMargins QCocoaWindow::safeAreaMargins() const
{
QMacAutoReleasePool pool;
// The safe area of the view reflects the area not covered by navigation
// bars, tab bars, toolbars, and other ancestor views that might obscure
// the current view (by setting additionalSafeAreaInsets). If the window
// uses NSWindowStyleMaskFullSizeContentView this also includes the area
// of the view covered by the title bar.
QMarginsF viewSafeAreaMargins = {
m_view.safeAreaInsets.left,
m_view.safeAreaInsets.top,
m_view.safeAreaInsets.right,
m_view.safeAreaInsets.bottom
};
// The screen's safe area insets represent the distances from the screen's
// edges at which content isn't obscured. The view's safe area margins do
// not include the screen's insets automatically, so we need to manually
// merge them.
auto screenRect = m_view.window.screen.frame;
auto screenInsets = m_view.window.screen.safeAreaInsets;
auto screenSafeArea = QCocoaScreen::mapFromNative(NSMakeRect(
NSMinX(screenRect) + screenInsets.left,
NSMinY(screenRect) + screenInsets.bottom, // Non-flipped
NSWidth(screenRect) - screenInsets.left - screenInsets.right,
NSHeight(screenRect) - screenInsets.top - screenInsets.bottom
));
auto screenRelativeViewBounds = QCocoaScreen::mapFromNative(
[m_view.window convertRectToScreen:
[m_view convertRect:m_view.bounds toView:nil]]
);
// The margins are relative to the screen the window is on.
// Note that we do not want represent the area outside of the
// screen as being outside of the safe area.
QMarginsF screenSafeAreaMargins = {
qMin(screenSafeArea.left() - screenRelativeViewBounds.left(), screenInsets.left),
qMin(screenSafeArea.top() - screenRelativeViewBounds.top(), screenInsets.top),
qMin(screenRelativeViewBounds.right() - screenSafeArea.right(), screenInsets.right),
qMin(screenRelativeViewBounds.bottom() - screenSafeArea.bottom(), screenInsets.bottom)
};
return (screenSafeAreaMargins | viewSafeAreaMargins).toMargins();
}
void QCocoaWindow::updateSafeAreaMarginsIfNeeded()
{
if (safeAreaMargins() != m_lastReportedSafeAreaMargins) {
m_lastReportedSafeAreaMargins = safeAreaMargins();
QWindowSystemInterface::handleSafeAreaMarginsChanged(window());
}
}
bool QCocoaWindow::startSystemMove()
{
// Only allow move by pressing left mouse button
if (!(NSEvent.pressedMouseButtons == 1))
return false;
// Synthesize an event, so that we don't have to rely on
// NSApp.currentEvent, which may not be a mouse event.
NSEvent *mouseEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
location:m_view.window.mouseLocationOutsideOfEventStream
modifierFlags:NSEvent.modifierFlags
timestamp:NSProcessInfo.processInfo.systemUptime
windowNumber:m_view.window.windowNumber context:nil
eventNumber:0 clickCount:1 pressure:1.0];
[m_view.window performWindowDragWithEvent:mouseEvent];
return true;
}
void QCocoaWindow::setVisible(bool visible)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::setVisible" << window() << visible;
// Our implementation of setVisible below is not idempotent, as for
// modal windows it calls beginSheet/endSheet or starts/ends modal
// sessions. However we can't simply guard for m_view.hidden already
// having the right state, as the behavior of this function differs
// based on whether the window has been initialized or not, as
// handleGeometryChange will bail out if the window is still
// initializing. Since we know we'll get a second setVisible
// call after creation, we can check for that case specifically,
// which means we can then safely guard on m_view.hidden changing.
if (!m_initialized) {
qCDebug(lcQpaWindow) << "Window still initializing, skipping setting visibility";
return; // We'll get another setVisible call after create is done
}
if (visible == !m_view.hidden && (!isContentView() || visible == m_view.window.visible)) {
qCDebug(lcQpaWindow) << "No change in visible status. Ignoring.";
return;
}
if (m_inSetVisible) {
qCWarning(lcQpaWindow) << "Already setting window visible!";
return;
}
QScopedValueRollback<bool> rollback(m_inSetVisible, true);
QMacAutoReleasePool pool;
QCocoaWindow *parentCocoaWindow = nullptr;
if (window()->transientParent())
parentCocoaWindow = static_cast<QCocoaWindow *>(window()->transientParent()->handle());
auto eventDispatcher = [] {
return static_cast<QCocoaEventDispatcherPrivate *>(QObjectPrivate::get(qApp->eventDispatcher()));
};
if (visible) {
// The flags may have changed, in which case we may need to switch window type
recreateWindowIfNeeded();
// We didn't send geometry changes during creation, as that would have confused
// Qt, which expects a show-event to be sent before any resize events. But now
// that the window is made visible, we know that the show-event has been sent
// so we can send the geometry change. FIXME: Get rid of this workaround.
handleGeometryChange();
if (parentCocoaWindow) {
// The parent window might have moved while this window was hidden,
// update the window geometry if there is a parent.
setGeometry(windowGeometry());
if (window()->type() == Qt::Popup) {
// QTBUG-30266: a window should not be resizable while a transient popup is open
// Since this isn't a native popup, the window manager doesn't close the popup when you click outside
NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
NSUInteger parentStyleMask = nativeParentWindow.styleMask;
if ((m_resizableTransientParent = (parentStyleMask & NSWindowStyleMaskResizable))
&& !(nativeParentWindow.styleMask & NSWindowStyleMaskFullScreen))
nativeParentWindow.styleMask &= ~NSWindowStyleMaskResizable;
}
}
// Make the NSView visible first, before showing the NSWindow (in case of top level windows)
m_view.hidden = NO;
if (isContentView()) {
QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents);
// setWindowState might have been called while the window was hidden and
// will not change the NSWindow state in that case. Sync up here:
applyWindowState(window()->windowStates());
if (window()->windowState() != Qt::WindowMinimized) {
if (parentCocoaWindow && (window()->modality() == Qt::WindowModal || window()->type() == Qt::Sheet)) {
// Show the window as a sheet
NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
if (!nativeParentWindow.attachedSheet)
[nativeParentWindow beginSheet:m_view.window completionHandler:nil];
else
[nativeParentWindow beginCriticalSheet:m_view.window completionHandler:nil];
} else if (window()->modality() == Qt::ApplicationModal) {
// Show the window as application modal
eventDispatcher()->beginModalSession(window());
} else if (m_view.window.canBecomeKeyWindow) {
bool shouldBecomeKeyNow = !NSApp.modalWindow
|| m_view.window.worksWhenModal
|| !NSApp.modalWindow.visible;
// Panels with becomesKeyOnlyIfNeeded set should not activate until a view
// with needsPanelToBecomeKey, for example a line edit, is clicked.
if ([m_view.window isKindOfClass:[NSPanel class]])
shouldBecomeKeyNow &= !(static_cast<NSPanel*>(m_view.window).becomesKeyOnlyIfNeeded);
if (shouldBecomeKeyNow)
[m_view.window makeKeyAndOrderFront:nil];
else
[m_view.window orderFront:nil];
} else {
[m_view.window orderFront:nil];
}
}
}
} else {
// Window not visible, hide it
if (isContentView()) {
if (eventDispatcher()->hasModalSession())
eventDispatcher()->endModalSession(window());
else if ([m_view.window isSheet])
[m_view.window.sheetParent endSheet:m_view.window];
// Note: We do not guard the order out by checking NSWindow.visible, as AppKit will
// in some cases, such as when hiding the application, order out and make a window
// invisible, but keep it in a list of "hidden windows", that it then restores again
// when the application is unhidden. We need to call orderOut explicitly, to bring
// the window out of this "hidden list".
[m_view.window orderOut:nil];
if (m_view.window == [NSApp keyWindow] && !eventDispatcher()->hasModalSession()) {
// Probably because we call runModalSession: outside [NSApp run] in QCocoaEventDispatcher
// (e.g., when show()-ing a modal QDialog instead of exec()-ing it), it can happen that
// the current NSWindow is still key after being ordered out. Then, after checking we
// don't have any other modal session left, it's safe to make the main window key again.
NSWindow *mainWindow = [NSApp mainWindow];
if (mainWindow && [mainWindow canBecomeKeyWindow])
[mainWindow makeKeyWindow];
}
}
// AppKit will in some cases set up the key view loop for child views, even if we
// don't set autorecalculatesKeyViewLoop, nor call recalculateKeyViewLoop ourselves.
// When a child window is promoted to a top level, AppKit will maintain the key view
// loop between the views, even if these views now cross NSWindows, even after we
// explicitly call recalculateKeyViewLoop. When the top level is then hidden, AppKit
// will complain when -[NSView _setHidden:setNeedsDisplay:] tries to transfer first
// responder by reading the nextValidKeyView, and it turns out to live in a different
// window. We mitigate this by a last second reset of the first responder, which is
// what AppKit also falls back to. It's unclear if the original situation of views
// having their nextKeyView pointing to views in other windows is kosher or not.
if (m_view.window.firstResponder == m_view && m_view.nextValidKeyView
&& m_view.nextValidKeyView.window != m_view.window) {
qCDebug(lcQpaWindow) << "Detected nextValidKeyView" << m_view.nextValidKeyView
<< "in different window" << m_view.nextValidKeyView.window
<< "Resetting" << m_view.window << "first responder to nil.";
[m_view.window makeFirstResponder:nil];
}
m_view.hidden = YES;
if (parentCocoaWindow && window()->type() == Qt::Popup) {
NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
if (m_resizableTransientParent
&& !(nativeParentWindow.styleMask & NSWindowStyleMaskFullScreen))
// A window should not be resizable while a transient popup is open
nativeParentWindow.styleMask |= NSWindowStyleMaskResizable;
}
}
}
NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags)
{
Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
NSInteger windowLevel = NSNormalWindowLevel;
if (type == Qt::Tool)
windowLevel = NSFloatingWindowLevel;
else if ((type & Qt::Popup) == Qt::Popup)
windowLevel = NSPopUpMenuWindowLevel;
// StayOnTop window should appear above Tool windows.
if (flags & Qt::WindowStaysOnTopHint)
windowLevel = NSModalPanelWindowLevel;
// Tooltips should appear above StayOnTop windows.
if (type == Qt::ToolTip)
windowLevel = NSScreenSaverWindowLevel;
auto *transientParent = window()->transientParent();
if (transientParent && transientParent->handle()) {
// We try to keep windows in at least the same window level as
// their transient parent. Unfortunately this only works when the
// window is created. If the window level changes after that, as
// a result of a call to setWindowFlags, or by changing the level
// of the native window, we will not pick this up, and the window
// will be left behind (or in a different window level than) its
// parent. We could KVO-observe the window level of our transient
// parent, but that requires us to know when the parent goes away
// so that we can unregister the observation before the parent is
// dealloced, something we can't do for generic NSWindows. Another
// way would be to override [NSWindow setLevel:] and notify child
// windows about the change, but that doesn't work for foreign
// windows, which can still be transient parents via fromWinId().
// One area where this problem is apparent is when AppKit tweaks
// the window level of modal windows during application activation
// and deactivation. Since we don't pick up on these window level
// changes in a generic way, we need to add logic explicitly to
// re-evaluate the window level after AppKit has done its tweaks.
auto *transientCocoaWindow = static_cast<QCocoaWindow *>(transientParent->handle());
auto *nsWindow = transientCocoaWindow->nativeWindow();
// We only upgrade the window level for "special" windows, to work
// around Qt Widgets Designer parenting the designer windows to the widget
// palette window (QTBUG-31779). This should be fixed in designer.
if (type != Qt::Window)
windowLevel = qMax(windowLevel, nsWindow.level);
}
return windowLevel;
}
NSUInteger QCocoaWindow::windowStyleMask(Qt::WindowFlags flags) const
{
const Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
// Determine initial style mask based on whether the window should
// have a frame and title or not. The NSWindowStyleMaskBorderless
// and NSWindowStyleMaskTitled styles are mutually exclusive, with
// values of 0 and 1 correspondingly.
NSUInteger styleMask = [&]{
// Honor explicit requests for borderless windows
if (flags & Qt::FramelessWindowHint)
return NSWindowStyleMaskBorderless;
// Popup windows should always be borderless
if (windowIsPopupType(type))
return NSWindowStyleMaskBorderless;
if (flags & Qt::CustomizeWindowHint) {
// CustomizeWindowHint turns off the default window title hints,
// so the choice is then up to the user via Qt::WindowTitleHint.
return flags & Qt::WindowTitleHint
? NSWindowStyleMaskTitled
: NSWindowStyleMaskBorderless;
} else {
// Otherwise, default to using titled windows
return NSWindowStyleMaskTitled;
}
}();
// We determine which buttons to show in updateTitleBarButtons,
// so we can enable all the relevant style masks here to ensure
// that behaviors that don't involve the title bar buttons are
// working (for example minimizing frameless windows, or resizing
// windows that don't have zoom or fullscreen titlebar buttons).
styleMask |= NSWindowStyleMaskClosable
| NSWindowStyleMaskMiniaturizable;
if (type != Qt::Popup) // We only care about popups exactly.
styleMask |= NSWindowStyleMaskResizable;
if (type == Qt::Tool)
styleMask |= NSWindowStyleMaskUtilityWindow;
if (flags & Qt::ExpandedClientAreaHint)
styleMask |= NSWindowStyleMaskFullSizeContentView;
// Don't wipe existing states for style flags we don't control here
styleMask |= (m_view.window.styleMask & (
NSWindowStyleMaskFullScreen
| NSWindowStyleMaskUnifiedTitleAndToolbar
| NSWindowStyleMaskDocModalWindow
| NSWindowStyleMaskNonactivatingPanel
| NSWindowStyleMaskHUDWindow));
return styleMask;
}
bool QCocoaWindow::isFixedSize() const
{
return windowMinimumSize().isValid() && windowMaximumSize().isValid()
&& windowMinimumSize() == windowMaximumSize();
}
void QCocoaWindow::updateTitleBarButtons(Qt::WindowFlags windowFlags)
{
if (!isContentView())
return;
static constexpr std::pair<NSWindowButton, Qt::WindowFlags> buttons[] = {
{ NSWindowCloseButton, Qt::WindowCloseButtonHint },
{ NSWindowMiniaturizeButton, Qt::WindowMinimizeButtonHint},
{ NSWindowZoomButton, Qt::WindowMaximizeButtonHint | Qt::WindowFullscreenButtonHint }
};
bool hideButtons = true;
for (const auto &[button, buttonHint] : buttons) {
// Set up Qt defaults based on window type
bool enabled = true;
if (button == NSWindowMiniaturizeButton)
enabled = window()->type() != Qt::Dialog;
// Let users override via CustomizeWindowHint
if (windowFlags & Qt::CustomizeWindowHint)
enabled = windowFlags & buttonHint;
// Then do some final sanitizations
if (button == NSWindowZoomButton && isFixedSize())
enabled = false;
// Mimic what macOS natively does for parent windows of modal
// sheets, which is to disable the close button, but leave the
// other buttons as they were.
if (button == NSWindowCloseButton && enabled
&& QWindowPrivate::get(window())->blockedByModalWindow) {
enabled = false;
// If we end up having no enabled buttons, our workaround
// should not be a reason for hiding all of them.
hideButtons = false;
}
[m_view.window standardWindowButton:button].enabled = enabled;
hideButtons &= !enabled;
}
// Hide buttons in case we disabled all of them
for (const auto &[button, buttonHint] : buttons)
[m_view.window standardWindowButton:button].hidden = hideButtons;
}
void QCocoaWindow::setWindowFlags(Qt::WindowFlags flags)
{
// Updating the window flags may affect the window's theme frame, which
// in the process retains and then autoreleases the NSWindow. To make
// sure this doesn't leave lingering releases when there is no pool in
// place (e.g. during main(), before exec), we add one locally here.
QMacAutoReleasePool pool;
if (!isContentView())
return;
qCDebug(lcQpaWindow) << "Setting" << flags << "for" << window();
// FIXME: Some flags may require a different NSWindow class, in
// which case we should recreate here, after first updating the
// QWindow flags state. But currently applyContentBorderThickness
// depends on setWindowFlags, causing a recursion, so we need to
// clean up that entanglement first.
{
// While setting style mask we can have handleGeometryChange calls on a content
// view with null geometry, reporting an invalid coordinates as a result.
QScopedValueRollback<bool> geometryChangeBlocker(m_inSetStyleMask, true);
const auto newMask = windowStyleMask(flags);
const bool expandedClientAreaChanged = (
(newMask & NSWindowStyleMaskFullSizeContentView) !=
(m_view.window.styleMask & NSWindowStyleMaskFullSizeContentView));
const QRect frameGeometry = geometry().marginsAdded(frameMargins());
m_view.window.styleMask = newMask;
if (expandedClientAreaChanged) {
// Maintain stable frame geometry when toggling expanded client area
setGeometry(frameGeometry.marginsRemoved(frameMargins()),
QWindowPrivate::PositionPolicy::WindowFrameExclusive);
}
}
Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
if ((type & Qt::Popup) != Qt::Popup && (type & Qt::Dialog) != Qt::Dialog) {
NSWindowCollectionBehavior behavior = m_view.window.collectionBehavior;
const bool enableFullScreen = m_view.window.qt_fullScreen
|| !(flags & Qt::CustomizeWindowHint)
|| (flags & Qt::WindowFullscreenButtonHint);
if (enableFullScreen) {
behavior |= NSWindowCollectionBehaviorFullScreenPrimary;
behavior &= ~NSWindowCollectionBehaviorFullScreenAuxiliary;
} else {
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
behavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
}
m_view.window.collectionBehavior = behavior;
}
// Set styleMask and collectionBehavior before applying window level, as
// the window level change will trigger verification of the two properties.
m_view.window.level = this->windowLevel(flags);
m_view.window.hasShadow = !(flags & Qt::NoDropShadowWindowHint);
if (!(flags & Qt::FramelessWindowHint))
setWindowTitle(window()->title());
updateTitleBarButtons(flags);
// Make window ignore mouse events if WindowTransparentForInput is set.
// Note that ignoresMouseEvents has a special initial state where events
// are ignored (passed through) based on window transparency, and that
// setting the property to false does not return us to that state. Instead,
// this makes the window capture all mouse events. Take care to only
// set the property if needed. FIXME: recreate window if needed or find
// some other way to implement WindowTransparentForInput.
bool ignoreMouse = flags & Qt::WindowTransparentForInput;
if (m_view.window.ignoresMouseEvents != ignoreMouse)
m_view.window.ignoresMouseEvents = ignoreMouse;
m_view.window.titlebarAppearsTransparent = flags & Qt::NoTitleBarBackgroundHint;
}
// ----------------------- Window state -----------------------
/*!
Changes the state of the NSWindow, going in/out of minimize/zoomed/fullscreen
When this is called from QWindow::setWindowState(), the QWindow state has not been
updated yet, so window()->windowState() will reflect the previous state that was
reported to QtGui.
*/
void QCocoaWindow::setWindowState(Qt::WindowStates state)
{
if (window()->isVisible())
applyWindowState(state); // Window state set for hidden windows take effect when show() is called
}
void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState)
{
if (!isContentView())
return;
const Qt::WindowState currentState = QWindowPrivate::effectiveState(windowState());
const Qt::WindowState newState = QWindowPrivate::effectiveState(requestedState);
if (newState == currentState)
return;
qCDebug(lcQpaWindow) << "Applying" << newState << "to" << window() << "in" << currentState;
const NSSize contentSize = m_view.frame.size;
if (contentSize.width <= 0 || contentSize.height <= 0) {
// If content view width or height is 0 then the window animations will crash so
// do nothing. We report the current state back to reflect the failed operation.
qWarning("invalid window content view size, check your window geometry");
handleWindowStateChanged(HandleUnconditionally);
return;
}
const NSWindow *nsWindow = m_view.window;
if (nsWindow.styleMask & NSWindowStyleMaskUtilityWindow
&& newState & (Qt::WindowMinimized | Qt::WindowFullScreen)) {
qWarning() << window()->type() << "windows cannot be made" << newState;
handleWindowStateChanged(HandleUnconditionally);
return;
}
const id sender = nsWindow;
// First we need to exit states that can't transition directly to other states
switch (currentState) {
case Qt::WindowMinimized:
[nsWindow deminiaturize:sender];
// Deminiaturizing is not synchronous, so we need to wait for the
// NSWindowDidMiniaturizeNotification before continuing to apply
// the new state.
return;
case Qt::WindowFullScreen: {
toggleFullScreen();
// Exiting fullscreen is not synchronous, so we need to wait for the
// NSWindowDidExitFullScreenNotification before continuing to apply
// the new state.
return;
}
default:;
}
// Then we apply the new state if needed
if (newState == windowState())
return;
switch (newState) {
case Qt::WindowFullScreen:
toggleFullScreen();
break;
case Qt::WindowMaximized:
toggleMaximized();
break;
case Qt::WindowMinimized:
[nsWindow miniaturize:sender];
break;
case Qt::WindowNoState:
if (windowState() == Qt::WindowMaximized)
toggleMaximized();
break;
default:
Q_UNREACHABLE();
}
}
Qt::WindowStates QCocoaWindow::windowState() const
{
Qt::WindowStates states = Qt::WindowNoState;
NSWindow *window = m_view.window;
if (window.miniaturized)
states |= Qt::WindowMinimized;
// Full screen and maximized are mutually exclusive, as macOS
// will report a full screen window as zoomed.
if (window.qt_fullScreen) {
states |= Qt::WindowFullScreen;
} else if ((window.zoomed && !isTransitioningToFullScreen())
|| (m_lastReportedWindowState == Qt::WindowMaximized && isTransitioningToFullScreen())) {
states |= Qt::WindowMaximized;
}
// Note: We do not report Qt::WindowActive, even if isActive()
// is true, as QtGui does not expect this window state to be set.
return states;
}
void QCocoaWindow::toggleMaximized()
{
const NSWindow *window = m_view.window;
// The NSWindow needs to be resizable, otherwise the window will
// not be possible to zoom back to non-zoomed state.
const bool wasResizable = window.styleMask & NSWindowStyleMaskResizable;
window.styleMask |= NSWindowStyleMaskResizable;
const id sender = window;
[window zoom:sender];
if (!wasResizable)
window.styleMask &= ~NSWindowStyleMaskResizable;
}
void QCocoaWindow::windowWillZoom()
{
updateNormalGeometry();
}
void QCocoaWindow::toggleFullScreen()
{
const NSWindow *window = m_view.window;
// The window needs to have the correct collection behavior for the
// toggleFullScreen call to have an effect. The collection behavior
// will be reset in windowDidEnterFullScreen/windowDidLeaveFullScreen.
window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
const id sender = window;
[window toggleFullScreen:sender];
}
void QCocoaWindow::windowWillEnterFullScreen()
{
if (!isContentView())
return;
updateNormalGeometry();
// The NSWindow needs to be resizable, otherwise we'll end up with
// the normal window geometry, centered in the middle of the screen
// on a black background. The styleMask will be reset below.
m_view.window.styleMask |= NSWindowStyleMaskResizable;
}
bool QCocoaWindow::isTransitioningToFullScreen() const
{
NSWindow *window = m_view.window;
return window.styleMask & NSWindowStyleMaskFullScreen && !window.qt_fullScreen;
}
void QCocoaWindow::windowDidEnterFullScreen()
{
if (!isContentView())
return;
Q_ASSERT_X(m_view.window.qt_fullScreen, "QCocoaWindow",
"FullScreen category processes window notifications first");
// Reset to original styleMask
setWindowFlags(window()->flags());
handleWindowStateChanged();
}
void QCocoaWindow::windowWillExitFullScreen()
{
if (!isContentView())
return;
// The NSWindow needs to be resizable, otherwise we'll end up with
// a weird zoom animation. The styleMask will be reset below.
m_view.window.styleMask |= NSWindowStyleMaskResizable;
}
void QCocoaWindow::windowDidExitFullScreen()
{
if (!isContentView())
return;
Q_ASSERT_X(!m_view.window.qt_fullScreen, "QCocoaWindow",
"FullScreen category processes window notifications first");
// Reset to original styleMask
setWindowFlags(window()->flags());
Qt::WindowState requestedState = window()->windowState();
// Deliver update of QWindow state
handleWindowStateChanged();
if (requestedState != windowState() && requestedState != Qt::WindowFullScreen) {
// We were only going out of full screen as an intermediate step before
// progressing into the final step, so re-sync the desired state.
applyWindowState(requestedState);
}
}
void QCocoaWindow::windowDidMiniaturize()
{
if (!isContentView())
return;
handleWindowStateChanged();
}
void QCocoaWindow::windowDidDeminiaturize()
{
if (!isContentView())
return;
Qt::WindowState requestedState = window()->windowState();
handleWindowStateChanged();
if (requestedState != windowState() && requestedState != Qt::WindowMinimized) {
// We were only going out of minimized as an intermediate step before
// progressing into the final step, so re-sync the desired state.
applyWindowState(requestedState);
}
}
void QCocoaWindow::handleWindowStateChanged(HandleFlags flags)
{
Qt::WindowStates currentState = windowState();
if (!(flags & HandleUnconditionally) && currentState == m_lastReportedWindowState)
return;
qCDebug(lcQpaWindow) << "QCocoaWindow::handleWindowStateChanged" <<
m_lastReportedWindowState << "-->" << currentState;
QPointer<QCocoaWindow> destructionGuard = this;
QWindowSystemInterface::handleWindowStateChanged<QWindowSystemInterface::SynchronousDelivery>(
window(), currentState, m_lastReportedWindowState);
if (!destructionGuard)
return;
m_lastReportedWindowState = currentState;
}
// ------------------------------------------------------------
void QCocoaWindow::setWindowTitle(const QString &title)
{
QMacAutoReleasePool pool;
if (!isContentView())
return;
m_view.window.title = title.toNSString();
if (title.isEmpty() && !window()->filePath().isEmpty()) {
// Clearing the title should restore the default filename
setWindowFilePath(window()->filePath());
}
}
void QCocoaWindow::setWindowFilePath(const QString &filePath)
{
QMacAutoReleasePool pool;
if (!isContentView())
return;
if (window()->title().isNull())
[m_view.window setTitleWithRepresentedFilename:filePath.toNSString()];
else
m_view.window.representedFilename = filePath.toNSString();
// Changing the file path may affect icon visibility
setWindowIcon(window()->icon());
}
void QCocoaWindow::setWindowIcon(const QIcon &icon)
{
QMacAutoReleasePool pool;
if (!isContentView())
return;
NSButton *iconButton = [m_view.window standardWindowButton:NSWindowDocumentIconButton];
if (!iconButton) {
// Window icons are only supported on macOS in combination with a document filePath
return;
}
if (icon.isNull()) {
iconButton.image = [NSWorkspace.sharedWorkspace iconForFile:m_view.window.representedFilename];
} else {
// Fall back to a size that looks good on the highest resolution screen available
// for icon engines that don't have an intrinsic size (like SVG).
auto fallbackSize = QSizeF::fromCGSize(iconButton.frame.size) * qGuiApp->devicePixelRatio();
iconButton.image = [NSImage imageFromQIcon:icon withSize:fallbackSize.toSize()];
}
}
void QCocoaWindow::setAlertState(bool enabled)
{
if (m_alertRequest == NoAlertRequest && enabled) {
m_alertRequest = [NSApp requestUserAttention:NSCriticalRequest];
} else if (m_alertRequest != NoAlertRequest && !enabled) {
[NSApp cancelUserAttentionRequest:m_alertRequest];
m_alertRequest = NoAlertRequest;
}
}
bool QCocoaWindow::isAlertState() const
{
return m_alertRequest != NoAlertRequest;
}
void QCocoaWindow::raise()
{
qCDebug(lcQpaWindow) << "QCocoaWindow::raise" << window();
// ### handle spaces (see Qt 4 raise_sys in qwidget_mac.mm)
if (isContentView()) {
if (m_view.window.visible) {
{
// Clean up auto-released temp objects from orderFront immediately.
// Failure to do so has been observed to cause leaks also beyond any outer
// autorelease pool (for example around a complete QWindow
// construct-show-raise-hide-delete cycle), counter to expected autoreleasepool
// behavior.
QMacAutoReleasePool pool;
[m_view.window orderFront:m_view.window];
}
static bool raiseProcess = qt_mac_resolveOption(true, "QT_MAC_SET_RAISE_PROCESS");
if (raiseProcess)
[NSApp activateIgnoringOtherApps:YES];
}
} else {
[m_view.superview addSubview:m_view positioned:NSWindowAbove relativeTo:nil];
}
}
void QCocoaWindow::lower()
{
qCDebug(lcQpaWindow) << "QCocoaWindow::lower" << window();
if (isContentView()) {
if (m_view.window.visible)
[m_view.window orderBack:m_view.window];
} else {
[m_view.superview addSubview:m_view positioned:NSWindowBelow relativeTo:nil];
}
}
bool QCocoaWindow::isExposed() const
{
return !m_exposedRect.isEmpty();
}
/*!
Returns \c true if the window is a child of a non-Qt window.
A embedded window has no parent platform window as reflected
though parent(), but has a native parent handle.
*/
bool QCocoaWindow::isEmbedded() const
{
// We compute this in viewDidMoveToSuperview, so that we don't
// report inconsistent states during reparenting, where the
// QPlatformWindow and QWindow parent() is not in sync with
// the view's superview.
return m_isEmbedded;
}
bool QCocoaWindow::isOpaque() const
{
// OpenGL surfaces can be ordered either above(default) or below the NSWindow.
// When ordering below the window must be tranclucent.
static GLint openglSourfaceOrder = qt_mac_resolveOption(1, "QT_MAC_OPENGL_SURFACE_ORDER");
bool translucent = window()->format().alphaBufferSize() > 0
|| window()->opacity() < 1
|| !window()->mask().isEmpty()
|| (surface()->supportsOpenGL() && openglSourfaceOrder == -1);
return !translucent;
}
void QCocoaWindow::propagateSizeHints()
{
QMacAutoReleasePool pool;
if (!isContentView())
return;
qCDebug(lcQpaWindow) << "QCocoaWindow::propagateSizeHints" << window()
<< "min:" << windowMinimumSize() << "max:" << windowMaximumSize()
<< "increment:" << windowSizeIncrement()
<< "base:" << windowBaseSize();
const NSWindow *window = m_view.window;
// Set the minimum content size.
QSize minimumSize = windowMinimumSize();
if (!minimumSize.isValid()) // minimumSize is (-1, -1) when not set. Make that (0, 0) for Cocoa.
minimumSize = QSize(0, 0);
window.contentMinSize = NSSizeFromCGSize(minimumSize.toCGSize());
// Set the maximum content size.
window.contentMaxSize = NSSizeFromCGSize(windowMaximumSize().toCGSize());
// The window may end up with a fixed size; in this case the zoom button should be disabled.
updateTitleBarButtons(this->window()->flags());
// sizeIncrement is observed to take values of (-1, -1) and (0, 0) for windows that should be
// resizable and that have no specific size increment set. Cocoa expects (1.0, 1.0) in this case.
QSize sizeIncrement = windowSizeIncrement();
if (sizeIncrement.isEmpty())
sizeIncrement = QSize(1, 1);
window.resizeIncrements = NSSizeFromCGSize(sizeIncrement.toCGSize());
QRect rect = geometry();
QSize baseSize = windowBaseSize();
if (!baseSize.isNull() && baseSize.isValid())
[window setFrame:NSMakeRect(rect.x(), rect.y(), baseSize.width(), baseSize.height()) display:YES];
}
void QCocoaWindow::setOpacity(qreal level)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::setOpacity" << level;
if (!isContentView())
return;
m_view.window.alphaValue = level;
}
void QCocoaWindow::setMask(const QRegion &region)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::setMask" << window() << region;
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;
}
}
bool QCocoaWindow::setKeyboardGrabEnabled(bool)
{
return false; // FIXME (QTBUG-106597)
}
bool QCocoaWindow::setMouseGrabEnabled(bool)
{
return false; // FIXME (QTBUG-106597)
}
WId QCocoaWindow::winId() const
{
return WId(m_view);
}
void QCocoaWindow::setParent(const QPlatformWindow *parentWindow)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::setParent" << window() << (parentWindow ? parentWindow->window() : 0);
#if QT_CONFIG(accessibility)
// The accessibility parent we push onto a foreign view is resolved when a
// client reaches the view, and a client reaches it through the window
// container that hosts the window. A window that changes parent is on its
// way out of its container, so let go of the parent we pushed rather than
// leave the view pointing into a hierarchy that no longer holds it.
if (isForeignWindow())
clearAccessibleParent();
#endif
// Recreate in case we need to get rid of a NSWindow, or create one
recreateWindowIfNeeded();
setGeometry(geometry(), QWindowPrivate::WindowFrameExclusive);
}
NSView *QCocoaWindow::view() const
{
return m_view;
}
NSWindow *QCocoaWindow::nativeWindow() const
{
return m_view.window;
}
void QCocoaWindow::updateEmbeddedState()
{
const bool wasEmbedded = m_isEmbedded;
m_isEmbedded = [&]{
if (QPlatformWindow::parent())
return false;
if (!m_view.superview)
return false;
// In a top level window the view will still have the window's theme
// frame as their superview, which does not count as being embedded.
auto *qtWindowController = qt_objc_cast<QNSWindowController*>(m_view.window.windowController);
if (m_view == qtWindowController.window.contentView)
return false;
return true;
}();
if (m_isEmbedded != wasEmbedded) {
qCDebug(lcQpaWindow) << "Updated embedded state from"
<< wasEmbedded << "to" << m_isEmbedded;
}
}
// ----------------------- NSView notifications -----------------------
void QCocoaWindow::viewDidChangeFrame()
{
// Note: When the view is the content view, it would seem redundant
// to deliver geometry changes both from windowDidResize and this
// callback, but in some cases such as when macOS native tabbed
// windows are enabled we may end up with the wrong geometry in
// the initial windowDidResize callback when a new tab is created.
handleGeometryChange();
}
/*!
Callback for NSViewGlobalFrameDidChangeNotification.
Posted whenever an NSView object that has attached surfaces (that is,
NSOpenGLContext objects) moves to a different screen, or other cases
where the NSOpenGLContext object needs to be updated.
*/
void QCocoaWindow::viewDidChangeGlobalFrame()
{
[m_view setNeedsDisplay:YES];
}
/*!
Notification that the view has moved to a different superview.
Unlike [NSView viewDidMoveToSuperview] this callback happens
after the view's new window has been resolved.
*/
void QCocoaWindow::viewDidMoveToSuperview(NSView *previousSuperview)
{
qCDebug(lcQpaWindow) << "Done re-parenting" << m_view
<< "from" << previousSuperview << "into" << m_view.superview;
// Update embedded state now that we have a consistent state
updateEmbeddedState();
if (isEmbedded()) {
// FIXME: Align this with logic in QCocoaWindow::setParent
handleGeometryChange();
if (m_view.superview)
[m_view setNeedsDisplay:YES];
}
// The default coordinate system of NSViews is with the origin in the bottom
// left corner (also known as non-flipped). Qt's coordinate system on the other
// hand has the origin in the top left corner (flipped, in Cocoa terms). When
// we're parented into a non-flipped NSView (such as for foreign window parents),
// the position we set on our view in setCocoaGeometry will only accurately
// represent the QWindow position as long as the superview doesn't change
// its size. To ensure a stable y position (following the Qt semantics),
// we explicitly set an auto resizing mask, unless one is already set.
if (m_view.superview && !m_view.superview.flipped && !isContentView()) {
if (m_view.autoresizingMask == NSViewNotSizable) {
qCDebug(lcQpaWindow) << "Setting auto resizing mask on" << m_view
<< "in non-flipped superview to maintain stable y-positioning";
setGeometry(geometry(), QWindowPrivate::WindowFrameExclusive);
m_view.autoresizingMask = NSViewMinYMargin;
}
} else if (previousSuperview && !previousSuperview.flipped
&& m_view.autoresizingMask == NSViewMinYMargin) {
// Reset back to default. This assumes someone didn't
// actively set NSViewMinYMargin and want it to stay
// that way. In that rare case, they can re-apply the
// auto resizing mask after reparenting.
qCDebug(lcQpaWindow) << "Clearing auto resizing mask on" << m_view
<< "as explicit stable y-positioning is no longer needed";
m_view.autoresizingMask = NSViewNotSizable;
}
}
/*!
Notification that the view has moved to a different window.
The viewDidMoveToSuperview callback comes in before this one.
*/
void QCocoaWindow::viewDidMoveToWindow(NSWindow *previousWindow)
{
qCDebug(lcQpaWindow) << "Done moving" << m_view
<< "from" << previousWindow << "to" << m_view.window;
if (auto *qtWindowController = qt_objc_cast<QNSWindowController*>(m_view.window.windowController)) {
qCDebug(lcQpaWindow) << "Retaining new window controller" << qtWindowController;
[qtWindowController retain];
}
if (auto *qtWindowController = qt_objc_cast<QNSWindowController*>(previousWindow.windowController)) {
qCDebug(lcQpaWindow) << "Releasing old window controller" << qtWindowController;
[qtWindowController autorelease];
}
// Moving to a new window might result in a new screen. This is normally
// handled for top level windows via windowDidChangeScreen, but for child
// windows we need to handle it manually.
auto *previousScreen = previousWindow ? QCocoaScreen::get(previousWindow.screen) : nullptr;
auto *currentScreen = m_view.window ? QCocoaScreen::get(m_view.window.screen) : nullptr;
if (currentScreen && currentScreen != previousScreen)
windowDidChangeScreen();
}
// ----------------------- NSWindow notifications -----------------------
// Note: The following notifications are delivered to every QCocoaWindow
// that is a child of the NSWindow that triggered the notification. Each
// callback should make sure to filter out notifications if they do not
// apply to that QCocoaWindow, e.g. if the window is not a content view.
void QCocoaWindow::windowDidMove()
{
if (!isContentView())
return;
handleGeometryChange();
// Moving a window might bring it out of maximized state
handleWindowStateChanged();
}
void QCocoaWindow::windowDidResize()
{
if (!isContentView())
return;
handleGeometryChange();
if (!m_view.inLiveResize)
handleWindowStateChanged();
}
void QCocoaWindow::windowWillStartLiveResize()
{
// Track live resizing for all windows, including
// child windows, so we know if it's safe to update
// the window unthrottled outside of the main thread.
m_inLiveResize = true;
}
bool QCocoaWindow::allowsIndependentThreadedRendering() const
{
// Use member variable to track this instead of reflecting
// NSView.inLiveResize directly, so it can be called from
// non-main threads.
return !m_inLiveResize;
}
void QCocoaWindow::windowDidEndLiveResize()
{
m_inLiveResize = false;
if (!isContentView())
return;
handleWindowStateChanged();
}
void QCocoaWindow::windowDidBecomeKey()
{
// The NSWindow we're part of become key. Check if we're the first
// responder, and if so, deliver focus window change to our window.
if (m_view.window.firstResponder != m_view)
return;
qCDebug(lcQpaWindow) << m_view.window << "became key window."
<< "Updating focus window to" << this << "with view" << m_view;
if (windowIsPopupType()) {
qCDebug(lcQpaWindow) << "Window is popup. Skipping focus window change.";
return;
}
// See also [QNSView becomeFirstResponder]
QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
window(), Qt::ActiveWindowFocusReason);
}
void QCocoaWindow::windowDidResignKey()
{
// The NSWindow we're part of lost key. Check if we're the first
// responder, and if so, deliver window deactivation to our window.
if (m_view.window.firstResponder != m_view)
return;
qCDebug(lcQpaWindow) << m_view.window << "resigned key window."
<< "Clearing focus window" << this << "with view" << m_view;
// Make sure popups are closed before we deliver activation changes, which are
// otherwise ignored by QApplication.
closeAllPopups();
// The current key window will be non-nil if another window became key. If that
// window is a Qt window, we delay the window activation event until the didBecomeKey
// notification is delivered to the active window, to ensure an atomic update.
NSWindow *newKeyWindow = [NSApp keyWindow];
if (newKeyWindow && newKeyWindow != m_view.window
&& [newKeyWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) {
qCDebug(lcQpaWindow) << "New key window" << newKeyWindow
<< "is Qt window. Deferring focus window change.";
return;
}
// Lost key window, go ahead and set the active window to zero
if (!windowIsPopupType()) {
QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
nullptr, Qt::ActiveWindowFocusReason);
}
}
void QCocoaWindow::windowDidOrderOnScreen()
{
// The current mouse window needs to get a leave event when a popup window opens.
// For modal dialogs, QGuiApplicationPrivate::showModalWindow takes care of this.
if (QWindowPrivate::get(window())->isPopup()) {
QWindowSystemInterface::handleLeaveEvent<QWindowSystemInterface::SynchronousDelivery>
(QGuiApplicationPrivate::currentMouseWindow);
}
[m_view setNeedsDisplay:YES];
}
void QCocoaWindow::windowDidOrderOffScreen()
{
handleExposeEvent(QRegion());
// We are closing a window, so the window that is now under the mouse
// might need to get an Enter event if it isn't already the mouse window.
if (window()->type() & Qt::Window) {
const QPointF screenPoint = QCocoaScreen::mapFromNative([NSEvent mouseLocation]);
if (QWindow *windowUnderMouse = QGuiApplication::topLevelAt(screenPoint.toPoint())) {
if (windowUnderMouse != QGuiApplicationPrivate::instance()->currentMouseWindow) {
const auto windowPoint = windowUnderMouse->mapFromGlobal(screenPoint);
// asynchronous delivery on purpose
QWindowSystemInterface::handleEnterEvent<QWindowSystemInterface::AsynchronousDelivery>
(windowUnderMouse, windowPoint, screenPoint);
}
}
}
}
void QCocoaWindow::windowDidChangeOcclusionState()
{
// Note, we don't take the view's hiddenOrHasHiddenAncestor state into
// account here, but instead leave that up to handleExposeEvent, just
// like all the other signals that could potentially change the exposed
// state of the window.
bool visible = m_view.window.occlusionState & NSWindowOcclusionStateVisible;
qCDebug(lcQpaWindow) << "Occlusion state of" << m_view.window << "for"
<< window() << "changed to" << (visible ? "visible" : "occluded");
if (visible)
[m_view setNeedsDisplay:YES];
else
handleExposeEvent(QRegion());
}
void QCocoaWindow::windowDidChangeScreen()
{
if (!window())
return;
// Note: When a window is resized to 0x0 Cocoa will report the window's screen as nil
NSScreen *nsScreen = m_view.window.screen;
qCDebug(lcQpaWindow) << window() << "did change" << nsScreen;
QCocoaScreen::updateScreens();
auto *previousScreen = static_cast<QCocoaScreen*>(screen());
auto *currentScreen = QCocoaScreen::get(nsScreen);
qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen;
// Note: The previous screen may be the same as the current screen, either because
// a) the screen was just reconfigured, which still results in AppKit sending an
// NSWindowDidChangeScreenNotification, b) because the previous screen was removed,
// and we ended up calling QWindow::setScreen to move the window, which doesn't
// actually move the window to the new screen, or c) because we've delivered the
// screen change to the top level window, which will make all the child windows
// of that window report the new screen when requested via QWindow::screen().
// We still need to deliver the screen change in all these cases, as the
// device-pixel ratio may have changed, and needs to be delivered to all
// windows, both top level and child windows.
QPointer<QCocoaWindow> destructionGuard = this;
QWindowSystemInterface::handleWindowScreenChanged<QWindowSystemInterface::SynchronousDelivery>(
window(), currentScreen ? currentScreen->screen() : nullptr);
if (!destructionGuard)
return;
if (currentScreen && hasPendingUpdateRequest()) {
// Restart display-link on new screen. We need to do this unconditionally,
// since we can't rely on the previousScreen reflecting whether or not the
// window actually moved from one screen to another, or just stayed on the
// same screen.
currentScreen->requestUpdate();
}
// If there are no exposed windows left on the previous screen
// we can stop its display link if it was running.
if (previousScreen)
previousScreen->maybeStopDisplayLink();
}
// ----------------------- NSWindowDelegate callbacks -----------------------
bool QCocoaWindow::windowShouldClose()
{
qCDebug(lcQpaWindow) << "QCocoaWindow::windowShouldClose" << window();
// This callback should technically only determine if the window
// should (be allowed to) close, but since our QPA API to determine
// that also involves actually closing the window we do both at the
// same time, instead of doing the latter in windowWillClose.
// If the window is closed, we will release and deallocate the NSWindow.
// But frames higher up in the stack might still expect the window to
// be alive, since the windowShouldClose: callback is technically only
// supposed to answer YES or NO. To ensure the window is still alive
// we put an autorelease in the closest pool (typically the runloop).
[[m_view.window retain] autorelease];
return QWindowSystemInterface::handleCloseEvent<QWindowSystemInterface::SynchronousDelivery>(window());
}
// ----------------------------- QPA forwarding -----------------------------
void QCocoaWindow::handleGeometryChange()
{
const QRect newGeometry = actualGeometry();
qCDebug(lcQpaWindow) << "QCocoaWindow::handleGeometryChange" << window()
<< "current" << geometry() << "new" << newGeometry;
// It can happen that the current NSWindow is nil (if we are changing styleMask
// from/to borderless, and the content view is being re-parented), which results
// in invalid coordinates.
if (m_inSetStyleMask && !m_view.window) {
qCDebug(lcQpaWindow) << "Lacking window during style mask update, ignoring geometry change";
return;
}
// Prevent geometry change during initialization, as that will result
// in a resize event, and Qt expects those to come after the show event.
// FIXME: Remove once we've clarified the Qt behavior for this.
if (!m_initialized) {
// But update the QPlatformWindow reality
QPlatformWindow::setGeometry(newGeometry);
qCDebug(lcQpaWindow) << "Window still initializing, skipping event";
return;
}
QWindowSystemInterface::handleGeometryChange(window(), newGeometry);
// Changing the window geometry may affect the safe area margins
updateSafeAreaMarginsIfNeeded();
// Guard against processing window system events during QWindow::setGeometry
// calls, which Qt and Qt applications do not expect.
if (!m_inSetGeometry)
QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
}
void QCocoaWindow::handleExposeEvent(const QRegion &region)
{
// Ideally we'd implement isExposed() in terms of these properties,
// plus the occlusionState of the NSWindow, and let the expose event
// pull the exposed state out when needed. However, when the window
// is first shown we receive a drawRect call where the occlusionState
// of the window is still hidden, but we still want to prepare the
// window for display by issuing an expose event to Qt. To work around
// this we don't use the occlusionState directly, but instead base
// the exposed state on the region we get in, which in the case of
// a window being obscured is an empty region, and in the case of
// a drawRect call is a non-null region, even if occlusionState
// is still hidden. This ensures the window is prepared for display.
if (m_view.window.visible && m_view.window.screen
&& !geometry().size().isEmpty() && !region.isEmpty()
&& !m_view.hiddenOrHasHiddenAncestor) {
m_exposedRect = region.boundingRect();
} else {
m_exposedRect = QRect();
}
qCDebug(lcQpaDrawing) << "QCocoaWindow::handleExposeEvent" << window() << region << "isExposed" << isExposed();
QPointer<QCocoaWindow> destructionGuard = this;
QWindowSystemInterface::handleExposeEvent<QWindowSystemInterface::SynchronousDelivery>(window(), region);
if (!destructionGuard)
return;
if (!isExposed())
static_cast<QCocoaScreen *>(screen())->maybeStopDisplayLink();
}
// --------------------------------------------------------------------------
bool QCocoaWindow::windowIsPopupType(Qt::WindowType type) const
{
if (type == Qt::Widget)
type = window()->type();
if (type == Qt::Tool)
return false; // Qt::Tool has the Popup bit set but isn't, at least on Mac.
return ((type & Qt::Popup) == Qt::Popup);
}
/*!
Checks if the window is the content view of its immediate NSWindow.
Being the content view of a NSWindow means the QWindow is
the highest accessible NSView object in the window's view
hierarchy.
This is the case if the QWindow is a top level window.
*/
bool QCocoaWindow::isContentView() const
{
return m_view.window.contentView == m_view;
}
/*!
Recreates (or removes) the NSWindow for this QWindow, if needed.
A QWindow may need a corresponding NSWindow/NSPanel, depending on
whether or not it's a top level or not, window flags, etc.
\sa windowClass
*/
void QCocoaWindow::recreateWindowIfNeeded()
{
QMacAutoReleasePool pool;
auto *parentWindow = static_cast<QCocoaWindow *>(QPlatformWindow::parent());
const bool shouldManageTopLevelWindow = !parentWindow
&& !isSubWindow(window()) && !isForeignWindow();
if (shouldManageTopLevelWindow) {
auto *windowController = qt_objc_cast<QNSWindowController*>(m_view.window.windowController);
const bool isControllerContentView = m_view == windowController.window.contentView;
const bool windowClassIsCorrect = [m_view.window isKindOfClass:windowClass()];
if (!isControllerContentView || !windowClassIsCorrect) {
qCDebug(lcQpaWindow) << "Creating new window controller for" << m_view;
windowController = [[QNSWindowController alloc] initWithCocoaWindow:this];
m_view.postsFrameChangedNotifications = NO;
windowController.window.contentView = m_view;
m_view.postsFrameChangedNotifications = YES;
// The view now owns the window controller
[windowController release];
} else {
qCDebug(lcQpaWindow) << "Re-using existing window controller" << windowController;
}
} else if (parentWindow) {
if (m_view.superview != parentWindow->view()) {
qCDebug(lcQpaWindow) << "Reparenting view to new parent" << parentWindow;
[parentWindow->view() addSubview:m_view];
}
} else if (qnsview_cast(m_view.superview) && !isEmbedded()) {
qCDebug(lcQpaWindow) << "Removing view from superview" << m_view.superview;
[m_view removeFromSuperview];
}
}
Class QCocoaWindow::windowClass() const
{
const auto type = window()->type();
return ((type & Qt::Popup) == Qt::Popup
|| (type & Qt::Dialog) == Qt::Dialog) ?
QNSPanel.class : QNSWindow.class;
}
void QCocoaWindow::requestUpdate()
{
qCDebug(lcQpaDrawing) << "QCocoaWindow::requestUpdate" << window()
<< "using" << (updatesWithDisplayLink() ? "display-link" : "timer");
if (updatesWithDisplayLink()) {
if (!static_cast<QCocoaScreen *>(screen())->requestUpdate()) {
qCDebug(lcQpaDrawing) << "Falling back to timer-based update request";
QPlatformWindow::requestUpdate();
}
} else {
// Fall back to the un-throttled timer-based callback
QPlatformWindow::requestUpdate();
}
}
bool QCocoaWindow::updatesWithDisplayLink() const
{
// Update via CVDisplayLink if Vsync is enabled
return format().swapInterval() != 0;
}
void QCocoaWindow::deliverUpdateRequest()
{
qCDebug(lcQpaDrawing) << "Delivering update request to" << window();
QScopedValueRollback<bool> blocker(m_deliveringUpdateRequest, true);
if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(contentLayer())) {
// We attempt a read lock here, so that the animation/render thread is
// prioritized lower than the main thread's displayLayer processing.
// Without this the two threads might fight over the next drawable,
// starving the main thread's presentation of the resized layer.
if (!qtMetalLayer.displayLock.tryLockForRead()) {
qCDebug(lcQpaDrawing) << "Deferring update request"
<< "due to" << qtMetalLayer << "needing display";
return;
}
// But we don't hold the lock, as the update request can recurse
// back into setNeedsDisplay, which would deadlock.
qtMetalLayer.displayLock.unlock();
}
QPlatformWindow::deliverUpdateRequest();
}
void QCocoaWindow::requestActivateWindow()
{
QMacAutoReleasePool pool;
[m_view.window makeFirstResponder:m_view];
[m_view.window makeKeyWindow];
}
/*
Closes all popups, and removes observers and monitors.
*/
void QCocoaWindow::closeAllPopups()
{
QGuiApplicationPrivate::instance()->closeAllPopups();
removePopupMonitor();
}
void QCocoaWindow::removePopupMonitor()
{
if (s_globalMouseMonitor) {
[NSEvent removeMonitor:s_globalMouseMonitor];
s_globalMouseMonitor = nil;
}
if (s_applicationActivationObserver) {
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:s_applicationActivationObserver];
s_applicationActivationObserver = nil;
}
}
void QCocoaWindow::setupPopupMonitor()
{
// we open a popup window while we are not active. None of our existing event
// handlers will get called if the user now clicks anywhere outside the application
// or activates another window. Use a global event monitor to watch for mouse
// presses, and close popups. We also want mouse tracking in the popup to work, so
// also watch for MouseMoved.
if (!s_globalMouseMonitor) {
// we only get LeftMouseDown events when we also set LeftMouseUp.
constexpr NSEventMask mouseButtonMask = NSEventTypeLeftMouseDown | NSEventTypeLeftMouseUp
| NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown
| NSEventMaskMouseMoved;
s_globalMouseMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mouseButtonMask
handler:^(NSEvent *e){
if (!QGuiApplicationPrivate::instance()->activePopupWindow()) {
removePopupMonitor();
return;
}
const auto eventType = cocoaEvent2QtMouseEvent(e);
if (eventType == QEvent::MouseMove) {
if (s_windowUnderMouse) {
QWindow *window = s_windowUnderMouse->window();
const auto button = cocoaButton2QtButton(e);
const auto buttons = currentlyPressedMouseButtons();
const auto globalPoint = QCocoaScreen::mapFromNative(NSEvent.mouseLocation);
const auto localPoint = window->mapFromGlobal(globalPoint.toPoint());
QWindowSystemInterface::handleMouseEvent(window, localPoint, globalPoint,
buttons, button, eventType);
}
} else {
closeAllPopups();
}
}];
}
// The activation observer also gets called when we become active because the user clicks
// into the popup. This should not close the popup, so QCocoaApplicationDelegate's
// applicationDidBecomeActive implementation removes this observer.
if (!s_applicationActivationObserver) {
s_applicationActivationObserver = [[[NSWorkspace sharedWorkspace] notificationCenter]
addObserverForName:NSWorkspaceDidActivateApplicationNotification
object:nil queue:nil
usingBlock:^(NSNotification *){
closeAllPopups();
}];
}
}
QCocoaNSWindow *QCocoaWindow::createNSWindow()
{
QMacAutoReleasePool pool;
Qt::WindowType type = window()->type();
Qt::WindowFlags flags = window()->flags();
QRect rect = geometry();
QScreen *targetScreen = nullptr;
for (QScreen *screen : QGuiApplication::screens()) {
if (screen->handle()->geometry().contains(rect.topLeft())) {
targetScreen = screen;
break;
}
}
NSWindowStyleMask styleMask = windowStyleMask(flags);
if (!targetScreen) {
qCWarning(lcQpaWindow) << "Window position" << rect << "outside any known screen, using primary screen";
targetScreen = QGuiApplication::primaryScreen();
// Unless the window is created as borderless AppKit won't find a position and
// screen that's close to the requested invalid position, and will always place
// the window on the primary screen.
styleMask = NSWindowStyleMaskBorderless;
}
auto *targetCocoaScreen = static_cast<QCocoaScreen *>(targetScreen->handle());
rect.translate(-targetCocoaScreen->geometry().topLeft());
NSRect contentRect = QCocoaScreen::mapToNative(rect, targetCocoaScreen);
if (targetScreen->primaryOrientation() == Qt::PortraitOrientation) {
// The macOS window manager has a bug, where if a screen is rotated, it will not allow
// a window to be created within the area of the screen that has a Y coordinate (I quadrant)
// higher than the height of the screen in its non-rotated state (including a magic padding
// of 24 points), unless the window is created with the NSWindowStyleMaskBorderless style mask.
if (styleMask && (contentRect.origin.y + 24 > targetCocoaScreen->geometry().width())) {
qCDebug(lcQpaWindow) << "Window positioned on portrait screen."
<< "Adjusting style mask during creation";
styleMask = NSWindowStyleMaskBorderless;
}
}
// Create NSWindow
QCocoaNSWindow *nsWindow = [[windowClass() alloc] initWithContentRect:contentRect
// Mask will be updated in setWindowFlags if not the final mask
styleMask:styleMask
// Deferring window creation breaks OpenGL (the GL context is
// set up before the window is shown and needs a proper window)
backing:NSBackingStoreBuffered defer:NO
screen:targetCocoaScreen->nativeScreen()
platformWindow:this];
// The resulting screen can be different from the screen requested if
// for example the application has been assigned to a specific display.
auto resultingScreen = QCocoaScreen::get(nsWindow.screen);
// But may not always be resolved at this point, in which case we fall back
// to the target screen. The real screen will be delivered as a screen change
// when resolved as part of ordering the window on screen.
if (!resultingScreen)
resultingScreen = targetCocoaScreen;
if (resultingScreen->screen() != window()->screen()) {
QWindowSystemInterface::handleWindowScreenChanged<
QWindowSystemInterface::SynchronousDelivery>(window(), resultingScreen->screen());
}
static QSharedPointer<QNSWindowDelegate> sharedDelegate([[QNSWindowDelegate alloc] init],
[](QNSWindowDelegate *delegate) { [delegate release]; });
nsWindow.delegate = sharedDelegate.get();
// Prevent Cocoa from releasing the window on close. Qt
// handles the close event asynchronously and we want to
// make sure that NSWindow stays valid until the
// QCocoaWindow is deleted by Qt.
[nsWindow setReleasedWhenClosed:NO];
if (alwaysShowToolWindow()) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:[QNSWindow class] selector:@selector(applicationActivationChanged:)
name:NSApplicationWillResignActiveNotification object:nil];
[center addObserver:[QNSWindow class] selector:@selector(applicationActivationChanged:)
name:NSApplicationWillBecomeActiveNotification object:nil];
});
}
nsWindow.restorable = NO;
nsWindow.level = windowLevel(flags);
nsWindow.tabbingMode = NSWindowTabbingModeDisallowed;
if ([nsWindow isKindOfClass:NSPanel.class]) {
// Qt::Tool windows hide on app deactivation, unless Qt::WA_MacAlwaysShowToolWindow is set
nsWindow.hidesOnDeactivate = ((type & Qt::Tool) == Qt::Tool) && !alwaysShowToolWindow();
// Make popup windows show on the same desktop as the parent window
nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary
| NSWindowCollectionBehaviorMoveToActiveSpace;
if ((type & Qt::Popup) == Qt::Popup) {
nsWindow.hasShadow = YES;
nsWindow.animationBehavior = NSWindowAnimationBehaviorUtilityWindow;
if (QGuiApplication::applicationState() != Qt::ApplicationActive)
setupPopupMonitor();
}
}
// We propagate the view's color space granulary to both the IOSurfaces
// used for QSurface::RasterSurface, as well as the CAMetalLayer used for
// QSurface::MetalSurface, but for QSurface::OpenGLSurface we don't have
// that option as we use NSOpenGLContext instead of CAOpenGLLayer. As a
// workaround we set the NSWindow's color space, which affects GL drawing
// with NSOpenGLContext as well. This does not conflict with the granular
// modifications we do to each surface for raster or Metal.
if (auto *qtView = qnsview_cast(m_view))
nsWindow.colorSpace = qtView.colorSpace;
return nsWindow;
}
bool QCocoaWindow::alwaysShowToolWindow() const
{
return qt_mac_resolveOption(false, window(), "_q_macAlwaysShowToolWindow", "");
}
bool QCocoaWindow::setWindowModified(bool modified)
{
QMacAutoReleasePool pool;
if (!isContentView())
return false;
m_view.window.documentEdited = modified;
return true;
}
void QCocoaWindow::setMenubar(QCocoaMenuBar *mb)
{
m_menubar = mb;
}
QCocoaMenuBar *QCocoaWindow::menubar() const
{
return m_menubar;
}
void QCocoaWindow::setWindowCursor(NSCursor *cursor)
{
QMacAutoReleasePool pool;
// Setting a cursor in a foreign view is not supported
if (isForeignWindow())
return;
qCInfo(lcQpaMouse) << "Setting" << this << "cursor to" << cursor;
QNSView *view = qnsview_cast(m_view);
if (cursor == view.cursor)
return;
view.cursor = cursor;
// We're not using the the legacy cursor rects API to manage our
// cursor, but calling this function also invalidates AppKit's
// view of whether or not we need a cursorUpdate callback for
// our tracking area.
[m_view.window invalidateCursorRectsForView:m_view];
// We've informed AppKit that we need a cursorUpdate, but cursor
// updates for tracking areas are deferred in some cases, such as
// when the mouse is down, whereas we want a synchronous update.
// To ensure an updated cursor we synthesize a cursor update event
// now if the window is otherwise allowed to change the cursor.
auto locationInWindow = m_view.window.mouseLocationOutsideOfEventStream;
auto locationInSuperview = [m_view.superview convertPoint:locationInWindow fromView:nil];
bool mouseIsOverView = [m_view hitTest:locationInSuperview] == m_view;
auto utilityMask = NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskTitled;
bool isUtilityWindow = (m_view.window.styleMask & utilityMask) == utilityMask;
if (mouseIsOverView && (m_view.window.keyWindow || isUtilityWindow)) {
qCDebug(lcQpaMouse) << "Synthesizing cursor update";
[m_view cursorUpdate:[NSEvent enterExitEventWithType:NSEventTypeCursorUpdate
location:locationInWindow modifierFlags:0 timestamp:0
windowNumber:m_view.window.windowNumber context:nil
eventNumber:0 trackingNumber:0 userData:0]];
}
}
/*!
Register if a window should generate trackpad touch events.
Enabling trackpad touch events has implications for delivery
of other events, for example by causing scrolling event lag.
The registration is ref-counted: multiple clients can enable
touch events, which then will be delivered until all clients
deregister.
*/
void QCocoaWindow::enableTrackpadTouchDelivery(bool enable)
{
m_registeredTrackpadTouchConsumers += enable ? 1 : -1;
updateAllowedTouchTypes();
}
void QCocoaWindow::updateAllowedTouchTypes()
{
bool allowingTrackpadTouch = m_view.allowedTouchTypes & NSTouchTypeMaskIndirect;
if (m_registeredTrackpadTouchConsumers > 0 && !allowingTrackpadTouch) {
qCDebug(lcQpaTouch) << "Enabling trackpad touches for" << m_view;
m_view.allowedTouchTypes |= NSTouchTypeMaskIndirect;
} else if (!m_registeredTrackpadTouchConsumers && allowingTrackpadTouch) {
const auto devices = QInputDevice::devices();
for (const auto *device : devices) {
if (device->type() != QInputDevice::DeviceType::TouchPad)
continue;
const auto *pointingDevice = static_cast<const QPointingDevice *>(device);
const auto *devicePriv = QPointingDevicePrivate::get(pointingDevice);
for (const auto &[pointId, point] : std::as_const(devicePriv->activePoints)) {
if (QMutableEventPoint::window(point.eventPoint) == window()) {
qCDebug(lcQpaTouch) << "Deferring allowed touch type update for"
<< m_view << "due to still active" << point.eventPoint;
return;
}
}
}
qCDebug(lcQpaTouch) << "Disabling trackpad touches for" << m_view;
m_view.allowedTouchTypes &= ~NSTouchTypeMaskIndirect;
}
}
qreal QCocoaWindow::devicePixelRatio() const
{
// The documented way to observe the relationship between device-independent
// and device pixels is to use one for the convertToBacking functions. Other
// methods such as [NSWindow backingScaleFactor] might not give the correct
// result, for example if setWantsBestResolutionOpenGLSurface is not set or
// or ignored by the OpenGL driver.
NSSize backingSize = [m_view convertSizeToBacking:NSMakeSize(1.0, 1.0)];
return backingSize.height;
}
QWindow *QCocoaWindow::childWindowAt(QPoint windowPoint)
{
QWindow *targetWindow = window();
for (QObject *child : targetWindow->children())
if (QWindow *childWindow = qobject_cast<QWindow *>(child))
if (QPlatformWindow *handle = childWindow->handle())
if (handle->isExposed() && childWindow->geometry().contains(windowPoint))
targetWindow = static_cast<QCocoaWindow*>(handle)->childWindowAt(windowPoint - childWindow->position());
return targetWindow;
}
bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder()
{
// This function speaks up if there's any reason
// to refuse key window or first responder state.
if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput))
return true;
// For application modal windows, as well as direct parent windows
// of window modal windows, AppKit takes care of blocking interaction.
// The Qt expectation however, is that all transient parents of a
// window modal window is blocked, as reflected by QGuiApplication.
// We reflect this by returning false from this function for transient
// parents blocked by a modal window, but limit it to the cases not
// covered by AppKit to avoid potential unwanted side effects.
QWindow *modalWindow = nullptr;
if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) {
if (modalWindow->modality() == Qt::WindowModal && modalWindow->transientParent() != window()) {
qCDebug(lcQpaWindow) << "Refusing key window for" << this << "due to being"
<< "blocked by" << modalWindow;
return true;
}
}
if (m_inSetVisible) {
QVariant showWithoutActivating = window()->property("_q_showWithoutActivating");
if (showWithoutActivating.isValid() && showWithoutActivating.toBool())
return true;
}
return false;
}
bool QCocoaWindow::windowEvent(QEvent *event)
{
switch (event->type()) {
case QEvent::WindowBlocked:
case QEvent::WindowUnblocked:
updateTitleBarButtons(window()->flags());
break;
default:
break;
}
return QPlatformWindow::windowEvent(event);
}
QPoint QCocoaWindow::bottomLeftClippedByNSWindowOffset() const
{
if (!m_view)
return QPoint();
const NSPoint origin = [m_view isFlipped] ? NSMakePoint(0, [m_view frame].size.height)
: NSMakePoint(0, 0);
const NSRect visibleRect = [m_view visibleRect];
return QPoint(visibleRect.origin.x, -visibleRect.origin.y + (origin.y - visibleRect.size.height));
}
QMargins QCocoaWindow::frameMargins() const
{
QMacAutoReleasePool pool;
// Child windows don't have frame margins. We explicitly don't check
// isContentView() here, as we want to know the frame argins also for
// windows that haven't gotten their NSWindow yet.
if (QPlatformWindow::parent())
return QMargins();
NSRect frameRect;
NSRect contentRect;
if (m_view.window) {
frameRect = m_view.window.frame;
contentRect = [m_view.window contentRectForFrameRect:frameRect];
} else {
contentRect = m_view.frame;
frameRect = [NSWindow frameRectForContentRect:contentRect styleMask:windowStyleMask(window()->flags())];
}
return QMargins(frameRect.origin.x - contentRect.origin.x,
(frameRect.origin.y + frameRect.size.height) - (contentRect.origin.y + contentRect.size.height),
(frameRect.origin.x + frameRect.size.width) - (contentRect.origin.x + contentRect.size.width),
contentRect.origin.y - contentRect.origin.y);
}
void QCocoaWindow::setFrameStrutEventsEnabled(bool enabled)
{
m_frameStrutEventsEnabled = enabled;
}
QPoint QCocoaWindow::mapToGlobal(const QPoint &point) const
{
NSPoint windowPoint = [m_view convertPoint:point.toCGPoint() toView:nil];
NSPoint screenPoint = [m_view.window convertPointToScreen:windowPoint];
return QCocoaScreen::mapFromNative(screenPoint).toPoint();
}
QPoint QCocoaWindow::mapFromGlobal(const QPoint &point) const
{
NSPoint screenPoint = QCocoaScreen::mapToNative(point);
NSPoint windowPoint = [m_view.window convertPointFromScreen:screenPoint];
return QPointF::fromCGPoint([m_view convertPoint:windowPoint fromView:nil]).toPoint();
}
CGPoint QCocoaWindow::mapToNative(const QPointF &point, NSView *referenceView)
{
if (!referenceView || referenceView.flipped)
return point.toCGPoint();
else
return qt_mac_flip(point, QRectF::fromCGRect(referenceView.bounds)).toCGPoint();
}
CGRect QCocoaWindow::mapToNative(const QRectF &rect, NSView *referenceView)
{
if (!referenceView || referenceView.flipped)
return rect.toCGRect();
else
return qt_mac_flip(rect, QRectF::fromCGRect(referenceView.bounds)).toCGRect();
}
QPointF QCocoaWindow::mapFromNative(CGPoint point, NSView *referenceView)
{
if (!referenceView || referenceView.flipped)
return QPointF::fromCGPoint(point);
else
return qt_mac_flip(QPointF::fromCGPoint(point), QRectF::fromCGRect(referenceView.bounds));
}
QRectF QCocoaWindow::mapFromNative(CGRect rect, NSView *referenceView)
{
if (!referenceView || referenceView.flipped)
return QRectF::fromCGRect(rect);
else
return qt_mac_flip(QRectF::fromCGRect(rect), QRectF::fromCGRect(referenceView.bounds));
}
CALayer *QCocoaWindow::contentLayer() const
{
auto *layer = m_view.layer;
if (auto *containerLayer = qt_objc_cast<QContainerLayer*>(layer))
layer = containerLayer.contentLayer;
return layer;
}
void QCocoaWindow::manageVisualEffectArea(quintptr identifier, const QRect &rect,
NSVisualEffectMaterial material, NSVisualEffectBlendingMode blendMode,
NSVisualEffectState activationState)
{
if (!qt_objc_cast<QContainerLayer*>(m_view.layer)) {
qCWarning(lcQpaWindow) << "Can not manage visual effect areas"
<< "in views without a container layer";
return;
}
qCDebug(lcQpaWindow) << "Updating visual effect area" << identifier
<< "to" << rect << "with material" << material << "blend mode"
<< blendMode << "and activation state" << activationState;
NSVisualEffectView *effectView = nullptr;
if (m_effectViews.contains(identifier)) {
effectView = m_effectViews.value(identifier);
if (rect.isEmpty()) {
[effectView removeFromSuperview];
m_effectViews.remove(identifier);
return;
}
} else if (!rect.isEmpty()) {
effectView = [NSVisualEffectView new];
// Ensure that the visual effect layer is stacked well
// below our content layer (which defaults to a z of 0).
effectView.wantsLayer = YES;
effectView.layer.zPosition = -FLT_MAX;
[m_view addSubview:effectView];
m_effectViews.insert(identifier, effectView);
}
if (!effectView)
return;
effectView.frame = rect.toCGRect();
effectView.material = material;
effectView.blendingMode = blendMode;
effectView.state = activationState;
}
#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug debug, const QCocoaWindow *window)
{
QDebugStateSaver saver(debug);
debug.nospace();
debug << "QCocoaWindow(" << (const void *)window;
if (window)
debug << ", window=" << window->window();
debug << ')';
return debug;
}
#endif // !QT_NO_DEBUG_STREAM
// --------------------------------------------------------------------------
#if QT_CONFIG(accessibility)
static void setAccessibilityParent(NSView *view, id accessibilityParent)
{
// The elements that show up in the parent's child list are the view itself
// if the view is unignored, and the view's own accessible children if it is
// not, so we set the parent on that same set, for the child and parent
// relations to match.
for (id<NSAccessibility> element in NSAccessibilityUnignoredChildrenForOnlyChild(view)) {
// We can't assume all elements implement this part of the NSAccessibility protocol
if (![element respondsToSelector:@selector(setAccessibilityParent:)])
continue;
if (element.accessibilityParent == accessibilityParent)
continue;
qCDebug(lcQpaAccessibility) << "Setting a11y parent of" << element
<< "for" << view << "to" << accessibilityParent;
element.accessibilityParent = accessibilityParent;
}
}
/*
Reflects the window's accessibility parent onto a foreign view.
Our own views resolve the parent on demand via [QNSView accessibilityParent],
but a foreign view has no getter of ours to run, so the parent has to be
pushed onto it. The parent we push is the unignored ancestor of the window
container hosting the window, and it changes whenever anything above the
container moves in the hierarchy, or stops being ignored.
Rather than tracking those changes we resolve the parent here, called from
QCocoaAccessible::viewFor() on each of the paths that hand a foreign view to
an accessibility client, so that the pushed parent is resolved as late as
possible.
*/
void QCocoaWindow::updateAccessibleParent()
{
if (!isForeignWindow())
return;
QObject *accessibleParent = QWindowPrivate::get(window())->accessibleParent;
if (QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(accessibleParent)) {
setAccessibilityParent(m_view, NSAccessibilityUnignoredAncestor(
[QMacAccessibilityElement elementWithInterface:iface]));
m_didOverrideAccessibleParent = true;
} else {
clearAccessibleParent();
}
}
void QCocoaWindow::clearAccessibleParent()
{
if (!isForeignWindow() || !m_didOverrideAccessibleParent)
return;
// AppKit has no way to remove an accessibility attribute override, and
// stores nil as an explicit "no parent", so the view can not be handed back
// the parent it resolves by default. By the time we get here the view has
// normally left the view hierarchy of its former accessible parent, so our
// best option is to reflect an unknown accessible parent and let the new
// parent update it if needed.
setAccessibilityParent(m_view, nil);
m_didOverrideAccessibleParent = false;
}
#endif // QT_CONFIG(accessibility)
// --------------------------------------------------------------------------
QT_END_NAMESPACE
#include "moc_qcocoawindow.cpp"