macOS: Decouple NSWindow notifications and delegate callbacks from QNSView

The logic for handling NSWindow events was split partly between QNSView
observing notifications, and QNSWindowDelegate implementing direct delegate
callbacks. The logic of how to handle the events was then split further
by sometimes handling the event in the delegate callback or notification
handler, and sometimes forwarding the event to QCocoaWindow.

We now handle most events via notifications, and propagate these directly
to QCocoaWindow, so that all the logic is in one place. This improves the
situation for foreign windows, since we're not relying on having a QNSView,
or being able to inject our QNSWindowDelegate.

To keep code duplication to a minimum and risking missing a notification
in the forwarding logic, the logic is based on QMetatType and QMetaMethod
tags, so that the notifications are declared in the header file, along
with the handler function.

Change-Id: I2fb6372010048a8a1f6e4426b988a3f6f5abdbab
Reviewed-by: Erik Verbruggen <erik.verbruggen@qt.io>
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
bb10
Tor Arne Vestbø 2016-10-13 14:49:01 +02:00
parent d04207342e
commit 7f507c1620
7 changed files with 195 additions and 163 deletions

View File

@ -160,10 +160,8 @@ Qt::DropActions qt_mac_mapNSDragOperations(NSDragOperation nsActions)
*/
QNSView *qnsview_cast(NSView *view)
{
if (![view isKindOfClass:[QNSView class]]) {
qCWarning(lcQpaCocoaWindow) << "NSView is not QNSView, consider checking for Qt::ForeignWindow";
if (![view isKindOfClass:[QNSView class]])
return nil;
}
return static_cast<QNSView *>(view);
}

View File

@ -141,6 +141,13 @@ QT_BEGIN_NAMESPACE
// See the qt_on_cocoa manual tests for a working example, located
// in tests/manual/cocoa at the time of writing.
#ifdef Q_MOC_RUN
#define Q_NOTIFICATION_HANDLER(notification) Q_INVOKABLE Q_COCOA_NOTIFICATION_##notification
#else
#define Q_NOTIFICATION_HANDLER(notification)
#define Q_NOTIFICATION_PREFIX QT_STRINGIFY2(Q_COCOA_NOTIFICATION_)
#endif
class QCocoaMenuBar;
class QCocoaWindow : public QObject, public QPlatformWindow
@ -187,12 +194,23 @@ public:
void setEmbeddedInForeignView(bool subwindow);
void windowWillMove();
void windowDidMove();
void windowDidResize();
void windowDidEndLiveResize();
Q_NOTIFICATION_HANDLER(NSWindowWillMoveNotification) void windowWillMove();
Q_NOTIFICATION_HANDLER(NSWindowDidMoveNotification) void windowDidMove();
Q_NOTIFICATION_HANDLER(NSWindowDidResizeNotification) void windowDidResize();
Q_NOTIFICATION_HANDLER(NSWindowDidEndLiveResizeNotification) void windowDidEndLiveResize();
Q_NOTIFICATION_HANDLER(NSWindowDidBecomeKeyNotification) void windowDidBecomeKey();
Q_NOTIFICATION_HANDLER(NSWindowDidResignKeyNotification) void windowDidResignKey();
Q_NOTIFICATION_HANDLER(NSWindowDidMiniaturizeNotification) void windowDidMiniaturize();
Q_NOTIFICATION_HANDLER(NSWindowDidDeminiaturizeNotification) void windowDidDeminiaturize();
Q_NOTIFICATION_HANDLER(NSWindowDidEnterFullScreenNotification) void windowDidEnterFullScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidExitFullScreenNotification) void windowDidExitFullScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidOrderOffScreenNotification) void windowDidOrderOffScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidOrderOnScreenAndFinishAnimatingNotification) void windowDidOrderOnScreen();
Q_NOTIFICATION_HANDLER(NSWindowDidChangeOcclusionStateNotification) void windowDidChangeOcclusionState();
Q_NOTIFICATION_HANDLER(NSWindowDidChangeScreenNotification) void windowDidChangeScreen();
Q_NOTIFICATION_HANDLER(NSWindowWillCloseNotification) void windowWillClose();
bool windowShouldClose();
void windowWillClose();
bool windowIsPopupType(Qt::WindowType type = Qt::Widget) const;
void setSynchedWindowStateFromWindow();

