macOS: Deliver NSWindow notifications to all windows, not just top level

Child QWindows (or in the case of QWindows embedded in native applications:
top level QWindows where the corresponding NSView is a child of another
view, so not being the contentView of its window), still need some of the
NSWindow notifications to e.g. update their exposed state when the window
becomes visible.

We make sure to send the notification to all QCococaWindow children of
the relevant NSWindow, and let each callback decide if it should only
apply to content views.

This fixes an issue where a QWindow would never be exposed if the window
was a child NSView and added to a NSWindow that was yet to be shown.

Change-Id: I7f7df8bc5f4ca3ac553a2c146f8c3229b197c059
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
This commit is contained in:
Tor Arne Vestbø 2017-09-11 14:04:51 +02:00
parent 44c304cefb
commit 49154acde3
4 changed files with 102 additions and 60 deletions

View File

@ -134,11 +134,12 @@ public:
void setEmbeddedInForeignView(bool subwindow);
Q_NOTIFICATION_HANDLER(NSViewFrameDidChangeNotification) void viewDidChangeFrame();
Q_NOTIFICATION_HANDLER(NSViewGlobalFrameDidChangeNotification) void viewDidChangeGlobalFrame();
Q_NOTIFICATION_HANDLER(NSWindowWillMoveNotification) void windowWillMove();
Q_NOTIFICATION_HANDLER(NSWindowDidMoveNotification) void windowDidMove();
Q_NOTIFICATION_HANDLER(NSWindowDidResizeNotification) void windowDidResize();
Q_NOTIFICATION_HANDLER(NSViewFrameDidChangeNotification) void viewDidChangeFrame();
Q_NOTIFICATION_HANDLER(NSViewGlobalFrameDidChangeNotification) void viewDidChangeGlobalFrame();
Q_NOTIFICATION_HANDLER(NSWindowDidEndLiveResizeNotification) void windowDidEndLiveResize();
Q_NOTIFICATION_HANDLER(NSWindowDidBecomeKeyNotification) void windowDidBecomeKey();
Q_NOTIFICATION_HANDLER(NSWindowDidResignKeyNotification) void windowDidResignKey();
@ -148,8 +149,8 @@ public:
Q_NOTIFICATION_HANDLER(NSWindowDidEnterFullScreenNotification) void windowDidEnterFullScreen();
Q_NOTIFICATION_HANDLER(NSWindowWillExitFullScreenNotification) void windowWillExitFullScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidExitFullScreenNotification) void windowDidExitFullScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidOrderOffScreenNotification) void windowDidOrderOffScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidOrderOnScreenAndFinishAnimatingNotification) void windowDidOrderOnScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidOrderOffScreenNotification) void windowDidOrderOffScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidChangeOcclusionStateNotification) void windowDidChangeOcclusionState();
Q_NOTIFICATION_HANDLER(NSWindowDidChangeScreenNotification) void windowDidChangeScreen();
Q_NOTIFICATION_HANDLER(NSWindowWillCloseNotification) void windowWillClose();

View File

