summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/cocoa/qcocoawindow.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoawindow.mm')
-rw-r--r--src/plugins/platforms/cocoa/qcocoawindow.mm246
1 files changed, 207 insertions, 39 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm
index fc896d65f8..b4c8ae4ba1 100644
--- a/src/plugins/platforms/cocoa/qcocoawindow.mm
+++ b/src/plugins/platforms/cocoa/qcocoawindow.mm
@@ -24,6 +24,7 @@
#include <qpa/qplatformscreen.h>
#include <QtGui/private/qcoregraphics_p.h>
#include <QtGui/private/qhighdpiscaling_p.h>
+#include <QtGui/private/qmetallayer_p.h>
#include <QDebug>
@@ -150,7 +151,10 @@ QCocoaWindow::~QCocoaWindow()
QMacAutoReleasePool pool;
[m_nsWindow makeFirstResponder:nil];
[m_nsWindow setContentView:nil];
- if ([m_view superview])
+
+ // Remove from superview only if we have a Qt window parent,
+ // as we don't want to affect window container foreign windows.
+ if (QPlatformWindow::parent())
[m_view removeFromSuperview];
// Make sure to disconnect observer in all case if view is valid
@@ -174,8 +178,7 @@ QCocoaWindow::~QCocoaWindow()
object:m_view];
[m_view release];
- [m_nsWindow close];
- [m_nsWindow release];
+ [m_nsWindow closeAndRelease];
}
QSurfaceFormat QCocoaWindow::format() const
@@ -283,6 +286,54 @@ void QCocoaWindow::setCocoaGeometry(const QRect &rect)
// will call QPlatformWindow::setGeometry(rect) during resize confirmation (see qnsview.mm)
}
+QMargins QCocoaWindow::safeAreaMargins() const
+{
+ // 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 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 = {
+ screenInsets.left ?
+ qMax(0.0f, screenInsets.left - screenRelativeViewBounds.left())
+ : 0.0f,
+ screenInsets.top ?
+ qMax(0.0f, screenInsets.top - screenRelativeViewBounds.top())
+ : 0.0f,
+ screenInsets.right ?
+ qMax(0.0f, screenInsets.right
+ - (screenRect.size.width - screenRelativeViewBounds.right()))
+ : 0.0f,
+ screenInsets.bottom ?
+ qMax(0.0f, screenInsets.bottom
+ - (screenRect.size.height - screenRelativeViewBounds.bottom()))
+ : 0.0f
+ };
+
+ return (screenSafeAreaMargins | viewSafeAreaMargins).toMargins();
+}
+
bool QCocoaWindow::startSystemMove()
{
switch (NSApp.currentEvent.type) {
@@ -306,6 +357,31 @@ void QCocoaWindow::setVisible(bool visible)
{
qCDebug(lcQpaWindow) << "QCocoaWindow::setVisible" << window() << visible;
+ // Our implementation of setVisible below is not idempotent, as for
+ // modal windows it calls beginSheet/endSheet or starts/ends modal
+ // sessions. However we can't simply guard for m_view.hidden already
+ // having the right state, as the behavior of this function differs
+ // based on whether the window has been initialized or not, as
+ // handleGeometryChange will bail out if the window is still
+ // initializing. Since we know we'll get a second setVisible
+ // call after creation, we can check for that case specifically,
+ // which means we can then safely guard on m_view.hidden changing.
+
+ if (!m_initialized) {
+ qCDebug(lcQpaWindow) << "Window still initializing, skipping setting visibility";
+ return; // We'll get another setVisible call after create is done
+ }
+
+ if (visible == !m_view.hidden && (!isContentView() || visible == m_view.window.visible)) {
+ qCDebug(lcQpaWindow) << "No change in visible status. Ignoring.";
+ return;
+ }
+
+ if (m_inSetVisible) {
+ qCWarning(lcQpaWindow) << "Already setting window visible!";
+ return;
+ }
+
QScopedValueRollback<bool> rollback(m_inSetVisible, true);
QMacAutoReleasePool pool;
@@ -414,6 +490,24 @@ void QCocoaWindow::setVisible(bool visible)
}
}
+ // 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) {
@@ -469,7 +563,7 @@ NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags)
auto *nsWindow = transientCocoaWindow->nativeWindow();
// We only upgrade the window level for "special" windows, to work
- // around Qt Designer parenting the designer windows to the widget
+ // around Qt Widgets Designer parenting the designer windows to the widget
// palette window (QTBUG-31779). This should be fixed in designer.
if (type != Qt::Window)
windowLevel = qMax(windowLevel, nsWindow.level);
@@ -1191,8 +1285,26 @@ void QCocoaWindow::windowDidResize()
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::inLiveResize() 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;
@@ -1201,32 +1313,33 @@ void QCocoaWindow::windowDidEndLiveResize()
void QCocoaWindow::windowDidBecomeKey()
{
- if (!isContentView())
+ // The NSWindow we're part of become key. Check if we're the first
+ // responder, and if so, deliver focus window change to our window.
+ if (m_view.window.firstResponder != m_view)
return;
- if (isForeignWindow())
- return;
-
- QNSView *firstResponderView = qt_objc_cast<QNSView *>(m_view.window.firstResponder);
- if (!firstResponderView)
- return;
+ qCDebug(lcQpaWindow) << m_view.window << "became key window."
+ << "Updating focus window to" << this << "with view" << m_view;
- const QCocoaWindow *focusCocoaWindow = firstResponderView.platformWindow;
- if (focusCocoaWindow->windowIsPopupType())
+ if (windowIsPopupType()) {
+ qCDebug(lcQpaWindow) << "Window is popup. Skipping focus window change.";
return;
+ }
// See also [QNSView becomeFirstResponder]
- QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>(
- focusCocoaWindow->window(), Qt::ActiveWindowFocusReason);
+ QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
+ window(), Qt::ActiveWindowFocusReason);
}
void QCocoaWindow::windowDidResignKey()
{
- if (!isContentView())
+ // The NSWindow we're part of lost key. Check if we're the first
+ // responder, and if so, deliver window deactivation to our window.
+ if (m_view.window.firstResponder != m_view)
return;
- if (isForeignWindow())
- return;
+ qCDebug(lcQpaWindow) << m_view.window << "resigned key window."
+ << "Clearing focus window" << this << "with view" << m_view;
// Make sure popups are closed before we deliver activation changes, which are
// otherwise ignored by QApplication.
@@ -1238,12 +1351,14 @@ void QCocoaWindow::windowDidResignKey()
NSWindow *newKeyWindow = [NSApp keyWindow];
if (newKeyWindow && newKeyWindow != m_view.window
&& [newKeyWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) {
+ qCDebug(lcQpaWindow) << "New key window" << newKeyWindow
+ << "is Qt window. Deferring focus window change.";
return;
}
// Lost key window, go ahead and set the active window to zero
if (!windowIsPopupType()) {
- QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>(
+ QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
nullptr, Qt::ActiveWindowFocusReason);
}
}
@@ -1280,8 +1395,14 @@ void QCocoaWindow::windowDidOrderOffScreen()
void QCocoaWindow::windowDidChangeOcclusionState()
{
+ // Note, we don't take the view's hiddenOrHasHiddenAncestor state into
+ // account here, but instead leave that up to handleExposeEvent, just
+ // like all the other signals that could potentially change the exposed
+ // state of the window.
bool visible = m_view.window.occlusionState & NSWindowOcclusionStateVisible;
- qCDebug(lcQpaWindow) << "QCocoaWindow::windowDidChangeOcclusionState" << window() << "is now" << (visible ? "visible" : "occluded");
+ qCDebug(lcQpaWindow) << "Occlusion state of" << m_view.window << "for"
+ << window() << "changed to" << (visible ? "visible" : "occluded");
+
if (visible)
[m_view setNeedsDisplay:YES];
else
@@ -1381,6 +1502,12 @@ void QCocoaWindow::handleGeometryChange()
QWindowSystemInterface::handleGeometryChange(window(), newGeometry);
+ // Changing the window geometry may affect the safe area margins
+ if (safeAreaMargins() != m_lastReportedSafeAreaMargins) {
+ m_lastReportedSafeAreaMargins = safeAreaMargins();
+ QWindowSystemInterface::handleSafeAreaMarginsChanged(window());
+ }
+
// Guard against processing window system events during QWindow::setGeometry
// calls, which Qt and Qt applications do not expect.
if (!m_inSetGeometry)
@@ -1448,23 +1575,31 @@ void QCocoaWindow::recreateWindowIfNeeded()
{
QMacAutoReleasePool pool;
+ QPlatformWindow *parentWindow = QPlatformWindow::parent();
+ auto *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow);
+
+ QCocoaWindow *oldParentCocoaWindow = nullptr;
+ if (QNSView *qnsView = qnsview_cast(m_view.superview))
+ oldParentCocoaWindow = qnsView.platformWindow;
+
if (isForeignWindow()) {
// A foreign window is created as such, and can never move between being
// foreign and not, so we don't need to get rid of any existing NSWindows,
// nor create new ones, as a foreign window is a single simple NSView.
qCDebug(lcQpaWindow) << "Skipping NSWindow management for foreign window" << this;
+
+ // We do however need to manage the parent relationship
+ if (parentCocoaWindow)
+ [parentCocoaWindow->m_view addSubview:m_view];
+ else if (oldParentCocoaWindow)
+ [m_view removeFromSuperview];
+
return;
}
- QPlatformWindow *parentWindow = QPlatformWindow::parent();
-
const bool isEmbeddedView = isEmbedded();
RecreationReasons recreateReason = RecreationNotNeeded;
- QCocoaWindow *oldParentCocoaWindow = nullptr;
- if (QNSView *qnsView = qnsview_cast(m_view.superview))
- oldParentCocoaWindow = qnsView.platformWindow;
-
if (parentWindow != oldParentCocoaWindow)
recreateReason |= ParentChanged;
@@ -1495,8 +1630,6 @@ void QCocoaWindow::recreateWindowIfNeeded()
if (recreateReason == RecreationNotNeeded)
return;
- QCocoaWindow *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow);
-
// Remove current window (if any)
if ((isContentView() && !shouldBeContentView) || (recreateReason & PanelChanged)) {
if (m_nsWindow) {
@@ -1557,6 +1690,23 @@ bool QCocoaWindow::updatesWithDisplayLink() const
void QCocoaWindow::deliverUpdateRequest()
{
qCDebug(lcQpaDrawing) << "Delivering update request to" << window();
+
+ if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(m_view.layer)) {
+ // 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();
}
@@ -1603,7 +1753,7 @@ void QCocoaWindow::setupPopupMonitor()
| NSEventMaskMouseMoved;
s_globalMouseMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mouseButtonMask
handler:^(NSEvent *e){
- if (!QGuiApplicationPrivate::instance()->popupActive()) {
+ if (!QGuiApplicationPrivate::instance()->activePopupWindow()) {
removePopupMonitor();
return;
}
@@ -1735,8 +1885,9 @@ QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel)
// Qt::Tool windows hide on app deactivation, unless Qt::WA_MacAlwaysShowToolWindow is set
nsWindow.hidesOnDeactivate = ((type & Qt::Tool) == Qt::Tool) && !alwaysShowToolWindow();
- // Make popup windows show on the same desktop as the parent full-screen window
- nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary;
+ // Make popup windows show on the same desktop as the parent window
+ nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary
+ | NSWindowCollectionBehaviorMoveToActiveSpace;
if ((type & Qt::Popup) == Qt::Popup) {
nsWindow.hasShadow = YES;
@@ -1751,11 +1902,15 @@ QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel)
applyContentBorderThickness(nsWindow);
- if (QColorSpace colorSpace = format().colorSpace(); colorSpace.isValid()) {
- NSData *iccData = colorSpace.iccProfile().toNSData();
- nsWindow.colorSpace = [[[NSColorSpace alloc] initWithICCProfileData:iccData] autorelease];
- qCDebug(lcQpaDrawing) << "Set" << this << "color space to" << nsWindow.colorSpace;
- }
+ // We propagate the view's color space granulary to both the IOSurfaces
+ // used for QSurface::RasterSurface, as well as the CAMetalLayer used for
+ // QSurface::MetalSurface, but for QSurface::OpenGLSurface we don't have
+ // that option as we use NSOpenGLContext instead of CAOpenGLLayer. As a
+ // workaround we set the NSWindow's color space, which affects GL drawing
+ // with NSOpenGLContext as well. This does not conflict with the granular
+ // modifications we do to each surface for raster or Metal.
+ if (auto *qtView = qnsview_cast(m_view))
+ nsWindow.colorSpace = qtView.colorSpace;
return nsWindow;
}
@@ -1924,7 +2079,7 @@ qreal QCocoaWindow::devicePixelRatio() const
{
// The documented way to observe the relationship between device-independent
// and device pixels is to use one for the convertToBacking functions. Other
- // methods such as [NSWindow backingScaleFacor] might not give the correct
+ // methods such as [NSWindow backingScaleFactor] might not give the correct
// result, for example if setWantsBestResolutionOpenGLSurface is not set or
// or ignored by the OpenGL driver.
NSSize backingSize = [m_view convertSizeToBacking:NSMakeSize(1.0, 1.0)];
@@ -1951,8 +2106,21 @@ bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder()
if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput))
return true;
- if (QWindowPrivate::get(window())->blockedByModalWindow)
- return true;
+ // For application modal windows, as well as direct parent windows
+ // of window modal windows, AppKit takes care of blocking interaction.
+ // The Qt expectation however, is that all transient parents of a
+ // window modal window is blocked, as reflected by QGuiApplication.
+ // We reflect this by returning false from this function for transient
+ // parents blocked by a modal window, but limit it to the cases not
+ // covered by AppKit to avoid potential unwanted side effects.
+ QWindow *modalWindow = nullptr;
+ if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) {
+ if (modalWindow->modality() == Qt::WindowModal && modalWindow->transientParent() != window()) {
+ qCDebug(lcQpaWindow) << "Refusing key window for" << this << "due to being"
+ << "blocked by" << modalWindow;
+ return true;
+ }
+ }
if (m_inSetVisible) {
QVariant showWithoutActivating = window()->property("_q_showWithoutActivating");