From 8c91070606bffabb49d2d17fb78a78b459dd8bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Wed, 18 Jul 2018 12:29:50 +0200 Subject: macOS: Invalidate window shadow after flushing backingstore to CALayer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Tor Arne Vestbø --- src/plugins/platforms/cocoa/qcocoabackingstore.mm | 165 +++++++++++----------- src/plugins/platforms/cocoa/qcocoawindow.mm | 6 - 2 files changed, 83 insertions(+), 88 deletions(-) diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 5eb45be04a..639851e855 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -132,113 +132,114 @@ void QCocoaBackingStore::flush(QWindow *window, const QRegion ®ion, const QPo // The contentsRect is in unit coordinate system CGAffineTransformMakeScale(1.0 / m_image.width(), 1.0 / m_image.height())); } - return; - } - - // 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]; - - // We also need to ensure the flushed view has focus, so that the graphics - // 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; - } + } 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]; + + // We also need to ensure the flushed view has focus, so that the graphics + // 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(); + 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 - // of blending. - QCocoaWindow *cocoaWindow = static_cast(window->handle()); - const NSCompositingOperation compositingOperation = cocoaWindow->isContentView() - && cocoaWindow->isOpaque() && !windowHasUnifiedToolbar() ? - NSCompositingOperationCopy : NSCompositingOperationSourceOver; + // 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 + // of blending. + QCocoaWindow *cocoaWindow = static_cast(window->handle()); + const NSCompositingOperation compositingOperation = cocoaWindow->isContentView() + && cocoaWindow->isOpaque() && !windowHasUnifiedToolbar() ? + NSCompositingOperationCopy : NSCompositingOperationSourceOver; #ifdef QT_DEBUG - static bool debugBackingStoreFlush = [[NSUserDefaults standardUserDefaults] - boolForKey:@"QtCocoaDebugBackingStoreFlush"]; + static bool debugBackingStoreFlush = [[NSUserDefaults standardUserDefaults] + boolForKey:@"QtCocoaDebugBackingStoreFlush"]; #endif - // ------------------------------------------------------------------------- + // ------------------------------------------------------------------------- - // The current contexts is typically a NSWindowGraphicsContext, but can be - // 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. - // [NSGraphicsContext drawingToScreen], or the attributes of the context. - NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; - Q_ASSERT_X(graphicsContext, "QCocoaBackingStore", - "Focusing the view should give us a current graphics context"); + // The current contexts is typically a NSWindowGraphicsContext, but can be + // 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. + // [NSGraphicsContext drawingToScreen], or the attributes of the context. + NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; + Q_ASSERT_X(graphicsContext, "QCocoaBackingStore", + "Focusing the view should give us a current graphics context"); - // Create temporary image to use for blitting, without copying image data - NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease]; + // Create temporary image to use for blitting, without copying image data + NSImage *backingStoreImage = [[[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize] autorelease]; - QRegion clippedRegion = region; - for (QWindow *w = window; w; w = w->parent()) { - if (!w->mask().isEmpty()) { - clippedRegion &= w == window ? w->mask() - : w->mask().translated(window->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)))); + QRegion clippedRegion = region; + for (QWindow *w = window; w; w = w->parent()) { + if (!w->mask().isEmpty()) { + clippedRegion &= w == window ? w->mask() + : 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())); + 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(); + CGRect viewRect = viewLocalRect.toCGRect(); - if (windowHasUnifiedToolbar()) - NSDrawWindowBackground(viewRect); + if (windowHasUnifiedToolbar()) + NSDrawWindowBackground(viewRect); - [backingStoreImage drawInRect:viewRect fromRect:backingStoreRect.toCGRect() - operation:compositingOperation fraction:1.0 respectFlipped:YES hints:nil]; + [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()]; + 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 + } + + // ------------------------------------------------------------------------- + + 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(topLevelWindow->handle()); if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) { [topLevelView.window invalidateShadow]; topLevelCocoaWindow->m_needsInvalidateShadow = false; } - - // ------------------------------------------------------------------------- - - if (shouldHandleViewLockManually) - [view unlockFocus]; - - if (drawingOutsideOfDisplayCycle) { - redrawRoundedBottomCorners([view convertRect:region.boundingRect().toCGRect() toView:nil]); - [view.window flushWindow]; - } } /* diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index c916ae41de..98e808bdf3 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -1032,12 +1032,6 @@ void QCocoaWindow::setMask(const QRegion ®ion) // time, and so would not result in an updated backingstore. m_needsInvalidateShadow = true; [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. } } -- cgit v1.2.3