@ -97,34 +97,30 @@ static void qRegisterNotificationCallbacks()
[center addObserverForName:notificationName.toNSString() object:nil queue:nil
usingBlock:^(NSNotification *notification) {
NSView *view = nullptr;
QVarLengthArray<QCocoaWindow *, 32> cocoaWindows;
if ([notification.object isKindOfClass:[NSWindow class]]) {
NSWindow *window = notification.object;
if (!window.contentView)
return;
view = window.contentView;
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]]) {
view = notification.object;
if (QNSView *qnsView = qnsview_cast(notification.object))
cocoaWindows += qnsView.platformWindow;
} else {
qCWarning(lcQpaCocoaWindow) << "Unhandled notifcation"
<< notification.name << "for" << notification.object;
return;
}
Q_ASSERT(view);
QCocoaWindow *cocoaWindow = nullptr;
if (QNSView *qnsView = qnsview_cast(view))
cocoaWindow = qnsView.platformWindow;
// FIXME: Could be a foreign window, look up by iterating top level QWindows
if (!cocoaWindow)
return;
if (!method.invoke(cocoaWindow, Qt::DirectConnection)) {
qCWarning(lcQpaCocoaWindow) << "Failed to invoke NSNotification callback for"
<< notification.name << "on" << cocoaWindow;
for (QCocoaWindow *cocoaWindow : cocoaWindows) {
if (!method.invoke(cocoaWindow, Qt::DirectConnection)) {
qCWarning(lcQpaCocoaWindow) << "Failed to invoke NSNotification callback for"
<< notification.name << "on" << cocoaWindow;
}
}
}];
}
@ -844,32 +840,7 @@ void QCocoaWindow::setEmbeddedInForeignView(bool embedded)
m_nsWindow = 0;
}
// ----------------------- NSWindow notifications -----------------------
void QCocoaWindow::windowWillMove()
{
// Close any open popups on window move
qt_closePopups();
}
void QCocoaWindow::windowDidMove()
{
handleGeometryChange();
// Moving a window might bring it out of maximized state
handleWindowStateChanged();
}
void QCocoaWindow::windowDidResize()
{
if (!isContentView())
return;
handleGeometryChange();
if (!m_view.inLiveResize)
handleWindowStateChanged();
}
// ----------------------- NSView notifications -----------------------
void QCocoaWindow::viewDidChangeFrame()
{
@ -888,13 +859,54 @@ void QCocoaWindow::viewDidChangeGlobalFrame()
[m_view setNeedsDisplay:YES];
}
// ----------------------- 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::windowWillMove()
{
// Close any open popups on window move
qt_closePopups();
}
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::windowDidEndLiveResize()
{
if (!isContentView())
return;
handleWindowStateChanged();
}
void QCocoaWindow::windowDidBecomeKey()
{
if (!isContentView())
return;
if (isForeignWindow())
return;
@ -911,6 +923,9 @@ void QCocoaWindow::windowDidBecomeKey()
void QCocoaWindow::windowDidResignKey()
{
if (!isContentView())
return;
if (isForeignWindow())
return;
@ -927,16 +942,25 @@ void QCocoaWindow::windowDidResignKey()
void QCocoaWindow::windowDidMiniaturize()
{
if (!isContentView())
return;
handleWindowStateChanged();
}
void QCocoaWindow::windowDidDeminiaturize()
{
if (!isContentView())
return;
handleWindowStateChanged();
}
void QCocoaWindow::windowWillEnterFullScreen()
{
if (!isContentView())
return;
// 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.
@ -945,6 +969,9 @@ void QCocoaWindow::windowWillEnterFullScreen()
void QCocoaWindow::windowDidEnterFullScreen()
{
if (!isContentView())
return;
Q_ASSERT_X(m_view.window.qt_fullScreen, "QCocoaWindow",
"FullScreen category processes window notifications first");
@ -956,6 +983,9 @@ void QCocoaWindow::windowDidEnterFullScreen()
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 |= NSResizableWindowMask;
@ -963,6 +993,9 @@ void QCocoaWindow::windowWillExitFullScreen()
void QCocoaWindow::windowDidExitFullScreen()
{
if (!isContentView())
return;
Q_ASSERT_X(!m_view.window.qt_fullScreen, "QCocoaWindow",
"FullScreen category processes window notifications first");
@ -981,16 +1014,16 @@ void QCocoaWindow::windowDidExitFullScreen()
}
}
void QCocoaWindow::windowDidOrderOffScreen()
{
handleExposeEvent(QRegion());
}
void QCocoaWindow::windowDidOrderOnScreen()
{
[m_view setNeedsDisplay:YES];
}
void QCocoaWindow::windowDidOrderOffScreen()
{
handleExposeEvent(QRegion());
}
void QCocoaWindow::windowDidChangeOcclusionState()
{
if (m_view.window.occlusionState & NSWindowOcclusionStateVisible)
@ -1422,15 +1455,15 @@ QRect QCocoaWindow::nativeWindowGeometry() const
*/
void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState)
{
if (!isContentView())
return;
const Qt::WindowState currentState = windowState();
const Qt::WindowState newState = QWindowPrivate::effectiveState(requestedState);
if (newState == currentState)
return;
if (!isContentView())
return;
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

View File

@ -87,8 +87,6 @@
options:NSTrackingActiveInActiveApp | NSTrackingInVisibleRect | NSTrackingCursorUpdate
owner:contentView userInfo:nil]];
window.contentView = contentView;
// Create the QWindow, add its NSView to the content view
m_window = new RasterWindow;
m_window->setObjectName("RasterWindow");
@ -104,10 +102,15 @@
NSTextField *textField = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 80, 25)];
[(NSView*)childWindow->winId() addSubview:textField];
[window.contentView addSubview:reinterpret_cast<NSView *>(m_window->winId())];
[contentView addSubview:reinterpret_cast<NSView *>(m_window->winId())];
// Show the NSWindow
[window makeKeyAndOrderFront:NSApp];
window.contentView = contentView;
// Show the NSWindow delayed, so that we can verify that Qt picks up the right
// notifications to expose the window when it does become visible.
dispatch_async(dispatch_get_main_queue(), ^{
[window makeKeyAndOrderFront:NSApp];
});
}
- (void)applicationWillTerminate:(NSNotification *)notification

View File

@ -148,6 +148,11 @@ bool RasterWindow::event(QEvent *e)
void RasterWindow::render()
{
if (!isExposed()) {
qDebug() << "Skipping render, not exposed";
return;
}
QRect rect(QPoint(), geometry().size());
m_backingStore->resize(rect.size());