View File

@ -343,6 +343,49 @@ static void qt_closePopups()
@end
static void qRegisterNotificationCallbacks()
{
static const QLatin1String notificationHandlerPrefix(Q_NOTIFICATION_PREFIX);
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
const QMetaObject *metaObject = QMetaType::metaObjectForType(qRegisterMetaType<QCocoaWindow*>());
Q_ASSERT(metaObject);
for (int i = 0; i < metaObject->methodCount(); ++i) {
QMetaMethod method = metaObject->method(i);
const QString methodTag = QString::fromLatin1(method.tag());
if (!methodTag.startsWith(notificationHandlerPrefix))
continue;
const QString notificationName = methodTag.mid(notificationHandlerPrefix.size());
[center addObserverForName:notificationName.toNSString() object:nil queue:nil
usingBlock:^(NSNotification *notification) {
NSWindow *window = notification.object;
// Only top level NSWindows should notify their QNSViews
if (window.parentWindow)
return;
QCocoaWindow *cocoaWindow = nullptr;
if (QNSView *view = qnsview_cast(window.contentView))
cocoaWindow = view.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;
}
}];
}
}
Q_CONSTRUCTOR_FUNCTION(qRegisterNotificationCallbacks)
const int QCocoaWindow::NoAlertRequest = -1;
QCocoaWindow::QCocoaWindow(QWindow *tlw)
@ -1180,6 +1223,8 @@ void QCocoaWindow::setEmbeddedInForeignView(bool embedded)
m_nsWindow = 0;
}
// ----------------------- NSWindow notifications -----------------------
void QCocoaWindow::windowWillMove()
{
// Close any open popups on window move
@ -1214,6 +1259,108 @@ void QCocoaWindow::windowDidEndLiveResize()
}
}
void QCocoaWindow::windowDidBecomeKey()
{
if (window()->type() == Qt::ForeignWindow)
return;
if (m_windowUnderMouse) {
QPointF windowPoint;
QPointF screenPoint;
[qnsview_cast(m_view) convertFromScreen:[NSEvent mouseLocation] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
QWindowSystemInterface::handleEnterEvent(m_enterLeaveTargetWindow, windowPoint, screenPoint);
}
if (!windowIsPopupType() && !qnsview_cast(m_view).isMenuView)
QWindowSystemInterface::handleWindowActivated(window());
}
void QCocoaWindow::windowDidResignKey()
{
if (window()->type() == Qt::ForeignWindow)
return;
// Key window will be non-nil if another window became key, so do not
// set the active window to zero here -- the new key window's
// NSWindowDidBecomeKeyNotification hander will change the active window.
NSWindow *keyWindow = [NSApp keyWindow];
if (!keyWindow || keyWindow == m_view.window) {
// No new key window, go ahead and set the active window to zero
if (!windowIsPopupType() && !qnsview_cast(m_view).isMenuView)
QWindowSystemInterface::handleWindowActivated(0);
}
}
void QCocoaWindow::windowDidMiniaturize()
{
[qnsview_cast(m_view) notifyWindowStateChanged:Qt::WindowMinimized];
}
void QCocoaWindow::windowDidDeminiaturize()
{
[qnsview_cast(m_view) notifyWindowStateChanged:Qt::WindowNoState];
}
void QCocoaWindow::windowDidEnterFullScreen()
{
[qnsview_cast(m_view) notifyWindowStateChanged:Qt::WindowFullScreen];
}
void QCocoaWindow::windowDidExitFullScreen()
{
[qnsview_cast(m_view) notifyWindowStateChanged:Qt::WindowNoState];
}
void QCocoaWindow::windowDidOrderOffScreen()
{
obscureWindow();
}
void QCocoaWindow::windowDidOrderOnScreen()
{
exposeWindow();
}
void QCocoaWindow::windowDidChangeOcclusionState()
{
// Several unit tests expect paint and/or expose events for windows that are
// sometimes (unpredictably) occluded and some unit tests depend on QWindow::isExposed.
// Don't send Expose/Obscure events when running under QTestLib.
static const bool onTestLib = qt_mac_resolveOption(false, "QT_QTESTLIB_RUNNING");
if (!onTestLib) {
if ((NSUInteger)[m_view.window occlusionState] & NSWindowOcclusionStateVisible) {
exposeWindow();
} else {
// Send Obscure events on window occlusion to stop animations.
obscureWindow();
}
}
}
void QCocoaWindow::windowDidChangeScreen()
{
if (!window())
return;
NSUInteger screenIndex = [[NSScreen screens] indexOfObject:m_view.window.screen];
if (screenIndex == NSNotFound)
return;
if (QCocoaScreen *cocoaScreen = QCocoaIntegration::instance()->screenAtIndex(screenIndex))
QWindowSystemInterface::handleWindowScreenChanged(window(), cocoaScreen->screen());
updateExposedGeometry();
}
void QCocoaWindow::windowWillClose()
{
// Close any open popups on window closing.
if (window() && !windowIsPopupType(window()->type()))
qt_closePopups();
}
// ----------------------- NSWindowDelegate callbacks -----------------------
bool QCocoaWindow::windowShouldClose()
{
qCDebug(lcQpaCocoaWindow) << "QCocoaWindow::windowShouldClose" << window();
@ -1227,12 +1374,7 @@ bool QCocoaWindow::windowShouldClose()
return accepted;
}
void QCocoaWindow::windowWillClose()
{
// Close any open popups on window closing.
if (window() && !windowIsPopupType(window()->type()))
qt_closePopups();
}
// --------------------------------------------------------------------------
void QCocoaWindow::setSynchedWindowStateFromWindow()
{
@ -1287,11 +1429,6 @@ void QCocoaWindow::recreateWindow()
bool usesNSPanel = [m_nsWindow isKindOfClass:[QNSPanel class]];
// No child QNSWindow should notify its QNSView
if (m_nsWindow && (window()->type() != Qt::ForeignWindow) && m_parentCocoaWindow && !oldParentCocoaWindow)
[[NSNotificationCenter defaultCenter] removeObserver:m_view
name:nil object:m_nsWindow];
// Remove current window (if any)
if ((m_nsWindow && !needsNSWindow) || (usesNSPanel != shouldUseNSPanel())) {
[m_nsWindow closeAndRelease];
@ -1305,14 +1442,6 @@ void QCocoaWindow::recreateWindow()
if (noPreviousWindow)
m_nsWindow = createNSWindow();
// Only non-child QNSWindows should notify their QNSViews
// (but don't register more than once).
if ((window()->type() != Qt::ForeignWindow) && (noPreviousWindow || (wasNSWindowChild && !m_isNSWindowChild)))
[[NSNotificationCenter defaultCenter] addObserver:m_view
selector:@selector(windowNotification:)
name:nil // Get all notifications
object:m_nsWindow];
if (oldParentCocoaWindow) {
if (!m_isNSWindowChild || oldParentCocoaWindow != m_parentCocoaWindow)
oldParentCocoaWindow->removeChildWindow(this);

View File

@ -102,7 +102,6 @@ Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper));
- (void)drawBackingStoreUsingCoreGraphics:(NSRect)dirtyRect;
- (void)updateGeometry;
- (void)notifyWindowStateChanged:(Qt::WindowState)newState;
- (void)windowNotification : (NSNotification *) windowNotification;
- (void)notifyWindowWillZoom:(BOOL)willZoom;
- (void)textInputContextKeyboardSelectionDidChangeNotification : (NSNotification *) textInputContextKeyboardSelectionDidChangeNotification;
- (void)viewDidHide;
@ -153,6 +152,11 @@ Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper));
@end
@interface QT_MANGLE_NAMESPACE(QNSView) (QtExtras)
@property (nonatomic, readonly) QCocoaWindow *platformWindow;
@property (nonatomic, readonly) BOOL isMenuView;
@end
QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSView);
#endif //QNSVIEW_H

