macOS: Invalidate window shadow after flushing backingstore to CALayer

The macOS bug preventing us from doing this has now been fixed:

  http://openradar.appspot.com/radar?id=4976602949615616

Change-Id: I3bfa75d6bf982a051a9b274f530529f4ec4351a0
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Tor Arne Vestbø 2018-07-18 12:29:50 +02:00
parent 99c3ef0789
commit 8c91070606
2 changed files with 88 additions and 93 deletions

View File

@ -132,113 +132,114 @@ void QCocoaBackingStore::flush(QWindow *window, const QRegion &region, const QPo
// The contentsRect is in unit coordinate system // The contentsRect is in unit coordinate system
CGAffineTransformMakeScale(1.0 / m_image.width(), 1.0 / m_image.height())); CGAffineTransformMakeScale(1.0 / m_image.width(), 1.0 / m_image.height()));
} }
return; } else {
} // Normally a NSView is drawn via drawRect, as part of the display cycle in the
// main runloop, via setNeedsDisplay and friends. AppKit will lock focus on each
// individual view, starting with the top level and then traversing any subviews,
// calling drawRect for each of them. This pull model results in expose events
// sent to Qt, which result in drawing to the backingstore and flushing it.
// Qt may also decide to paint and flush the backingstore via e.g. timers,
// or other events such as mouse events, in which case we're in a push model.
// If there is no focused view, it means we're in the latter case, and need
// to manually flush the NSWindow after drawing to its graphic context.
const bool drawingOutsideOfDisplayCycle = ![NSView focusView];
// Normally a NSView is drawn via drawRect, as part of the display cycle in the // We also need to ensure the flushed view has focus, so that the graphics
// main runloop, via setNeedsDisplay and friends. AppKit will lock focus on each // context is set up correctly (coordinate system, clipping, etc). Outside
// individual view, starting with the top level and then traversing any subviews, // of the normal display cycle there is no focused view, as explained above,
// calling drawRect for each of them. This pull model results in expose events // so we have to handle it manually. There's also a corner case inside the
// sent to Qt, which result in drawing to the backingstore and flushing it. // normal display cycle due to way QWidgetBackingStore composits native child
// Qt may also decide to paint and flush the backingstore via e.g. timers, // widgets, where we'll get a flush of a native child during the drawRect of
// or other events such as mouse events, in which case we're in a push model. // its parent/ancestor, and the parent/ancestor being the one locked by AppKit.
// If there is no focused view, it means we're in the latter case, and need // In this case we also need to lock and unlock focus manually.
// to manually flush the NSWindow after drawing to its graphic context. const bool shouldHandleViewLockManually = [NSView focusView] != view;
const bool drawingOutsideOfDisplayCycle = ![NSView focusView]; if (shouldHandleViewLockManually && ![view lockFocusIfCanDraw]) {
qWarning() << "failed to lock focus of" << view;
return;
}
// We also need to ensure the flushed view has focus, so that the graphics const qreal devicePixelRatio = m_image.devicePixelRatio();
// context is set up correctly (coordinate system, clipping, etc). Outside
// of the normal display cycle there is no focused view, as explained above,
// so we have to handle it manually. There's also a corner case inside the
// normal display cycle due to way QWidgetBackingStore composits native child
// widgets, where we'll get a flush of a native child during the drawRect of
// its parent/ancestor, and the parent/ancestor being the one locked by AppKit.
// In this case we also need to lock and unlock focus manually.
const bool shouldHandleViewLockManually = [NSView focusView] != view;
if (shouldHandleViewLockManually && ![view lockFocusIfCanDraw]) {
qWarning() << "failed to lock focus of" << view;
return;
}
const qreal devicePixelRatio = m_image.devicePixelRatio(); // If the flushed window is a content view, and not in unified toolbar mode,
// and is fully opaque, we can get away with copying the backingstore instead
// If the flushed window is a content view, and not in unified toolbar mode, // of blending.
// and is fully opaque, we can get away with copying the backingstore instead QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle());
// of blending. const NSCompositingOperation compositingOperation = cocoaWindow->isContentView()
QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()); && cocoaWindow->isOpaque() && !windowHasUnifiedToolbar() ?
const NSCompositingOperation compositingOperation = cocoaWindow->isContentView() NSCompositingOperationCopy : NSCompositingOperationSourceOver;
&& cocoaWindow->isOpaque() && !windowHasUnifiedToolbar() ?
NSCompositingOperationCopy : NSCompositingOperationSourceOver;
#ifdef QT_DEBUG #ifdef QT_DEBUG
static bool debugBackingStoreFlush = [[NSUserDefaults standardUserDefaults] static bool debugBackingStoreFlush = [[NSUserDefaults standardUserDefaults]
boolForKey:@"QtCocoaDebugBackingStoreFlush"]; boolForKey:@"QtCocoaDebugBackingStoreFlush"];
#endif #endif
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// The current contexts is typically a NSWindowGraphicsContext, but can be // The current contexts is typically a NSWindowGraphicsContext, but can be
// NSBitmapGraphicsContext e.g. when debugging the view hierarchy in Xcode. // NSBitmapGraphicsContext e.g. when debugging the view hierarchy in Xcode.
// If we need to distinguish things here in the future, we can use e.g. // If we need to distinguish things here in the future, we can use e.g.
// [NSGraphicsContext drawingToScreen], or the attributes of the context. // [NSGraphicsContext drawingToScreen], or the attributes of the context.
NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext];
Q_ASSERT_X(graphicsContext, "QCocoaBackingStore", Q_ASSERT_X(graphicsContext, "QCocoaBackingStore",
"Focusing the view should give us a current graphics context"); "Focusing the view should give us a current graphics context");
// Create temporary image to use for blitting, without copying image data // Create temporary image to use for blitting, without copying image data
NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease]; NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease];
QRegion clippedRegion = region; QRegion clippedRegion = region;
for (QWindow *w = window; w; w = w->parent()) { for (QWindow *w = window; w; w = w->parent()) {
if (!w->mask().isEmpty()) { if (!w->mask().isEmpty()) {
clippedRegion &= w == window ? w->mask() clippedRegion &= w == window ? w->mask()
: w->mask().translated(window->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)))); : w->mask().translated(window->mapFromGlobal(w->mapToGlobal(QPoint(0, 0))));
}
}
for (const QRect &viewLocalRect : clippedRegion) {
QPoint backingStoreOffset = viewLocalRect.topLeft() + offset;
QRect backingStoreRect(backingStoreOffset * devicePixelRatio, viewLocalRect.size() * devicePixelRatio);
if (graphicsContext.flipped) // Flip backingStoreRect to match graphics context
backingStoreRect.moveTop(m_image.height() - (backingStoreRect.y() + backingStoreRect.height()));
CGRect viewRect = viewLocalRect.toCGRect();
if (windowHasUnifiedToolbar())
NSDrawWindowBackground(viewRect);
[backingStoreImage drawInRect:viewRect fromRect:backingStoreRect.toCGRect()
operation:compositingOperation fraction:1.0 respectFlipped:YES hints:nil];
#ifdef QT_DEBUG
if (Q_UNLIKELY(debugBackingStoreFlush)) {
[[NSColor colorWithCalibratedRed:drand48() green:drand48() blue:drand48() alpha:0.3] set];
[NSBezierPath fillRect:viewRect];
if (drawingOutsideOfDisplayCycle) {
[[[NSColor magentaColor] colorWithAlphaComponent:0.5] set];
[NSBezierPath strokeLineFromPoint:viewLocalRect.topLeft().toCGPoint()
toPoint:viewLocalRect.bottomRight().toCGPoint()];
} }
} }
for (const QRect &viewLocalRect : clippedRegion) {
QPoint backingStoreOffset = viewLocalRect.topLeft() + offset;
QRect backingStoreRect(backingStoreOffset * devicePixelRatio, viewLocalRect.size() * devicePixelRatio);
if (graphicsContext.flipped) // Flip backingStoreRect to match graphics context
backingStoreRect.moveTop(m_image.height() - (backingStoreRect.y() + backingStoreRect.height()));
CGRect viewRect = viewLocalRect.toCGRect();
if (windowHasUnifiedToolbar())
NSDrawWindowBackground(viewRect);
[backingStoreImage drawInRect:viewRect fromRect:backingStoreRect.toCGRect()
operation:compositingOperation fraction:1.0 respectFlipped:YES hints:nil];
#ifdef QT_DEBUG
if (Q_UNLIKELY(debugBackingStoreFlush)) {
[[NSColor colorWithCalibratedRed:drand48() green:drand48() blue:drand48() alpha:0.3] set];
[NSBezierPath fillRect:viewRect];
if (drawingOutsideOfDisplayCycle) {
[[[NSColor magentaColor] colorWithAlphaComponent:0.5] set];
[NSBezierPath strokeLineFromPoint:viewLocalRect.topLeft().toCGPoint()
toPoint:viewLocalRect.bottomRight().toCGPoint()];
}
}
#endif #endif
}
// -------------------------------------------------------------------------
if (shouldHandleViewLockManually)
[view unlockFocus];
if (drawingOutsideOfDisplayCycle) {
redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]);
[view.window flushWindow];
}
} }
// Done flushing to either CALayer or NSWindow backingstore
QCocoaWindow *topLevelCocoaWindow = static_cast<QCocoaWindow *>(topLevelWindow->handle()); QCocoaWindow *topLevelCocoaWindow = static_cast<QCocoaWindow *>(topLevelWindow->handle());
if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) { if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) {
[topLevelView.window invalidateShadow]; [topLevelView.window invalidateShadow];
topLevelCocoaWindow->m_needsInvalidateShadow = false; topLevelCocoaWindow->m_needsInvalidateShadow = false;
} }
// -------------------------------------------------------------------------
if (shouldHandleViewLockManually)
[view unlockFocus];
if (drawingOutsideOfDisplayCycle) {
redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]);
[view.window flushWindow];
}
} }
/* /*

View File

@ -1032,12 +1032,6 @@ void QCocoaWindow::setMask(const QRegion &region)
// time, and so would not result in an updated backingstore. // time, and so would not result in an updated backingstore.
m_needsInvalidateShadow = true; m_needsInvalidateShadow = true;
[m_view setNeedsDisplay:YES]; [m_view setNeedsDisplay:YES];
// FIXME: [NSWindow invalidateShadow] has no effect when in layer-backed mode,
// so if the mask is changed after the initial mask is applied, it will not
// result in any visual change to the shadow. This is an Apple bug, and there
// may be ways to work around it, such as calling setFrame on the window to
// trigger some internal invalidation, but that needs more research.
} }
} }