diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.h b/src/plugins/platforms/cocoa/qcocoabackingstore.h index 5ed455fd71..bf6766beaa 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.h +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.h @@ -53,6 +53,7 @@ public: void flush(QWindow *, const QRegion &, const QPoint &) Q_DECL_OVERRIDE; private: + bool windowHasUnifiedToolbar() const; QImage::Format format() const Q_DECL_OVERRIDE; }; diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 1d7ad772dc..e0f7f7f57a 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -44,6 +44,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcCocoaBackingStore, "qt.qpa.cocoa.backingstore"); + QCocoaBackingStore::QCocoaBackingStore(QWindow *window) : QRasterBackingStore(window) { @@ -51,26 +53,157 @@ QCocoaBackingStore::QCocoaBackingStore(QWindow *window) QCocoaBackingStore::~QCocoaBackingStore() { - if (QCocoaWindow *cocoaWindow = static_cast(window()->handle())) - [qnsview_cast(cocoaWindow->view()) clearBackingStore:this]; +} + +bool QCocoaBackingStore::windowHasUnifiedToolbar() const +{ + Q_ASSERT(window()->handle()); + return static_cast(window()->handle())->m_drawContentBorderGradient; } QImage::Format QCocoaBackingStore::format() const { - QCocoaWindow *cocoaWindow = static_cast(window()->handle()); - if (cocoaWindow && cocoaWindow->m_drawContentBorderGradient) + if (windowHasUnifiedToolbar()) return QImage::Format_ARGB32_Premultiplied; return QRasterBackingStore::format(); } +#if !QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_12) +static const NSCompositingOperation NSCompositingOperationCopy = NSCompositeCopy; +static const NSCompositingOperation NSCompositingOperationSourceOver = NSCompositeSourceOver; +#endif + +/*! + Flushes the given \a region from the specified \a window onto the + screen. + + The \a window is the top level window represented by this backingstore, + or a non-transient child of that window. + + If the \a window is a child window, the \a region will be in child window + coordinates, and the \a offset will be the child window's offset in relation + to the backingstore's top level window. +*/ void QCocoaBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) { if (m_image.isNull()) return; - if (QCocoaWindow *cocoaWindow = static_cast(window->handle())) - [qnsview_cast(cocoaWindow->view()) flushBackingStore:this region:region offset:offset]; + const QWindow *topLevelWindow = this->window(); + + Q_ASSERT(topLevelWindow->handle() && window->handle()); + Q_ASSERT(!topLevelWindow->handle()->isForeignWindow() && !window->handle()->isForeignWindow()); + + QNSView *topLevelView = qnsview_cast(static_cast(topLevelWindow->handle())->view()); + QNSView *view = qnsview_cast(static_cast(window->handle())->view()); + + if (lcCocoaBackingStore().isDebugEnabled()) { + QString targetViewDescription; + if (view != topLevelView) { + QDebug targetDebug(&targetViewDescription); + targetDebug << "onto" << topLevelView << "at" << offset; + } + qCDebug(lcCocoaBackingStore) << "Flushing" << region << "of" << view << targetViewDescription; + } + + // 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(); + + // If the flushed window is a content view, and not in unified toolbar mode, + // we can get away with copying the backingstore instead of blending. + const NSCompositingOperation compositingOperation = static_cast( + window->handle())->isContentView() && !windowHasUnifiedToolbar() ? + NSCompositingOperationCopy : NSCompositingOperationSourceOver; + +#ifdef QT_DEBUG + 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"); + + // Create temporary image to use for blitting, without copying image data + NSImage *backingStoreImage = [[[NSImage alloc] + initWithCGImage:QCFType(m_image.toCGImage()) size:NSZeroSize] autorelease]; + + if ([topLevelView hasMask]) { + // FIXME: Implement via NSBezierPath and addClip + CGRect boundingRect = region.boundingRect().toCGRect(); + QCFType subMask = CGImageCreateWithImageInRect([topLevelView maskImage], boundingRect); + CGContextClipToMask(graphicsContext.CGContext, boundingRect, subMask); + } + + for (const QRect &viewLocalRect : region) { + 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 + } + + // ------------------------------------------------------------------------- + + if (shouldHandleViewLockManually) + [view unlockFocus]; + + if (drawingOutsideOfDisplayCycle) + [view.window flushWindow]; + + // FIXME: Tie to changing window flags and/or mask instead + [view invalidateWindowShadowIfNeeded]; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 86599a2529..9f98ad56de 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -1059,8 +1059,6 @@ void QCocoaWindow::handleGeometryChange() // calls, which Qt and Qt applications do not expect. if (!m_inSetGeometry) QWindowSystemInterface::flushWindowSystemEvents(); - else if (newGeometry.size() != geometry().size()) - [qnsview_cast(m_view) clearBackingStore]; } void QCocoaWindow::handleExposeEvent(const QRegion ®ion) diff --git a/src/plugins/platforms/cocoa/qnsview.h b/src/plugins/platforms/cocoa/qnsview.h index 770aae240e..414c6b7fe7 100644 --- a/src/plugins/platforms/cocoa/qnsview.h +++ b/src/plugins/platforms/cocoa/qnsview.h @@ -51,15 +51,12 @@ QT_BEGIN_NAMESPACE class QCocoaWindow; -class QCocoaBackingStore; class QCocoaGLContext; QT_END_NAMESPACE Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper)); @interface QT_MANGLE_NAMESPACE(QNSView) : NSView { - QCocoaBackingStore* m_backingStore; - QPoint m_backingStoreOffset; QRegion m_maskRegion; CGImageRef m_maskImage; bool m_shouldInvalidateWindowShadow; @@ -93,13 +90,10 @@ Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper)); #ifndef QT_NO_OPENGL - (void)setQCocoaGLContext:(QCocoaGLContext *)context; #endif -- (void)flushBackingStore:(QCocoaBackingStore *)backingStore region:(const QRegion &)region offset:(QPoint)offset; -- (void)clearBackingStore; -- (void)clearBackingStore:(QCocoaBackingStore *)backingStore; - (void)setMaskRegion:(const QRegion *)region; +- (CGImageRef)maskImage; - (void)invalidateWindowShadowIfNeeded; - (void)drawRect:(NSRect)dirtyRect; -- (void)drawBackingStoreUsingCoreGraphics:(NSRect)dirtyRect; - (void)textInputContextKeyboardSelectionDidChangeNotification : (NSNotification *) textInputContextKeyboardSelectionDidChangeNotification; - (void)viewDidHide; - (void)removeFromSuperview; diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index 73d308213d..e531e79c8f 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -134,7 +134,6 @@ static QTouchDevice *touchDevice = 0; - (id) init { if (self = [super initWithFrame:NSZeroRect]) { - m_backingStore = 0; m_maskImage = 0; m_shouldInvalidateWindowShadow = false; m_buttons = Qt::NoButton; @@ -267,11 +266,6 @@ static QTouchDevice *touchDevice = 0; } } -- (void)viewDidMoveToWindow -{ - m_backingStore = Q_NULLPTR; -} - - (QWindow *)topLevelWindow { if (!m_platformWindow) @@ -316,67 +310,6 @@ static QTouchDevice *touchDevice = 0; [super removeFromSuperview]; } -- (void)flushBackingStore:(QCocoaBackingStore *)backingStore region:(const QRegion &)region offset:(QPoint)offset -{ - qCDebug(lcQpaCocoaWindow) << "[QNSView flushBackingStore:]" << m_platformWindow->window() << region.rectCount() << region.boundingRect() << offset; - - m_backingStore = backingStore; - m_backingStoreOffset = offset * m_backingStore->paintDevice()->devicePixelRatio(); - - // FIXME: Clean up this method now that the drawRect logic has been merged into it - - const NSRect dirtyRect = region.boundingRect().toCGRect(); - - // 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] != self; - if (shouldHandleViewLockManually && ![self lockFocusIfCanDraw]) { - qWarning() << "failed to lock focus of" << self; - return; - } - - if (m_platformWindow->m_drawContentBorderGradient) - NSDrawWindowBackground(dirtyRect); - - [self drawBackingStoreUsingCoreGraphics:dirtyRect]; - - if (shouldHandleViewLockManually) - [self unlockFocus]; - - if (drawingOutsideOfDisplayCycle) - [self.window flushWindow]; - - [self invalidateWindowShadowIfNeeded]; -} - -- (void)clearBackingStore -{ - m_backingStore = nullptr; -} - -- (void)clearBackingStore:(QCocoaBackingStore *)backingStore -{ - if (backingStore == m_backingStore) - m_backingStore = 0; -} - - (BOOL) hasMask { return !m_maskRegion.isEmpty(); @@ -418,6 +351,11 @@ static QTouchDevice *touchDevice = 0; m_maskImage = qt_mac_toCGImageMask(maskImage); } +- (CGImageRef)maskImage +{ + return m_maskImage; +} + - (void)invalidateWindowShadowIfNeeded { if (m_shouldInvalidateWindowShadow && m_platformWindow->isContentView()) { @@ -452,69 +390,7 @@ static QTouchDevice *touchDevice = 0; m_platformWindow->handleExposeEvent(exposedRegion); } -// Draws the backing store content to the QNSView using Core Graphics. -// This function assumes that the QNSView is in a configuration that -// supports Core Graphics, such as "classic" mode or layer mode with -// the default layer. -- (void)drawBackingStoreUsingCoreGraphics:(NSRect)dirtyRect -{ - if (!m_backingStore) - return; - - // Calculate source and target rects. The target rect is the dirtyRect: - CGRect dirtyWindowRect = NSRectToCGRect(dirtyRect); - - // The backing store source rect will be larger on retina displays. - // Scale dirtyRect by the device pixel ratio: - const qreal devicePixelRatio = m_backingStore->paintDevice()->devicePixelRatio(); - CGRect dirtyBackingRect = CGRectMake(dirtyRect.origin.x * devicePixelRatio, - dirtyRect.origin.y * devicePixelRatio, - dirtyRect.size.width * devicePixelRatio, - dirtyRect.size.height * devicePixelRatio); - - NSGraphicsContext *nsGraphicsContext = [NSGraphicsContext currentContext]; - CGContextRef cgContext = (CGContextRef) [nsGraphicsContext graphicsPort]; - - // Translate coordiate system from CoreGraphics (bottom-left) to NSView (top-left): - CGContextSaveGState(cgContext); - int dy = dirtyWindowRect.origin.y + CGRectGetMaxY(dirtyWindowRect); - - CGContextTranslateCTM(cgContext, 0, dy); - CGContextScaleCTM(cgContext, 1, -1); - - // If a mask is set, modify the sub image accordingly: - CGImageRef subMask = 0; - if (m_maskImage) { - subMask = CGImageCreateWithImageInRect(m_maskImage, dirtyWindowRect); - CGContextClipToMask(cgContext, dirtyWindowRect, subMask); - } - - // Clip out and draw the correct sub image from the (shared) backingstore: - CGRect backingStoreRect = CGRectMake( - dirtyBackingRect.origin.x + m_backingStoreOffset.x(), - dirtyBackingRect.origin.y + m_backingStoreOffset.y(), - dirtyBackingRect.size.width, - dirtyBackingRect.size.height - ); - CGImageRef bsCGImage = qt_mac_toCGImage(m_backingStore->toImage()); - CGImageRef cleanImg = CGImageCreateWithImageInRect(bsCGImage, backingStoreRect); - - // Optimization: Copy frame buffer content instead of blending for - // top-level windows where Qt fills the entire window content area. - // (But don't overpaint the title-bar gradient) - if (m_platformWindow->isContentView() && !m_platformWindow->m_drawContentBorderGradient) - CGContextSetBlendMode(cgContext, kCGBlendModeCopy); - - CGContextDrawImage(cgContext, dirtyWindowRect, cleanImg); - - // Clean-up: - CGContextRestoreGState(cgContext); - CGImageRelease(cleanImg); - CGImageRelease(subMask); - CGImageRelease(bsCGImage); -} - -- (BOOL) isFlipped +- (BOOL)isFlipped { return YES; }