View File

@ -281,22 +281,6 @@ static bool _q_dontOverrideCtrlLMB = false;
m_backingStore = Q_NULLPTR;
}
- (void)viewWillMoveToWindow:(NSWindow *)newWindow
{
// ### Merge "normal" window code path with this one for 5.1.
if (!(m_platformWindow->window()->type() & Qt::SubWindow))
return;
if (newWindow) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowNotification:)
name:nil // Get all notifications
object:newWindow];
}
if ([self window])
[[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:[self window]];
}
- (QWindow *)topLevelWindow
{
QWindow *focusWindow = m_platformWindow->window();
@ -395,64 +379,6 @@ static bool _q_dontOverrideCtrlLMB = false;
m_platformWindow->setSynchedWindowStateFromWindow();
}
- (void)windowNotification : (NSNotification *) windowNotification
{
//qDebug() << "windowNotification" << QString::fromNSString([windowNotification name]);
NSString *notificationName = [windowNotification name];
if (notificationName == NSWindowDidBecomeKeyNotification) {
if (!m_platformWindow->windowIsPopupType() && !m_isMenuView)
QWindowSystemInterface::handleWindowActivated(m_platformWindow->window());
} else if (notificationName == NSWindowDidResignKeyNotification) {
// key window will be non-nil if another window became key... do not
// set the active window to zero here, the new key window's
// NSWindowDidBecomeKeyNotification hander will change the active window
NSWindow *keyWindow = [NSApp keyWindow];
if (!keyWindow || keyWindow == windowNotification.object) {
// no new key window, go ahead and set the active window to zero
if (!m_platformWindow->windowIsPopupType() && !m_isMenuView)
QWindowSystemInterface::handleWindowActivated(0);
}
} else if (notificationName == NSWindowDidMiniaturizeNotification
|| notificationName == NSWindowDidDeminiaturizeNotification) {
Qt::WindowState newState = notificationName == NSWindowDidMiniaturizeNotification ?
Qt::WindowMinimized : Qt::WindowNoState;
[self notifyWindowStateChanged:newState];
} else if ([notificationName isEqualToString: @"NSWindowDidOrderOffScreenNotification"]) {
m_platformWindow->obscureWindow();
} else if ([notificationName isEqualToString: @"NSWindowDidOrderOnScreenAndFinishAnimatingNotification"]) {
m_platformWindow->exposeWindow();
} else if ([notificationName isEqualToString:NSWindowDidChangeOcclusionStateNotification]) {
// Several unit tests expect paint and/or expose events for windows that are
// sometimes (unpredictably) occluded and some unit tests depend on QWindow::isExposed -
// don't send Expose/Obscure events when running under QTestLib.
static const bool onTestLib = qt_mac_resolveOption(false, "QT_QTESTLIB_RUNNING");
if (!onTestLib) {
if ((NSUInteger)[self.window occlusionState] & NSWindowOcclusionStateVisible) {
m_platformWindow->exposeWindow();
} else {
// Send Obscure events on window occlusion to stop animations.
m_platformWindow->obscureWindow();
}
}
} else if (notificationName == NSWindowDidChangeScreenNotification) {
if (m_platformWindow->window()) {
NSUInteger screenIndex = [[NSScreen screens] indexOfObject:self.window.screen];
if (screenIndex != NSNotFound) {
QCocoaScreen *cocoaScreen = QCocoaIntegration::instance()->screenAtIndex(screenIndex);
if (cocoaScreen)
QWindowSystemInterface::handleWindowScreenChanged(m_platformWindow->window(), cocoaScreen->screen());
m_platformWindow->updateExposedGeometry();
}
}
} else if (notificationName == NSWindowDidEnterFullScreenNotification
|| notificationName == NSWindowDidExitFullScreenNotification) {
Qt::WindowState newState = notificationName == NSWindowDidEnterFullScreenNotification ?
Qt::WindowFullScreen : Qt::WindowNoState;
[self notifyWindowStateChanged:newState];
}
}
- (void)textInputContextKeyboardSelectionDidChangeNotification : (NSNotification *) textInputContextKeyboardSelectionDidChangeNotification
{
Q_UNUSED(textInputContextKeyboardSelectionDidChangeNotification)
@ -2219,3 +2145,17 @@ static QPoint mapWindowCoordinates(QWindow *source, QWindow *target, QPoint poin
}
@end
@implementation QT_MANGLE_NAMESPACE(QNSView) (QtExtras)
- (QCocoaWindow*)platformWindow
{
return m_platformWindow.data();;
}
- (BOOL)isMenuView
{
return m_isMenuView;
}
@end

View File

@ -49,15 +49,10 @@
QCocoaWindow *m_cocoaWindow;
}
- (id)initWithQCocoaWindow: (QCocoaWindow *) cocoaWindow;
- (id)initWithQCocoaWindow:(QCocoaWindow *)cocoaWindow;
- (void)windowDidBecomeKey:(NSNotification *)notification;
- (void)windowDidResize:(NSNotification *)notification;
- (void)windowDidMove:(NSNotification *)notification;
- (void)windowWillMove:(NSNotification *)notification;
- (BOOL)windowShouldClose:(NSNotification *)notification;
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame;
- (void)windowWillClose:(NSNotification *)notification;
@end

View File

@ -45,59 +45,14 @@
@implementation QNSWindowDelegate
- (id) initWithQCocoaWindow: (QCocoaWindow *) cocoaWindow
- (id)initWithQCocoaWindow:(QCocoaWindow *)cocoaWindow
{
self = [super init];
if (self) {
if (self = [super init])
m_cocoaWindow = cocoaWindow;
}
return self;
}
- (void)windowDidBecomeKey:(NSNotification *)notification
{
Q_UNUSED(notification);
if (m_cocoaWindow->m_windowUnderMouse) {
QPointF windowPoint;
QPointF screenPoint;
[qnsview_cast(m_cocoaWindow->view()) convertFromScreen:[NSEvent mouseLocation] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
QWindowSystemInterface::handleEnterEvent(m_cocoaWindow->m_enterLeaveTargetWindow, windowPoint, screenPoint);
}
}
- (void)windowDidResize:(NSNotification *)notification
{
Q_UNUSED(notification);
if (m_cocoaWindow) {
m_cocoaWindow->windowDidResize();
}
}
- (void)windowDidEndLiveResize:(NSNotification *)notification
{
Q_UNUSED(notification);
if (m_cocoaWindow) {
m_cocoaWindow->windowDidEndLiveResize();
}
}
- (void)windowWillMove:(NSNotification *)notification
{
Q_UNUSED(notification);
if (m_cocoaWindow) {
m_cocoaWindow->windowWillMove();
}
}
- (void)windowDidMove:(NSNotification *)notification
{
Q_UNUSED(notification);
if (m_cocoaWindow) {
m_cocoaWindow->windowDidMove();
}
}
- (BOOL)windowShouldClose:(NSNotification *)notification
{
Q_UNUSED(notification);
@ -116,11 +71,4 @@
return YES;
}
- (void)windowWillClose:(NSNotification *)notification
{
Q_UNUSED(notification);
if (m_cocoaWindow)
m_cocoaWindow->windowWillClose();
}
@end