Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcocoawindow.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <AppKit/AppKit.h>
6#include <QuartzCore/QuartzCore.h>
7
8#include "qcocoawindow.h"
10#include "qcocoascreen.h"
13#ifndef QT_NO_OPENGL
15#endif
16#include "qcocoahelpers.h"
18#include "qnsview.h"
19#include "qnswindow.h"
20#include <QtCore/qfileinfo.h>
21#include <QtCore/private/qcore_mac_p.h>
22#include <qwindow.h>
23#include <private/qwindow_p.h>
24#include <qpa/qwindowsysteminterface.h>
25#include <qpa/qplatformscreen.h>
26#include <QtGui/private/qcoregraphics_p.h>
27#include <QtGui/private/qhighdpiscaling_p.h>
28#include <QtGui/private/qmetallayer_p.h>
29
30#include <QDebug>
31
32#include <vector>
33
34QT_BEGIN_NAMESPACE
35
36enum {
37 defaultWindowWidth = 160,
38 defaultWindowHeight = 160
39};
40
41Q_LOGGING_CATEGORY(lcCocoaNotifications, "qt.qpa.cocoa.notifications");
42
44{
45 static const QLatin1StringView notificationHandlerPrefix(Q_NOTIFICATION_PREFIX);
46
47 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
48
49 const QMetaObject *metaObject = QMetaType(qRegisterMetaType<QCocoaWindow*>()).metaObject();
50 Q_ASSERT(metaObject);
51
52 for (int i = 0; i < metaObject->methodCount(); ++i) {
53 QMetaMethod method = metaObject->method(i);
54 const QString methodTag = QString::fromLatin1(method.tag());
55 if (!methodTag.startsWith(notificationHandlerPrefix))
56 continue;
57
58 const QString notificationName = methodTag.mid(notificationHandlerPrefix.size());
59 [center addObserverForName:notificationName.toNSString() object:nil queue:nil
60 usingBlock:^(NSNotification *notification) {
61
62 QVarLengthArray<QCocoaWindow *, 32> cocoaWindows;
63 if ([notification.object isKindOfClass:[NSWindow class]]) {
64 NSWindow *nsWindow = notification.object;
65 for (const QWindow *window : QGuiApplication::allWindows()) {
66 if (QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(window->handle()))
67 if (cocoaWindow->nativeWindow() == nsWindow)
68 cocoaWindows += cocoaWindow;
69 }
70 } else if ([notification.object isKindOfClass:[NSView class]]) {
71 if (QNSView *qnsView = qnsview_cast(notification.object))
72 cocoaWindows += qnsView.platformWindow;
73 } else {
74 qCWarning(lcCocoaNotifications) << "Unhandled notification"
75 << notification.name << "for" << notification.object;
76 return;
77 }
78
79 if (lcCocoaNotifications().isDebugEnabled() && !cocoaWindows.isEmpty()) {
80 QVector<QCocoaWindow *> debugWindows;
81 for (QCocoaWindow *cocoaWindow : cocoaWindows)
82 debugWindows += cocoaWindow;
83 qCDebug(lcCocoaNotifications) << "Forwarding" << qPrintable(notificationName) <<
84 "to" << debugWindows;
85 }
86
87 // FIXME: Could be a foreign window, look up by iterating top level QWindows
88
89 for (QCocoaWindow *cocoaWindow : cocoaWindows) {
90 if (!method.invoke(cocoaWindow, Qt::DirectConnection)) {
91 qCWarning(lcQpaWindow) << "Failed to invoke NSNotification callback for"
92 << notification.name << "on" << cocoaWindow;
93 }
94 }
95 }];
96 }
97}
98Q_CONSTRUCTOR_FUNCTION(qRegisterNotificationCallbacks)
99
100const int QCocoaWindow::NoAlertRequest = -1;
102
103QCocoaWindow::QCocoaWindow(QWindow *win, WId nativeHandle) : QPlatformWindow(win)
104{
105 qCDebug(lcQpaWindow) << "QCocoaWindow::QCocoaWindow" << window();
106
107 if (nativeHandle) {
108 m_view = reinterpret_cast<NSView *>(nativeHandle);
109 [m_view retain];
110 }
111}
112
114{
115 qCDebug(lcQpaWindow) << "QCocoaWindow::initialize" << window();
116
117 QMacAutoReleasePool pool;
118
119 if (!m_view)
120 m_view = [[QNSView alloc] initWithCocoaWindow:this];
121
122 if (!isForeignWindow()) {
123 // Compute the initial geometry based on the geometry set on the
124 // QWindow. This geometry has already been reflected to the
125 // QPlatformWindow in the constructor, so to ensure that the
126 // resulting setGeometry call does not think the geometry has
127 // already been applied, we reset the QPlatformWindow's view
128 // of the geometry first.
129 auto initialGeometry = QPlatformWindow::initialGeometry(window(),
130 windowGeometry(), defaultWindowWidth, defaultWindowHeight);
131 QPlatformWindow::d_ptr->rect = QRect();
132 setGeometry(initialGeometry);
133
134 setMask(QHighDpi::toNativeLocalRegion(window()->mask(), window()));
135
136 m_safeAreaInsetsObserver = QMacKeyValueObserver(
137 m_view, @"safeAreaInsets", [this] {
138 // Defer to next runloop pass, so that any changes to the
139 // margins during resizing have settled down.
140 QMetaObject::invokeMethod(this, [this]{
141 updateSafeAreaMarginsIfNeeded();
142 }, Qt::QueuedConnection);
143 }, NSKeyValueObservingOptionNew);
144
145 } else {
146 // Pick up essential foreign window state
147 QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect());
148 }
149
151
152 m_initialized = true;
153}
154
155const NSNotificationName QCocoaWindowWillReleaseQNSViewNotification = @"QCocoaWindowWillReleaseQNSViewNotification";
156
158{
159 qCDebug(lcQpaWindow) << "QCocoaWindow::~QCocoaWindow" << window();
160
161 QMacAutoReleasePool pool;
162 [m_nsWindow makeFirstResponder:nil];
163 [m_nsWindow setContentView:nil];
164
165 m_safeAreaInsetsObserver = {};
166
167 // Remove from superview only if we have a Qt window parent,
168 // as we don't want to affect window container foreign windows.
169 if (QPlatformWindow::parent())
170 [m_view removeFromSuperview];
171
172 // Make sure to disconnect observer in all case if view is valid
173 // to avoid notifications received when deleting when using Qt::AA_NativeWindows attribute
175 [[NSNotificationCenter defaultCenter] removeObserver:m_view];
176
177#if QT_CONFIG(vulkan)
178 if (QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance()) {
179 auto vulcanInstance = cocoaIntegration->getCocoaVulkanInstance();
180 if (vulcanInstance)
181 vulcanInstance->destroySurface(m_vulkanSurface);
182 }
183#endif
184
185 // Must send notification before calling release, as doing it from
186 // [QNSView dealloc] would mean that any weak references to the view
187 // would already return nil.
188 [NSNotificationCenter.defaultCenter
189 postNotificationName:QCocoaWindowWillReleaseQNSViewNotification
190 object:m_view];
191
192 [m_view release];
193 [m_nsWindow closeAndRelease];
194
195 // Disposing of the view and window should have resulted in an
196 // expose event with isExposed=false, but just in case we try
197 // to stop the display link here as well.
198 static_cast<QCocoaScreen *>(screen())->maybeStopDisplayLink();
199}
200
202{
203 auto format = window()->requestedFormat();
204 if (auto *view = qnsview_cast(m_view); view.colorSpace) {
205 auto colorSpace = QColorSpace::fromIccProfile(QByteArray::fromNSData(view.colorSpace.ICCProfileData));
206 if (!colorSpace.isValid()) {
207 qCWarning(lcQpaWindow) << "Failed to parse ICC profile for" << view.colorSpace
208 << "with ICC data" << view.colorSpace.ICCProfileData;
209 }
210 format.setColorSpace(colorSpace);
211 }
212 return format;
213}
214
215void QCocoaWindow::setGeometry(const QRect &rectIn)
216{
217 qCDebug(lcQpaWindow) << "QCocoaWindow::setGeometry" << window() << rectIn;
218
219 QScopedValueRollback inSetGeometry(m_inSetGeometry, true);
220
221 QRect rect = rectIn;
222 // This means it is a call from QWindow::setFramePosition() and
223 // the coordinates include the frame (size is still the contents rectangle).
224 if (qt_window_private(const_cast<QWindow *>(window()))->positionPolicy
225 == QWindowPrivate::WindowFrameInclusive) {
226 const QMargins margins = frameMargins();
227 rect.moveTopLeft(rect.topLeft() + QPoint(margins.left(), margins.top()));
228 }
229
230 setCocoaGeometry(rect);
231}
232
234{
235 return ![m_view isKindOfClass:[QNSView class]];
236}
237
239{
240 // QWindows that are embedded in a NSView hierarchy may be considered
241 // top-level from Qt's point of view but are not from Cocoa's point
242 // of view. Embedded QWindows get global (screen) geometry.
243 if (isEmbedded()) {
244 NSPoint windowPoint = [m_view convertPoint:NSMakePoint(0, 0) toView:nil];
245 NSRect screenRect = [[m_view window] convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
246 NSPoint screenPoint = screenRect.origin;
247 QPoint position = QCocoaScreen::mapFromNative(screenPoint).toPoint();
248 QSize size = QRectF::fromCGRect(NSRectToCGRect([m_view bounds])).toRect().size();
249 return QRect(position, size);
250 }
251
252 return QPlatformWindow::geometry();
253}
254
255/*!
256 \brief the geometry of the window as it will appear when shown as
257 a normal (not maximized or full screen) top-level window.
258
259 For child windows this property always holds an empty rectangle.
260
261 \sa QWidget::normalGeometry()
262*/
264{
265 if (!isContentView())
266 return QRect();
267
268 // We only persist the normal the geometry when going into
269 // fullscreen and maximized states. For all other cases we
270 // can just report the geometry as is.
271
272 if (!(windowState() & (Qt::WindowFullScreen | Qt::WindowMaximized)))
273 return geometry();
274
275 return m_normalGeometry;
276}
277
279{
280 if (!isContentView())
281 return;
282
283 if (windowState() != Qt::WindowNoState)
284 return;
285
286 m_normalGeometry = geometry();
287}
288
289void QCocoaWindow::setCocoaGeometry(const QRect &rect)
290{
291 qCDebug(lcQpaWindow) << "QCocoaWindow::setCocoaGeometry" << window() << rect;
292 QMacAutoReleasePool pool;
293
294 QPlatformWindow::setGeometry(rect);
295
296 if (isEmbedded()) {
297 if (!isForeignWindow()) {
298 [m_view setFrame:NSMakeRect(0, 0, rect.width(), rect.height())];
299 }
300 return;
301 }
302
303 if (isContentView()) {
304 NSRect bounds = QCocoaScreen::mapToNative(rect);
305 [m_view.window setFrame:[m_view.window frameRectForContentRect:bounds] display:YES animate:NO];
306 } else {
307 m_view.frame = QCocoaWindow::mapToNative(rect, m_view.superview);
308 }
309
310 // will call QPlatformWindow::setGeometry(rect) during resize confirmation (see qnsview.mm)
311}
312
314{
315 QMacAutoReleasePool pool;
316
317 // The safe area of the view reflects the area not covered by navigation
318 // bars, tab bars, toolbars, and other ancestor views that might obscure
319 // the current view (by setting additionalSafeAreaInsets). If the window
320 // uses NSWindowStyleMaskFullSizeContentView this also includes the area
321 // of the view covered by the title bar.
322 QMarginsF viewSafeAreaMargins = {
323 m_view.safeAreaInsets.left,
324 m_view.safeAreaInsets.top,
325 m_view.safeAreaInsets.right,
326 m_view.safeAreaInsets.bottom
327 };
328
329 // The screen's safe area insets represent the distances from the screen's
330 // edges at which content isn't obscured. The view's safe area margins do
331 // not include the screen's insets automatically, so we need to manually
332 // merge them.
333 auto screenRect = m_view.window.screen.frame;
334 auto screenInsets = m_view.window.screen.safeAreaInsets;
335 auto screenSafeArea = QCocoaScreen::mapFromNative(NSMakeRect(
336 NSMinX(screenRect) + screenInsets.left,
337 NSMinY(screenRect) + screenInsets.bottom, // Non-flipped
338 NSWidth(screenRect) - screenInsets.left - screenInsets.right,
339 NSHeight(screenRect) - screenInsets.top - screenInsets.bottom
340 ));
341
342 auto screenRelativeViewBounds = QCocoaScreen::mapFromNative(
343 [m_view.window convertRectToScreen:
344 [m_view convertRect:m_view.bounds toView:nil]]
345 );
346
347 // The margins are relative to the screen the window is on.
348 // Note that we do not want represent the area outside of the
349 // screen as being outside of the safe area.
350 QMarginsF screenSafeAreaMargins = {
351 qMin(screenSafeArea.left() - screenRelativeViewBounds.left(), screenInsets.left),
352 qMin(screenSafeArea.top() - screenRelativeViewBounds.top(), screenInsets.top),
353 qMin(screenRelativeViewBounds.right() - screenSafeArea.right(), screenInsets.right),
354 qMin(screenRelativeViewBounds.bottom() - screenSafeArea.bottom(), screenInsets.bottom)
355 };
356
357 return (screenSafeAreaMargins | viewSafeAreaMargins).toMargins();
358}
359
361{
362 if (safeAreaMargins() != m_lastReportedSafeAreaMargins) {
363 m_lastReportedSafeAreaMargins = safeAreaMargins();
365 }
366}
367
369{
370 switch (NSApp.currentEvent.type) {
371 case NSEventTypeLeftMouseDown:
372 case NSEventTypeRightMouseDown:
373 case NSEventTypeOtherMouseDown:
374 case NSEventTypeMouseMoved:
375 case NSEventTypeLeftMouseDragged:
376 case NSEventTypeRightMouseDragged:
377 case NSEventTypeOtherMouseDragged:
378 // The documentation only describes starting a system move
379 // based on mouse down events, but move events also work.
380 [m_view.window performWindowDragWithEvent:NSApp.currentEvent];
381 return true;
382 default:
383 return false;
384 }
385}
386
387void QCocoaWindow::setVisible(bool visible)
388{
389 qCDebug(lcQpaWindow) << "QCocoaWindow::setVisible" << window() << visible;
390
391 // Our implementation of setVisible below is not idempotent, as for
392 // modal windows it calls beginSheet/endSheet or starts/ends modal
393 // sessions. However we can't simply guard for m_view.hidden already
394 // having the right state, as the behavior of this function differs
395 // based on whether the window has been initialized or not, as
396 // handleGeometryChange will bail out if the window is still
397 // initializing. Since we know we'll get a second setVisible
398 // call after creation, we can check for that case specifically,
399 // which means we can then safely guard on m_view.hidden changing.
400
401 if (!m_initialized) {
402 qCDebug(lcQpaWindow) << "Window still initializing, skipping setting visibility";
403 return; // We'll get another setVisible call after create is done
404 }
405
406 if (visible == !m_view.hidden && (!isContentView() || visible == m_view.window.visible)) {
407 qCDebug(lcQpaWindow) << "No change in visible status. Ignoring.";
408 return;
409 }
410
411 if (m_inSetVisible) {
412 qCWarning(lcQpaWindow) << "Already setting window visible!";
413 return;
414 }
415
416 QScopedValueRollback<bool> rollback(m_inSetVisible, true);
417
418 QMacAutoReleasePool pool;
419 QCocoaWindow *parentCocoaWindow = nullptr;
420 if (window()->transientParent())
421 parentCocoaWindow = static_cast<QCocoaWindow *>(window()->transientParent()->handle());
422
423 auto eventDispatcher = [] {
424 return static_cast<QCocoaEventDispatcherPrivate *>(QObjectPrivate::get(qApp->eventDispatcher()));
425 };
426
427 if (visible) {
428 // We need to recreate if the modality has changed as the style mask will need updating
430
431 // We didn't send geometry changes during creation, as that would have confused
432 // Qt, which expects a show-event to be sent before any resize events. But now
433 // that the window is made visible, we know that the show-event has been sent
434 // so we can send the geometry change. FIXME: Get rid of this workaround.
436
437 if (parentCocoaWindow) {
438 // The parent window might have moved while this window was hidden,
439 // update the window geometry if there is a parent.
440 setGeometry(windowGeometry());
441
442 if (window()->type() == Qt::Popup) {
443 // QTBUG-30266: a window should not be resizable while a transient popup is open
444 // Since this isn't a native popup, the window manager doesn't close the popup when you click outside
445 NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
446 NSUInteger parentStyleMask = nativeParentWindow.styleMask;
447 if ((m_resizableTransientParent = (parentStyleMask & NSWindowStyleMaskResizable))
448 && !(nativeParentWindow.styleMask & NSWindowStyleMaskFullScreen))
449 nativeParentWindow.styleMask &= ~NSWindowStyleMaskResizable;
450 }
451
452 }
453
454 // Make the NSView visible first, before showing the NSWindow (in case of top level windows)
455 m_view.hidden = NO;
456
457 if (isContentView()) {
458 QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents);
459
460 // setWindowState might have been called while the window was hidden and
461 // will not change the NSWindow state in that case. Sync up here:
462 applyWindowState(window()->windowStates());
463
464 if (window()->windowState() != Qt::WindowMinimized) {
465 if (parentCocoaWindow && (window()->modality() == Qt::WindowModal || window()->type() == Qt::Sheet)) {
466 // Show the window as a sheet
467 NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
468 if (!nativeParentWindow.attachedSheet)
469 [nativeParentWindow beginSheet:m_view.window completionHandler:nil];
470 else
471 [nativeParentWindow beginCriticalSheet:m_view.window completionHandler:nil];
472 } else if (window()->modality() == Qt::ApplicationModal) {
473 // Show the window as application modal
474 eventDispatcher()->beginModalSession(window());
475 } else if (m_view.window.canBecomeKeyWindow) {
476 bool shouldBecomeKeyNow = !NSApp.modalWindow
477 || m_view.window.worksWhenModal
478 || !NSApp.modalWindow.visible;
479
480 // Panels with becomesKeyOnlyIfNeeded set should not activate until a view
481 // with needsPanelToBecomeKey, for example a line edit, is clicked.
482 if ([m_view.window isKindOfClass:[NSPanel class]])
483 shouldBecomeKeyNow &= !(static_cast<NSPanel*>(m_view.window).becomesKeyOnlyIfNeeded);
484
485 if (shouldBecomeKeyNow)
486 [m_view.window makeKeyAndOrderFront:nil];
487 else
488 [m_view.window orderFront:nil];
489 } else {
490 [m_view.window orderFront:nil];
491 }
492 }
493 }
494 } else {
495 // Window not visible, hide it
496 if (isContentView()) {
497 if (eventDispatcher()->hasModalSession())
498 eventDispatcher()->endModalSession(window());
499 else if ([m_view.window isSheet])
500 [m_view.window.sheetParent endSheet:m_view.window];
501
502 // Note: We do not guard the order out by checking NSWindow.visible, as AppKit will
503 // in some cases, such as when hiding the application, order out and make a window
504 // invisible, but keep it in a list of "hidden windows", that it then restores again
505 // when the application is unhidden. We need to call orderOut explicitly, to bring
506 // the window out of this "hidden list".
507 [m_view.window orderOut:nil];
508
509 if (m_view.window == [NSApp keyWindow] && !eventDispatcher()->hasModalSession()) {
510 // Probably because we call runModalSession: outside [NSApp run] in QCocoaEventDispatcher
511 // (e.g., when show()-ing a modal QDialog instead of exec()-ing it), it can happen that
512 // the current NSWindow is still key after being ordered out. Then, after checking we
513 // don't have any other modal session left, it's safe to make the main window key again.
514 NSWindow *mainWindow = [NSApp mainWindow];
515 if (mainWindow && [mainWindow canBecomeKeyWindow])
516 [mainWindow makeKeyWindow];
517 }
518 }
519
520 // AppKit will in some cases set up the key view loop for child views, even if we
521 // don't set autorecalculatesKeyViewLoop, nor call recalculateKeyViewLoop ourselves.
522 // When a child window is promoted to a top level, AppKit will maintain the key view
523 // loop between the views, even if these views now cross NSWindows, even after we
524 // explicitly call recalculateKeyViewLoop. When the top level is then hidden, AppKit
525 // will complain when -[NSView _setHidden:setNeedsDisplay:] tries to transfer first
526 // responder by reading the nextValidKeyView, and it turns out to live in a different
527 // window. We mitigate this by a last second reset of the first responder, which is
528 // what AppKit also falls back to. It's unclear if the original situation of views
529 // having their nextKeyView pointing to views in other windows is kosher or not.
530 if (m_view.window.firstResponder == m_view && m_view.nextValidKeyView
531 && m_view.nextValidKeyView.window != m_view.window) {
532 qCDebug(lcQpaWindow) << "Detected nextValidKeyView" << m_view.nextValidKeyView
533 << "in different window" << m_view.nextValidKeyView.window
534 << "Resetting" << m_view.window << "first responder to nil.";
535 [m_view.window makeFirstResponder:nil];
536 }
537
538 m_view.hidden = YES;
539
540 if (parentCocoaWindow && window()->type() == Qt::Popup) {
541 NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
542 if (m_resizableTransientParent
543 && !(nativeParentWindow.styleMask & NSWindowStyleMaskFullScreen))
544 // A window should not be resizable while a transient popup is open
545 nativeParentWindow.styleMask |= NSWindowStyleMaskResizable;
546 }
547 }
548}
549
550NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags)
551{
552 Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
553
554 NSInteger windowLevel = NSNormalWindowLevel;
555
556 if (type == Qt::Tool)
557 windowLevel = NSFloatingWindowLevel;
558 else if ((type & Qt::Popup) == Qt::Popup)
559 windowLevel = NSPopUpMenuWindowLevel;
560
561 // StayOnTop window should appear above Tool windows.
562 if (flags & Qt::WindowStaysOnTopHint)
563 windowLevel = NSModalPanelWindowLevel;
564 // Tooltips should appear above StayOnTop windows.
565 if (type == Qt::ToolTip)
566 windowLevel = NSScreenSaverWindowLevel;
567
568 auto *transientParent = window()->transientParent();
569 if (transientParent && transientParent->handle()) {
570 // We try to keep windows in at least the same window level as
571 // their transient parent. Unfortunately this only works when the
572 // window is created. If the window level changes after that, as
573 // a result of a call to setWindowFlags, or by changing the level
574 // of the native window, we will not pick this up, and the window
575 // will be left behind (or in a different window level than) its
576 // parent. We could KVO-observe the window level of our transient
577 // parent, but that requires us to know when the parent goes away
578 // so that we can unregister the observation before the parent is
579 // dealloced, something we can't do for generic NSWindows. Another
580 // way would be to override [NSWindow setLevel:] and notify child
581 // windows about the change, but that doesn't work for foreign
582 // windows, which can still be transient parents via fromWinId().
583 // One area where this problem is apparent is when AppKit tweaks
584 // the window level of modal windows during application activation
585 // and deactivation. Since we don't pick up on these window level
586 // changes in a generic way, we need to add logic explicitly to
587 // re-evaluate the window level after AppKit has done its tweaks.
588
589 auto *transientCocoaWindow = static_cast<QCocoaWindow *>(transientParent->handle());
590 auto *nsWindow = transientCocoaWindow->nativeWindow();
591
592 // We only upgrade the window level for "special" windows, to work
593 // around Qt Widgets Designer parenting the designer windows to the widget
594 // palette window (QTBUG-31779). This should be fixed in designer.
595 if (type != Qt::Window)
596 windowLevel = qMax(windowLevel, nsWindow.level);
597 }
598
599 return windowLevel;
600}
601
602NSUInteger QCocoaWindow::windowStyleMask(Qt::WindowFlags flags)
603{
604 const Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
605
606 // Determine initial style mask based on whether the window should
607 // have a frame and title or not. The NSWindowStyleMaskBorderless
608 // and NSWindowStyleMaskTitled styles are mutually exclusive, with
609 // values of 0 and 1 correspondingly.
610 NSUInteger styleMask = [&]{
611 // Honor explicit requests for borderless windows
612 if (flags & Qt::FramelessWindowHint)
613 return NSWindowStyleMaskBorderless;
614
615 // Popup windows should always be borderless
616 if (windowIsPopupType(type))
617 return NSWindowStyleMaskBorderless;
618
619 if (flags & Qt::CustomizeWindowHint) {
620 // CustomizeWindowHint turns off the default window title hints,
621 // so the choice is then up to the user via Qt::WindowTitleHint.
622 return flags & Qt::WindowTitleHint
623 ? NSWindowStyleMaskTitled
624 : NSWindowStyleMaskBorderless;
625 } else {
626 // Otherwise, default to using titled windows
627 return NSWindowStyleMaskTitled;
628 }
629 }();
630
631 // We determine which buttons to show in updateTitleBarButtons,
632 // so we can enable all the relevant style masks here to ensure
633 // that behaviors that don't involve the title bar buttons are
634 // working (for example minimizing frameless windows, or resizing
635 // windows that don't have zoom or fullscreen titlebar buttons).
636 styleMask |= NSWindowStyleMaskClosable
637 | NSWindowStyleMaskMiniaturizable;
638
639 if (type != Qt::Popup) // We only care about popups exactly.
640 styleMask |= NSWindowStyleMaskResizable;
641
642 if (type == Qt::Tool)
643 styleMask |= NSWindowStyleMaskUtilityWindow;
644
645 // FIXME (QTBUG-138829)
646 if (m_drawContentBorderGradient)
647 styleMask |= QT_IGNORE_DEPRECATIONS(NSWindowStyleMaskTexturedBackground);
648
649 if (flags & Qt::ExpandedClientAreaHint)
650 styleMask |= NSWindowStyleMaskFullSizeContentView;
651
652 // Don't wipe existing states for style flags we don't control here
653 styleMask |= (m_view.window.styleMask & (
654 NSWindowStyleMaskFullScreen
655 | NSWindowStyleMaskUnifiedTitleAndToolbar
656 | NSWindowStyleMaskDocModalWindow
657 | NSWindowStyleMaskNonactivatingPanel
658 | NSWindowStyleMaskHUDWindow));
659
660 return styleMask;
661}
662
663bool QCocoaWindow::isFixedSize() const
664{
665 return windowMinimumSize().isValid() && windowMaximumSize().isValid()
666 && windowMinimumSize() == windowMaximumSize();
667}
668
669void QCocoaWindow::updateTitleBarButtons(Qt::WindowFlags windowFlags)
670{
671 if (!isContentView())
672 return;
673
674 static constexpr std::pair<NSWindowButton, Qt::WindowFlags> buttons[] = {
675 { NSWindowCloseButton, Qt::WindowCloseButtonHint },
676 { NSWindowMiniaturizeButton, Qt::WindowMinimizeButtonHint},
677 { NSWindowZoomButton, Qt::WindowMaximizeButtonHint | Qt::WindowFullscreenButtonHint }
678 };
679
680 bool hideButtons = true;
681 for (const auto &[button, buttonHint] : buttons) {
682 // Set up Qt defaults based on window type
683 bool enabled = true;
684 if (button == NSWindowMiniaturizeButton)
685 enabled = window()->type() != Qt::Dialog;
686
687 // Let users override via CustomizeWindowHint
688 if (windowFlags & Qt::CustomizeWindowHint)
689 enabled = windowFlags & buttonHint;
690
691 // Then do some final sanitizations
692
693 if (button == NSWindowZoomButton && isFixedSize())
694 enabled = false;
695
696 // Mimic what macOS natively does for parent windows of modal
697 // sheets, which is to disable the close button, but leave the
698 // other buttons as they were.
699 if (button == NSWindowCloseButton && enabled
700 && QWindowPrivate::get(window())->blockedByModalWindow) {
701 enabled = false;
702 // If we end up having no enabled buttons, our workaround
703 // should not be a reason for hiding all of them.
704 hideButtons = false;
705 }
706
707 [m_view.window standardWindowButton:button].enabled = enabled;
708 hideButtons &= !enabled;
709 }
710
711 // Hide buttons in case we disabled all of them
712 for (const auto &[button, buttonHint] : buttons)
713 [m_view.window standardWindowButton:button].hidden = hideButtons;
714}
715
716void QCocoaWindow::setWindowFlags(Qt::WindowFlags flags)
717{
718 // Updating the window flags may affect the window's theme frame, which
719 // in the process retains and then autoreleases the NSWindow. To make
720 // sure this doesn't leave lingering releases when there is no pool in
721 // place (e.g. during main(), before exec), we add one locally here.
722 QMacAutoReleasePool pool;
723
724 if (!isContentView())
725 return;
726
727 // While setting style mask we can have handleGeometryChange calls on a content
728 // view with null geometry, reporting an invalid coordinates as a result.
729 m_inSetStyleMask = true;
730 m_view.window.styleMask = windowStyleMask(flags);
731 m_inSetStyleMask = false;
732
733 Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
734 if ((type & Qt::Popup) != Qt::Popup && (type & Qt::Dialog) != Qt::Dialog) {
735 NSWindowCollectionBehavior behavior = m_view.window.collectionBehavior;
736 const bool enableFullScreen = m_view.window.qt_fullScreen
737 || !(flags & Qt::CustomizeWindowHint)
738 || (flags & Qt::WindowFullscreenButtonHint);
739 if (enableFullScreen) {
740 behavior |= NSWindowCollectionBehaviorFullScreenPrimary;
741 behavior &= ~NSWindowCollectionBehaviorFullScreenAuxiliary;
742 } else {
743 behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
744 behavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
745 }
746 m_view.window.collectionBehavior = behavior;
747 }
748
749 // Set styleMask and collectionBehavior before applying window level, as
750 // the window level change will trigger verification of the two properties.
751 m_view.window.level = this->windowLevel(flags);
752
753 m_view.window.hasShadow = !(flags & Qt::NoDropShadowWindowHint);
754
755 if (!(flags & Qt::FramelessWindowHint))
756 setWindowTitle(window()->title());
757
758 updateTitleBarButtons(flags);
759
760 // Make window ignore mouse events if WindowTransparentForInput is set.
761 // Note that ignoresMouseEvents has a special initial state where events
762 // are ignored (passed through) based on window transparency, and that
763 // setting the property to false does not return us to that state. Instead,
764 // this makes the window capture all mouse events. Take care to only
765 // set the property if needed. FIXME: recreate window if needed or find
766 // some other way to implement WindowTransparentForInput.
767 bool ignoreMouse = flags & Qt::WindowTransparentForInput;
768 if (m_view.window.ignoresMouseEvents != ignoreMouse)
769 m_view.window.ignoresMouseEvents = ignoreMouse;
770
771 // FIXME (QTBUG-138829)
772 m_view.window.titlebarAppearsTransparent = (flags & Qt::NoTitleBarBackgroundHint)
773 || (m_view.window.styleMask & QT_IGNORE_DEPRECATIONS(NSWindowStyleMaskTexturedBackground));
774}
775
776// ----------------------- Window state -----------------------
777
778/*!
779 Changes the state of the NSWindow, going in/out of minimize/zoomed/fullscreen
780
781 When this is called from QWindow::setWindowState(), the QWindow state has not been
782 updated yet, so window()->windowState() will reflect the previous state that was
783 reported to QtGui.
784*/
785void QCocoaWindow::setWindowState(Qt::WindowStates state)
786{
787 if (window()->isVisible())
788 applyWindowState(state); // Window state set for hidden windows take effect when show() is called
789}
790
791void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState)
792{
793 if (!isContentView())
794 return;
795
796 const Qt::WindowState currentState = QWindowPrivate::effectiveState(windowState());
797 const Qt::WindowState newState = QWindowPrivate::effectiveState(requestedState);
798
799 if (newState == currentState)
800 return;
801
802 qCDebug(lcQpaWindow) << "Applying" << newState << "to" << window() << "in" << currentState;
803
804 const NSSize contentSize = m_view.frame.size;
805 if (contentSize.width <= 0 || contentSize.height <= 0) {
806 // If content view width or height is 0 then the window animations will crash so
807 // do nothing. We report the current state back to reflect the failed operation.
808 qWarning("invalid window content view size, check your window geometry");
809 handleWindowStateChanged(HandleUnconditionally);
810 return;
811 }
812
813 const NSWindow *nsWindow = m_view.window;
814
815 if (nsWindow.styleMask & NSWindowStyleMaskUtilityWindow
816 && newState & (Qt::WindowMinimized | Qt::WindowFullScreen)) {
817 qWarning() << window()->type() << "windows cannot be made" << newState;
818 handleWindowStateChanged(HandleUnconditionally);
819 return;
820 }
821
822 const id sender = nsWindow;
823
824 // First we need to exit states that can't transition directly to other states
825 switch (currentState) {
826 case Qt::WindowMinimized:
827 [nsWindow deminiaturize:sender];
828 // Deminiaturizing is not synchronous, so we need to wait for the
829 // NSWindowDidMiniaturizeNotification before continuing to apply
830 // the new state.
831 return;
832 case Qt::WindowFullScreen: {
833 toggleFullScreen();
834 // Exiting fullscreen is not synchronous, so we need to wait for the
835 // NSWindowDidExitFullScreenNotification before continuing to apply
836 // the new state.
837 return;
838 }
839 default:;
840 }
841
842 // Then we apply the new state if needed
843 if (newState == windowState())
844 return;
845
846 switch (newState) {
847 case Qt::WindowFullScreen:
848 toggleFullScreen();
849 break;
850 case Qt::WindowMaximized:
851 toggleMaximized();
852 break;
853 case Qt::WindowMinimized:
854 [nsWindow miniaturize:sender];
855 break;
856 case Qt::WindowNoState:
857 if (windowState() == Qt::WindowMaximized)
858 toggleMaximized();
859 break;
860 default:
861 Q_UNREACHABLE();
862 }
863}
864
865Qt::WindowStates QCocoaWindow::windowState() const
866{
867 Qt::WindowStates states = Qt::WindowNoState;
868 NSWindow *window = m_view.window;
869
870 if (window.miniaturized)
871 states |= Qt::WindowMinimized;
872
873 // Full screen and maximized are mutually exclusive, as macOS
874 // will report a full screen window as zoomed.
875 if (window.qt_fullScreen) {
876 states |= Qt::WindowFullScreen;
877 } else if ((window.zoomed && !isTransitioningToFullScreen())
878 || (m_lastReportedWindowState == Qt::WindowMaximized && isTransitioningToFullScreen())) {
879 states |= Qt::WindowMaximized;
880 }
881
882 // Note: We do not report Qt::WindowActive, even if isActive()
883 // is true, as QtGui does not expect this window state to be set.
884
885 return states;
886}
887
888void QCocoaWindow::toggleMaximized()
889{
890 const NSWindow *window = m_view.window;
891
892 // The NSWindow needs to be resizable, otherwise the window will
893 // not be possible to zoom back to non-zoomed state.
894 const bool wasResizable = window.styleMask & NSWindowStyleMaskResizable;
895 window.styleMask |= NSWindowStyleMaskResizable;
896
897 const id sender = window;
898 [window zoom:sender];
899
900 if (!wasResizable)
901 window.styleMask &= ~NSWindowStyleMaskResizable;
902}
903
904void QCocoaWindow::windowWillZoom()
905{
906 updateNormalGeometry();
907}
908
909void QCocoaWindow::toggleFullScreen()
910{
911 const NSWindow *window = m_view.window;
912
913 // The window needs to have the correct collection behavior for the
914 // toggleFullScreen call to have an effect. The collection behavior
915 // will be reset in windowDidEnterFullScreen/windowDidLeaveFullScreen.
916 window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
917
918 const id sender = window;
919 [window toggleFullScreen:sender];
920}
921
922void QCocoaWindow::windowWillEnterFullScreen()
923{
924 if (!isContentView())
925 return;
926
927 updateNormalGeometry();
928
929 // The NSWindow needs to be resizable, otherwise we'll end up with
930 // the normal window geometry, centered in the middle of the screen
931 // on a black background. The styleMask will be reset below.
932 m_view.window.styleMask |= NSWindowStyleMaskResizable;
933}
934
935bool QCocoaWindow::isTransitioningToFullScreen() const
936{
937 NSWindow *window = m_view.window;
938 return window.styleMask & NSWindowStyleMaskFullScreen && !window.qt_fullScreen;
939}
940
941void QCocoaWindow::windowDidEnterFullScreen()
942{
943 if (!isContentView())
944 return;
945
946 Q_ASSERT_X(m_view.window.qt_fullScreen, "QCocoaWindow",
947 "FullScreen category processes window notifications first");
948
949 // Reset to original styleMask
950 setWindowFlags(window()->flags());
951
952 handleWindowStateChanged();
953}
954
955void QCocoaWindow::windowWillExitFullScreen()
956{
957 if (!isContentView())
958 return;
959
960 // The NSWindow needs to be resizable, otherwise we'll end up with
961 // a weird zoom animation. The styleMask will be reset below.
962 m_view.window.styleMask |= NSWindowStyleMaskResizable;
963}
964
965void QCocoaWindow::windowDidExitFullScreen()
966{
967 if (!isContentView())
968 return;
969
970 Q_ASSERT_X(!m_view.window.qt_fullScreen, "QCocoaWindow",
971 "FullScreen category processes window notifications first");
972
973 // Reset to original styleMask
974 setWindowFlags(window()->flags());
975
976 Qt::WindowState requestedState = window()->windowState();
977
978 // Deliver update of QWindow state
979 handleWindowStateChanged();
980
981 if (requestedState != windowState() && requestedState != Qt::WindowFullScreen) {
982 // We were only going out of full screen as an intermediate step before
983 // progressing into the final step, so re-sync the desired state.
984 applyWindowState(requestedState);
985 }
986}
987
988void QCocoaWindow::windowDidMiniaturize()
989{
990 if (!isContentView())
991 return;
992
993 handleWindowStateChanged();
994}
995
996void QCocoaWindow::windowDidDeminiaturize()
997{
998 if (!isContentView())
999 return;
1000
1001 Qt::WindowState requestedState = window()->windowState();
1002
1003 handleWindowStateChanged();
1004
1005 if (requestedState != windowState() && requestedState != Qt::WindowMinimized) {
1006 // We were only going out of minimized as an intermediate step before
1007 // progressing into the final step, so re-sync the desired state.
1008 applyWindowState(requestedState);
1009 }
1010}
1011
1012void QCocoaWindow::handleWindowStateChanged(HandleFlags flags)
1013{
1014 Qt::WindowStates currentState = windowState();
1015 if (!(flags & HandleUnconditionally) && currentState == m_lastReportedWindowState)
1016 return;
1017
1018 qCDebug(lcQpaWindow) << "QCocoaWindow::handleWindowStateChanged" <<
1019 m_lastReportedWindowState << "-->" << currentState;
1020
1021 QWindowSystemInterface::handleWindowStateChanged<QWindowSystemInterface::SynchronousDelivery>(
1022 window(), currentState, m_lastReportedWindowState);
1023 m_lastReportedWindowState = currentState;
1024}
1025
1026// ------------------------------------------------------------
1027
1028void QCocoaWindow::setWindowTitle(const QString &title)
1029{
1030 QMacAutoReleasePool pool;
1031
1032 if (!isContentView())
1033 return;
1034
1035 m_view.window.title = title.toNSString();
1036
1037 if (title.isEmpty() && !window()->filePath().isEmpty()) {
1038 // Clearing the title should restore the default filename
1039 setWindowFilePath(window()->filePath());
1040 }
1041}
1042
1043void QCocoaWindow::setWindowFilePath(const QString &filePath)
1044{
1045 QMacAutoReleasePool pool;
1046
1047 if (!isContentView())
1048 return;
1049
1050 if (window()->title().isNull())
1051 [m_view.window setTitleWithRepresentedFilename:filePath.toNSString()];
1052 else
1053 m_view.window.representedFilename = filePath.toNSString();
1054
1055 // Changing the file path may affect icon visibility
1056 setWindowIcon(window()->icon());
1057}
1058
1059void QCocoaWindow::setWindowIcon(const QIcon &icon)
1060{
1061 QMacAutoReleasePool pool;
1062
1063 if (!isContentView())
1064 return;
1065
1066 NSButton *iconButton = [m_view.window standardWindowButton:NSWindowDocumentIconButton];
1067 if (!iconButton) {
1068 // Window icons are only supported on macOS in combination with a document filePath
1069 return;
1070 }
1071
1072 if (icon.isNull()) {
1073 iconButton.image = [NSWorkspace.sharedWorkspace iconForFile:m_view.window.representedFilename];
1074 } else {
1075 // Fall back to a size that looks good on the highest resolution screen available
1076 // for icon engines that don't have an intrinsic size (like SVG).
1077 auto fallbackSize = QSizeF::fromCGSize(iconButton.frame.size) * qGuiApp->devicePixelRatio();
1078 iconButton.image = [NSImage imageFromQIcon:icon withSize:fallbackSize.toSize()];
1079 }
1080}
1081
1082void QCocoaWindow::setAlertState(bool enabled)
1083{
1084 if (m_alertRequest == NoAlertRequest && enabled) {
1085 m_alertRequest = [NSApp requestUserAttention:NSCriticalRequest];
1086 } else if (m_alertRequest != NoAlertRequest && !enabled) {
1087 [NSApp cancelUserAttentionRequest:m_alertRequest];
1088 m_alertRequest = NoAlertRequest;
1089 }
1090}
1091
1092bool QCocoaWindow::isAlertState() const
1093{
1094 return m_alertRequest != NoAlertRequest;
1095}
1096
1097void QCocoaWindow::raise()
1098{
1099 qCDebug(lcQpaWindow) << "QCocoaWindow::raise" << window();
1100
1101 // ### handle spaces (see Qt 4 raise_sys in qwidget_mac.mm)
1102 if (isContentView()) {
1103 if (m_view.window.visible) {
1104 {
1105 // Clean up auto-released temp objects from orderFront immediately.
1106 // Failure to do so has been observed to cause leaks also beyond any outer
1107 // autorelease pool (for example around a complete QWindow
1108 // construct-show-raise-hide-delete cycle), counter to expected autoreleasepool
1109 // behavior.
1110 QMacAutoReleasePool pool;
1111 [m_view.window orderFront:m_view.window];
1112 }
1113 static bool raiseProcess = qt_mac_resolveOption(true, "QT_MAC_SET_RAISE_PROCESS");
1114 if (raiseProcess)
1115 [NSApp activateIgnoringOtherApps:YES];
1116 }
1117 } else {
1118 [m_view.superview addSubview:m_view positioned:NSWindowAbove relativeTo:nil];
1119 }
1120}
1121
1122void QCocoaWindow::lower()
1123{
1124 qCDebug(lcQpaWindow) << "QCocoaWindow::lower" << window();
1125
1126 if (isContentView()) {
1127 if (m_view.window.visible)
1128 [m_view.window orderBack:m_view.window];
1129 } else {
1130 [m_view.superview addSubview:m_view positioned:NSWindowBelow relativeTo:nil];
1131 }
1132}
1133
1134bool QCocoaWindow::isExposed() const
1135{
1136 return !m_exposedRect.isEmpty();
1137}
1138
1139bool QCocoaWindow::isEmbedded() const
1140{
1141 // Child QWindows are not embedded
1142 if (window()->parent())
1143 return false;
1144
1145 // Top-level QWindows with non-Qt NSWindows are embedded
1146 if (m_view.window)
1147 return !([m_view.window isKindOfClass:[QNSWindow class]] ||
1148 [m_view.window isKindOfClass:[QNSPanel class]]);
1149
1150 // The window has no QWindow parent but also no NSWindow,
1151 // conservatively reuturn false.
1152 return false;
1153}
1154
1155bool QCocoaWindow::isOpaque() const
1156{
1157 // OpenGL surfaces can be ordered either above(default) or below the NSWindow.
1158 // When ordering below the window must be tranclucent.
1159 static GLint openglSourfaceOrder = qt_mac_resolveOption(1, "QT_MAC_OPENGL_SURFACE_ORDER");
1160
1161 bool translucent = window()->format().alphaBufferSize() > 0
1162 || window()->opacity() < 1
1163 || !window()->mask().isEmpty()
1164 || (surface()->supportsOpenGL() && openglSourfaceOrder == -1);
1165 return !translucent;
1166}
1167
1168void QCocoaWindow::propagateSizeHints()
1169{
1170 QMacAutoReleasePool pool;
1171 if (!isContentView())
1172 return;
1173
1174 qCDebug(lcQpaWindow) << "QCocoaWindow::propagateSizeHints" << window()
1175 << "min:" << windowMinimumSize() << "max:" << windowMaximumSize()
1176 << "increment:" << windowSizeIncrement()
1177 << "base:" << windowBaseSize();
1178
1179 const NSWindow *window = m_view.window;
1180
1181 // Set the minimum content size.
1182 QSize minimumSize = windowMinimumSize();
1183 if (!minimumSize.isValid()) // minimumSize is (-1, -1) when not set. Make that (0, 0) for Cocoa.
1184 minimumSize = QSize(0, 0);
1185 window.contentMinSize = NSSizeFromCGSize(minimumSize.toCGSize());
1186
1187 // Set the maximum content size.
1188 window.contentMaxSize = NSSizeFromCGSize(windowMaximumSize().toCGSize());
1189
1190 // The window may end up with a fixed size; in this case the zoom button should be disabled.
1191 updateTitleBarButtons(this->window()->flags());
1192
1193 // sizeIncrement is observed to take values of (-1, -1) and (0, 0) for windows that should be
1194 // resizable and that have no specific size increment set. Cocoa expects (1.0, 1.0) in this case.
1195 QSize sizeIncrement = windowSizeIncrement();
1196 if (sizeIncrement.isEmpty())
1197 sizeIncrement = QSize(1, 1);
1198 window.resizeIncrements = NSSizeFromCGSize(sizeIncrement.toCGSize());
1199
1200 QRect rect = geometry();
1201 QSize baseSize = windowBaseSize();
1202 if (!baseSize.isNull() && baseSize.isValid())
1203 [window setFrame:NSMakeRect(rect.x(), rect.y(), baseSize.width(), baseSize.height()) display:YES];
1204}
1205
1206void QCocoaWindow::setOpacity(qreal level)
1207{
1208 qCDebug(lcQpaWindow) << "QCocoaWindow::setOpacity" << level;
1209 if (!isContentView())
1210 return;
1211
1212 m_view.window.alphaValue = level;
1213}
1214
1215void QCocoaWindow::setMask(const QRegion &region)
1216{
1217 qCDebug(lcQpaWindow) << "QCocoaWindow::setMask" << window() << region;
1218
1219 if (!region.isEmpty()) {
1220 QCFType<CGMutablePathRef> maskPath = CGPathCreateMutable();
1221 for (const QRect &r : region)
1222 CGPathAddRect(maskPath, nullptr, r.toCGRect());
1223 CAShapeLayer *maskLayer = [CAShapeLayer layer];
1224 maskLayer.path = maskPath;
1225 m_view.layer.mask = maskLayer;
1226 } else {
1227 m_view.layer.mask = nil;
1228 }
1229}
1230
1231bool QCocoaWindow::setKeyboardGrabEnabled(bool)
1232{
1233 return false; // FIXME (QTBUG-106597)
1234}
1235
1236bool QCocoaWindow::setMouseGrabEnabled(bool)
1237{
1238 return false; // FIXME (QTBUG-106597)
1239}
1240
1241WId QCocoaWindow::winId() const
1242{
1243 return WId(m_view);
1244}
1245
1246void QCocoaWindow::setParent(const QPlatformWindow *parentWindow)
1247{
1248 qCDebug(lcQpaWindow) << "QCocoaWindow::setParent" << window() << (parentWindow ? parentWindow->window() : 0);
1249
1250 // Recreate in case we need to get rid of a NSWindow, or create one
1251 recreateWindowIfNeeded();
1252
1253 setCocoaGeometry(geometry());
1254}
1255
1256NSView *QCocoaWindow::view() const
1257{
1258 return m_view;
1259}
1260
1261NSWindow *QCocoaWindow::nativeWindow() const
1262{
1263 return m_view.window;
1264}
1265
1266// ----------------------- NSView notifications -----------------------
1267
1268void QCocoaWindow::viewDidChangeFrame()
1269{
1270 // Note: When the view is the content view, it would seem redundant
1271 // to deliver geometry changes both from windowDidResize and this
1272 // callback, but in some cases such as when macOS native tabbed
1273 // windows are enabled we may end up with the wrong geometry in
1274 // the initial windowDidResize callback when a new tab is created.
1275 handleGeometryChange();
1276}
1277
1278/*!
1279 Callback for NSViewGlobalFrameDidChangeNotification.
1280
1281 Posted whenever an NSView object that has attached surfaces (that is,
1282 NSOpenGLContext objects) moves to a different screen, or other cases
1283 where the NSOpenGLContext object needs to be updated.
1284*/
1285void QCocoaWindow::viewDidChangeGlobalFrame()
1286{
1287 [m_view setNeedsDisplay:YES];
1288}
1289
1290// ----------------------- NSWindow notifications -----------------------
1291
1292// Note: The following notifications are delivered to every QCocoaWindow
1293// that is a child of the NSWindow that triggered the notification. Each
1294// callback should make sure to filter out notifications if they do not
1295// apply to that QCocoaWindow, e.g. if the window is not a content view.
1296
1297void QCocoaWindow::windowDidMove()
1298{
1299 if (!isContentView())
1300 return;
1301
1302 handleGeometryChange();
1303
1304 // Moving a window might bring it out of maximized state
1305 handleWindowStateChanged();
1306}
1307
1308void QCocoaWindow::windowDidResize()
1309{
1310 if (!isContentView())
1311 return;
1312
1313 handleGeometryChange();
1314
1315 if (!m_view.inLiveResize)
1316 handleWindowStateChanged();
1317}
1318
1319void QCocoaWindow::windowWillStartLiveResize()
1320{
1321 // Track live resizing for all windows, including
1322 // child windows, so we know if it's safe to update
1323 // the window unthrottled outside of the main thread.
1324 m_inLiveResize = true;
1325}
1326
1327bool QCocoaWindow::allowsIndependentThreadedRendering() const
1328{
1329 // Use member variable to track this instead of reflecting
1330 // NSView.inLiveResize directly, so it can be called from
1331 // non-main threads.
1332 return !m_inLiveResize;
1333}
1334
1335void QCocoaWindow::windowDidEndLiveResize()
1336{
1337 m_inLiveResize = false;
1338
1339 if (!isContentView())
1340 return;
1341
1342 handleWindowStateChanged();
1343}
1344
1345void QCocoaWindow::windowDidBecomeKey()
1346{
1347 // The NSWindow we're part of become key. Check if we're the first
1348 // responder, and if so, deliver focus window change to our window.
1349 if (m_view.window.firstResponder != m_view)
1350 return;
1351
1352 qCDebug(lcQpaWindow) << m_view.window << "became key window."
1353 << "Updating focus window to" << this << "with view" << m_view;
1354
1355 if (windowIsPopupType()) {
1356 qCDebug(lcQpaWindow) << "Window is popup. Skipping focus window change.";
1357 return;
1358 }
1359
1360 // See also [QNSView becomeFirstResponder]
1361 QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
1362 window(), Qt::ActiveWindowFocusReason);
1363}
1364
1365void QCocoaWindow::windowDidResignKey()
1366{
1367 // The NSWindow we're part of lost key. Check if we're the first
1368 // responder, and if so, deliver window deactivation to our window.
1369 if (m_view.window.firstResponder != m_view)
1370 return;
1371
1372 qCDebug(lcQpaWindow) << m_view.window << "resigned key window."
1373 << "Clearing focus window" << this << "with view" << m_view;
1374
1375 // Make sure popups are closed before we deliver activation changes, which are
1376 // otherwise ignored by QApplication.
1377 closeAllPopups();
1378
1379 // The current key window will be non-nil if another window became key. If that
1380 // window is a Qt window, we delay the window activation event until the didBecomeKey
1381 // notification is delivered to the active window, to ensure an atomic update.
1382 NSWindow *newKeyWindow = [NSApp keyWindow];
1383 if (newKeyWindow && newKeyWindow != m_view.window
1384 && [newKeyWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) {
1385 qCDebug(lcQpaWindow) << "New key window" << newKeyWindow
1386 << "is Qt window. Deferring focus window change.";
1387 return;
1388 }
1389
1390 // Lost key window, go ahead and set the active window to zero
1391 if (!windowIsPopupType()) {
1392 QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
1393 nullptr, Qt::ActiveWindowFocusReason);
1394 }
1395}
1396
1397void QCocoaWindow::windowDidOrderOnScreen()
1398{
1399 // The current mouse window needs to get a leave event when a popup window opens.
1400 // For modal dialogs, QGuiApplicationPrivate::showModalWindow takes care of this.
1401 if (QWindowPrivate::get(window())->isPopup()) {
1402 QWindowSystemInterface::handleLeaveEvent<QWindowSystemInterface::SynchronousDelivery>
1403 (QGuiApplicationPrivate::currentMouseWindow);
1404 }
1405
1406 [m_view setNeedsDisplay:YES];
1407}
1408
1409void QCocoaWindow::windowDidOrderOffScreen()
1410{
1411 handleExposeEvent(QRegion());
1412 // We are closing a window, so the window that is now under the mouse
1413 // might need to get an Enter event if it isn't already the mouse window.
1414 if (window()->type() & Qt::Window) {
1415 const QPointF screenPoint = QCocoaScreen::mapFromNative([NSEvent mouseLocation]);
1416 if (QWindow *windowUnderMouse = QGuiApplication::topLevelAt(screenPoint.toPoint())) {
1417 if (windowUnderMouse != QGuiApplicationPrivate::instance()->currentMouseWindow) {
1418 const auto windowPoint = windowUnderMouse->mapFromGlobal(screenPoint);
1419 // asynchronous delivery on purpose
1420 QWindowSystemInterface::handleEnterEvent<QWindowSystemInterface::AsynchronousDelivery>
1421 (windowUnderMouse, windowPoint, screenPoint);
1422 }
1423 }
1424 }
1425}
1426
1427void QCocoaWindow::windowDidChangeOcclusionState()
1428{
1429 // Note, we don't take the view's hiddenOrHasHiddenAncestor state into
1430 // account here, but instead leave that up to handleExposeEvent, just
1431 // like all the other signals that could potentially change the exposed
1432 // state of the window.
1433 bool visible = m_view.window.occlusionState & NSWindowOcclusionStateVisible;
1434 qCDebug(lcQpaWindow) << "Occlusion state of" << m_view.window << "for"
1435 << window() << "changed to" << (visible ? "visible" : "occluded");
1436
1437 if (visible)
1438 [m_view setNeedsDisplay:YES];
1439 else
1440 handleExposeEvent(QRegion());
1441}
1442
1443void QCocoaWindow::windowDidChangeScreen()
1444{
1445 if (!window())
1446 return;
1447
1448 // Note: When a window is resized to 0x0 Cocoa will report the window's screen as nil
1449 NSScreen *nsScreen = m_view.window.screen;
1450
1451 qCDebug(lcQpaWindow) << window() << "did change" << nsScreen;
1452 QCocoaScreen::updateScreens();
1453
1454 auto *previousScreen = static_cast<QCocoaScreen*>(screen());
1455 auto *currentScreen = QCocoaScreen::get(nsScreen);
1456
1457 qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen;
1458
1459 // Note: The previous screen may be the same as the current screen, either because
1460 // a) the screen was just reconfigured, which still results in AppKit sending an
1461 // NSWindowDidChangeScreenNotification, b) because the previous screen was removed,
1462 // and we ended up calling QWindow::setScreen to move the window, which doesn't
1463 // actually move the window to the new screen, or c) because we've delivered the
1464 // screen change to the top level window, which will make all the child windows
1465 // of that window report the new screen when requested via QWindow::screen().
1466 // We still need to deliver the screen change in all these cases, as the
1467 // device-pixel ratio may have changed, and needs to be delivered to all
1468 // windows, both top level and child windows.
1469
1470 QWindowSystemInterface::handleWindowScreenChanged<QWindowSystemInterface::SynchronousDelivery>(
1471 window(), currentScreen ? currentScreen->screen() : nullptr);
1472
1473 if (currentScreen && hasPendingUpdateRequest()) {
1474 // Restart display-link on new screen. We need to do this unconditionally,
1475 // since we can't rely on the previousScreen reflecting whether or not the
1476 // window actually moved from one screen to another, or just stayed on the
1477 // same screen.
1478 currentScreen->requestUpdate();
1479 }
1480}
1481
1482// ----------------------- NSWindowDelegate callbacks -----------------------
1483
1484bool QCocoaWindow::windowShouldClose()
1485{
1486 qCDebug(lcQpaWindow) << "QCocoaWindow::windowShouldClose" << window();
1487
1488 // This callback should technically only determine if the window
1489 // should (be allowed to) close, but since our QPA API to determine
1490 // that also involves actually closing the window we do both at the
1491 // same time, instead of doing the latter in windowWillClose.
1492
1493 // If the window is closed, we will release and deallocate the NSWindow.
1494 // But frames higher up in the stack might still expect the window to
1495 // be alive, since the windowShouldClose: callback is technically only
1496 // supposed to answer YES or NO. To ensure the window is still alive
1497 // we put an autorelease in the closest pool (typically the runloop).
1498 [[m_view.window retain] autorelease];
1499
1500 return QWindowSystemInterface::handleCloseEvent<QWindowSystemInterface::SynchronousDelivery>(window());
1501}
1502
1503// ----------------------------- QPA forwarding -----------------------------
1504
1505void QCocoaWindow::handleGeometryChange()
1506{
1507 QRect newGeometry;
1508 if (isContentView() && !isEmbedded()) {
1509 // Content views are positioned at (0, 0) in the window, so we resolve via the window
1510 CGRect contentRect = [m_view.window contentRectForFrameRect:m_view.window.frame];
1511
1512 // The result above is in native screen coordinates, so remap to the Qt coordinate system
1513 newGeometry = QCocoaScreen::mapFromNative(contentRect).toRect();
1514 } else {
1515 newGeometry = QCocoaWindow::mapFromNative(m_view.frame, m_view.superview).toRect();
1516 }
1517
1518 qCDebug(lcQpaWindow) << "QCocoaWindow::handleGeometryChange" << window()
1519 << "current" << geometry() << "new" << newGeometry;
1520
1521 // It can happen that the current NSWindow is nil (if we are changing styleMask
1522 // from/to borderless, and the content view is being re-parented), which results
1523 // in invalid coordinates.
1524 if (m_inSetStyleMask && !m_view.window) {
1525 qCDebug(lcQpaWindow) << "Lacking window during style mask update, ignoring geometry change";
1526 return;
1527 }
1528
1529 // Prevent geometry change during initialization, as that will result
1530 // in a resize event, and Qt expects those to come after the show event.
1531 // FIXME: Remove once we've clarified the Qt behavior for this.
1532 if (!m_initialized) {
1533 // But update the QPlatformWindow reality
1534 QPlatformWindow::setGeometry(newGeometry);
1535 qCDebug(lcQpaWindow) << "Window still initializing, skipping event";
1536 return;
1537 }
1538
1539 QWindowSystemInterface::handleGeometryChange(window(), newGeometry);
1540
1541 // Changing the window geometry may affect the safe area margins
1542 updateSafeAreaMarginsIfNeeded();
1543
1544 // Guard against processing window system events during QWindow::setGeometry
1545 // calls, which Qt and Qt applications do not expect.
1546 if (!m_inSetGeometry)
1547 QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
1548}
1549
1550void QCocoaWindow::handleExposeEvent(const QRegion &region)
1551{
1552 // Ideally we'd implement isExposed() in terms of these properties,
1553 // plus the occlusionState of the NSWindow, and let the expose event
1554 // pull the exposed state out when needed. However, when the window
1555 // is first shown we receive a drawRect call where the occlusionState
1556 // of the window is still hidden, but we still want to prepare the
1557 // window for display by issuing an expose event to Qt. To work around
1558 // this we don't use the occlusionState directly, but instead base
1559 // the exposed state on the region we get in, which in the case of
1560 // a window being obscured is an empty region, and in the case of
1561 // a drawRect call is a non-null region, even if occlusionState
1562 // is still hidden. This ensures the window is prepared for display.
1563 if (m_view.window.visible && m_view.window.screen
1564 && !geometry().size().isEmpty() && !region.isEmpty()
1565 && !m_view.hiddenOrHasHiddenAncestor) {
1566 m_exposedRect = region.boundingRect();
1567 } else {
1568 m_exposedRect = QRect();
1569 }
1570
1571 qCDebug(lcQpaDrawing) << "QCocoaWindow::handleExposeEvent" << window() << region << "isExposed" << isExposed();
1572 QWindowSystemInterface::handleExposeEvent<QWindowSystemInterface::SynchronousDelivery>(window(), region);
1573
1574 if (!isExposed())
1575 static_cast<QCocoaScreen *>(screen())->maybeStopDisplayLink();
1576}
1577
1578// --------------------------------------------------------------------------
1579
1580bool QCocoaWindow::windowIsPopupType(Qt::WindowType type) const
1581{
1582 if (type == Qt::Widget)
1583 type = window()->type();
1584 if (type == Qt::Tool)
1585 return false; // Qt::Tool has the Popup bit set but isn't, at least on Mac.
1586
1587 return ((type & Qt::Popup) == Qt::Popup);
1588}
1589
1590/*!
1591 Checks if the window is the content view of its immediate NSWindow.
1592
1593 Being the content view of a NSWindow means the QWindow is
1594 the highest accessible NSView object in the window's view
1595 hierarchy.
1596
1597 This is the case if the QWindow is a top level window.
1598*/
1599bool QCocoaWindow::isContentView() const
1600{
1601 return m_view.window.contentView == m_view;
1602}
1603
1604/*!
1605 Recreates (or removes) the NSWindow for this QWindow, if needed.
1606
1607 A QWindow may need a corresponding NSWindow/NSPanel, depending on
1608 whether or not it's a top level or not, window flags, etc.
1609*/
1610void QCocoaWindow::recreateWindowIfNeeded()
1611{
1612 QMacAutoReleasePool pool;
1613
1614 QPlatformWindow *parentWindow = QPlatformWindow::parent();
1615 auto *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow);
1616
1617 QCocoaWindow *oldParentCocoaWindow = nullptr;
1618 if (QNSView *qnsView = qnsview_cast(m_view.superview))
1619 oldParentCocoaWindow = qnsView.platformWindow;
1620
1621 if (isForeignWindow()) {
1622 // A foreign window is created as such, and can never move between being
1623 // foreign and not, so we don't need to get rid of any existing NSWindows,
1624 // nor create new ones, as a foreign window is a single simple NSView.
1625 qCDebug(lcQpaWindow) << "Skipping NSWindow management for foreign window" << this;
1626
1627 // We do however need to manage the parent relationship
1628 if (parentCocoaWindow)
1629 [parentCocoaWindow->m_view addSubview:m_view];
1630 else if (oldParentCocoaWindow)
1631 [m_view removeFromSuperview];
1632
1633 return;
1634 }
1635
1636 const bool isEmbeddedView = isEmbedded();
1637 RecreationReasons recreateReason = RecreationNotNeeded;
1638
1639 if (parentWindow != oldParentCocoaWindow)
1640 recreateReason |= ParentChanged;
1641
1642 if (isEmbeddedView && m_nsWindow)
1643 recreateReason |= EmbeddedChanged;
1644
1645 if (!m_view.window)
1646 recreateReason |= MissingWindow;
1647
1648 // If the modality has changed the style mask will need updating
1649 if (m_windowModality != window()->modality())
1650 recreateReason |= WindowModalityChanged;
1651
1652 Qt::WindowType type = window()->type();
1653
1654 const bool shouldBeContentView = !parentWindow
1655 && !((type & Qt::SubWindow) == Qt::SubWindow)
1656 && !isEmbeddedView;
1657 if (isContentView() != shouldBeContentView)
1658 recreateReason |= ContentViewChanged;
1659
1660 const bool isPanel = isContentView() && [m_view.window isKindOfClass:[QNSPanel class]];
1661 const bool shouldBePanel = shouldBeContentView &&
1662 ((type & Qt::Popup) == Qt::Popup || (type & Qt::Dialog) == Qt::Dialog);
1663
1664 if (isPanel != shouldBePanel)
1665 recreateReason |= PanelChanged;
1666
1667 qCDebug(lcQpaWindow) << "QCocoaWindow::recreateWindowIfNeeded" << window() << recreateReason;
1668
1669 if (recreateReason == RecreationNotNeeded)
1670 return;
1671
1672 // Remove current window (if any)
1673 if ((isContentView() && !shouldBeContentView) || (recreateReason & (PanelChanged | EmbeddedChanged))) {
1674 if (m_nsWindow) {
1675 qCDebug(lcQpaWindow) << "Getting rid of existing window" << m_nsWindow;
1676 [m_nsWindow closeAndRelease];
1677 if (isContentView() && !isEmbeddedView) {
1678 // We explicitly disassociate m_view from the window's contentView,
1679 // as AppKit does not automatically do this in response to removing
1680 // the view from the NSThemeFrame subview list, so we might end up
1681 // with a NSWindow contentView pointing to a deallocated NSView.
1682 m_view.window.contentView = nil;
1683 }
1684 m_nsWindow = nil;
1685 }
1686 }
1687
1688 if (shouldBeContentView && !m_nsWindow) {
1689 // Move view to new NSWindow if needed
1690 auto *newWindow = createNSWindow(shouldBePanel);
1691 qCDebug(lcQpaWindow) << "Ensuring that" << m_view << "is content view for" << newWindow;
1692 [m_view setPostsFrameChangedNotifications:NO];
1693 [newWindow setContentView:m_view];
1694 [m_view setPostsFrameChangedNotifications:YES];
1695
1696 m_nsWindow = newWindow;
1697 Q_ASSERT(m_view.window == m_nsWindow);
1698 }
1699
1700 if (parentCocoaWindow) {
1701 // Child windows have no NSWindow, re-parent to superview instead
1702 [parentCocoaWindow->m_view addSubview:m_view];
1703 }
1704}
1705
1706void QCocoaWindow::requestUpdate()
1707{
1708 qCDebug(lcQpaDrawing) << "QCocoaWindow::requestUpdate" << window()
1709 << "using" << (updatesWithDisplayLink() ? "display-link" : "timer");
1710
1711 if (updatesWithDisplayLink()) {
1712 if (!static_cast<QCocoaScreen *>(screen())->requestUpdate()) {
1713 qCDebug(lcQpaDrawing) << "Falling back to timer-based update request";
1714 QPlatformWindow::requestUpdate();
1715 }
1716 } else {
1717 // Fall back to the un-throttled timer-based callback
1718 QPlatformWindow::requestUpdate();
1719 }
1720}
1721
1722bool QCocoaWindow::updatesWithDisplayLink() const
1723{
1724 // Update via CVDisplayLink if Vsync is enabled
1725 return format().swapInterval() != 0;
1726}
1727
1728void QCocoaWindow::deliverUpdateRequest()
1729{
1730 qCDebug(lcQpaDrawing) << "Delivering update request to" << window();
1731
1732 if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(contentLayer())) {
1733 // We attempt a read lock here, so that the animation/render thread is
1734 // prioritized lower than the main thread's displayLayer processing.
1735 // Without this the two threads might fight over the next drawable,
1736 // starving the main thread's presentation of the resized layer.
1737 if (!qtMetalLayer.displayLock.tryLockForRead()) {
1738 qCDebug(lcQpaDrawing) << "Deferring update request"
1739 << "due to" << qtMetalLayer << "needing display";
1740 return;
1741 }
1742
1743 // But we don't hold the lock, as the update request can recurse
1744 // back into setNeedsDisplay, which would deadlock.
1745 qtMetalLayer.displayLock.unlock();
1746 }
1747
1748 QPlatformWindow::deliverUpdateRequest();
1749}
1750
1751void QCocoaWindow::requestActivateWindow()
1752{
1753 QMacAutoReleasePool pool;
1754 [m_view.window makeFirstResponder:m_view];
1755 [m_view.window makeKeyWindow];
1756}
1757
1758/*
1759 Closes all popups, and removes observers and monitors.
1760*/
1761void QCocoaWindow::closeAllPopups()
1762{
1763 QGuiApplicationPrivate::instance()->closeAllPopups();
1764
1765 removePopupMonitor();
1766}
1767
1768void QCocoaWindow::removePopupMonitor()
1769{
1770 if (s_globalMouseMonitor) {
1771 [NSEvent removeMonitor:s_globalMouseMonitor];
1772 s_globalMouseMonitor = nil;
1773 }
1774 if (s_applicationActivationObserver) {
1775 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:s_applicationActivationObserver];
1776 s_applicationActivationObserver = nil;
1777 }
1778}
1779
1780void QCocoaWindow::setupPopupMonitor()
1781{
1782 // we open a popup window while we are not active. None of our existing event
1783 // handlers will get called if the user now clicks anywhere outside the application
1784 // or activates another window. Use a global event monitor to watch for mouse
1785 // presses, and close popups. We also want mouse tracking in the popup to work, so
1786 // also watch for MouseMoved.
1787 if (!s_globalMouseMonitor) {
1788 // we only get LeftMouseDown events when we also set LeftMouseUp.
1789 constexpr NSEventMask mouseButtonMask = NSEventTypeLeftMouseDown | NSEventTypeLeftMouseUp
1790 | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown
1791 | NSEventMaskMouseMoved;
1792 s_globalMouseMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mouseButtonMask
1793 handler:^(NSEvent *e){
1794 if (!QGuiApplicationPrivate::instance()->activePopupWindow()) {
1795 removePopupMonitor();
1796 return;
1797 }
1798 const auto eventType = cocoaEvent2QtMouseEvent(e);
1799 if (eventType == QEvent::MouseMove) {
1800 if (s_windowUnderMouse) {
1801 QWindow *window = s_windowUnderMouse->window();
1802 const auto button = cocoaButton2QtButton(e);
1803 const auto buttons = currentlyPressedMouseButtons();
1804 const auto globalPoint = QCocoaScreen::mapFromNative(NSEvent.mouseLocation);
1805 const auto localPoint = window->mapFromGlobal(globalPoint.toPoint());
1806 QWindowSystemInterface::handleMouseEvent(window, localPoint, globalPoint,
1807 buttons, button, eventType);
1808 }
1809 } else {
1810 closeAllPopups();
1811 }
1812 }];
1813 }
1814 // The activation observer also gets called when we become active because the user clicks
1815 // into the popup. This should not close the popup, so QCocoaApplicationDelegate's
1816 // applicationDidBecomeActive implementation removes this observer.
1817 if (!s_applicationActivationObserver) {
1818 s_applicationActivationObserver = [[[NSWorkspace sharedWorkspace] notificationCenter]
1819 addObserverForName:NSWorkspaceDidActivateApplicationNotification
1820 object:nil queue:nil
1821 usingBlock:^(NSNotification *){
1822 closeAllPopups();
1823 }];
1824 }
1825}
1826
1827QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel)
1828{
1829 QMacAutoReleasePool pool;
1830
1831 Qt::WindowType type = window()->type();
1832 Qt::WindowFlags flags = window()->flags();
1833
1834 QRect rect = geometry();
1835
1836 QScreen *targetScreen = nullptr;
1837 for (QScreen *screen : QGuiApplication::screens()) {
1838 if (screen->geometry().contains(rect.topLeft())) {
1839 targetScreen = screen;
1840 break;
1841 }
1842 }
1843
1844 NSWindowStyleMask styleMask = windowStyleMask(flags);
1845
1846 if (!targetScreen) {
1847 qCWarning(lcQpaWindow) << "Window position" << rect << "outside any known screen, using primary screen";
1848 targetScreen = QGuiApplication::primaryScreen();
1849 // Unless the window is created as borderless AppKit won't find a position and
1850 // screen that's close to the requested invalid position, and will always place
1851 // the window on the primary screen.
1852 styleMask = NSWindowStyleMaskBorderless;
1853 }
1854
1855 rect.translate(-targetScreen->geometry().topLeft());
1856 auto *targetCocoaScreen = static_cast<QCocoaScreen *>(targetScreen->handle());
1857 NSRect contentRect = QCocoaScreen::mapToNative(rect, targetCocoaScreen);
1858
1859 if (targetScreen->primaryOrientation() == Qt::PortraitOrientation) {
1860 // The macOS window manager has a bug, where if a screen is rotated, it will not allow
1861 // a window to be created within the area of the screen that has a Y coordinate (I quadrant)
1862 // higher than the height of the screen in its non-rotated state (including a magic padding
1863 // of 24 points), unless the window is created with the NSWindowStyleMaskBorderless style mask.
1864 if (styleMask && (contentRect.origin.y + 24 > targetScreen->geometry().width())) {
1865 qCDebug(lcQpaWindow) << "Window positioned on portrait screen."
1866 << "Adjusting style mask during creation";
1867 styleMask = NSWindowStyleMaskBorderless;
1868 }
1869 }
1870
1871 // Create NSWindow
1872 Class windowClass = shouldBePanel ? [QNSPanel class] : [QNSWindow class];
1873 QCocoaNSWindow *nsWindow = [[windowClass alloc] initWithContentRect:contentRect
1874 // Mask will be updated in setWindowFlags if not the final mask
1875 styleMask:styleMask
1876 // Deferring window creation breaks OpenGL (the GL context is
1877 // set up before the window is shown and needs a proper window)
1878 backing:NSBackingStoreBuffered defer:NO
1879 screen:targetCocoaScreen->nativeScreen()
1880 platformWindow:this];
1881
1882 // The resulting screen can be different from the screen requested if
1883 // for example the application has been assigned to a specific display.
1884 auto resultingScreen = QCocoaScreen::get(nsWindow.screen);
1885
1886 // But may not always be resolved at this point, in which case we fall back
1887 // to the target screen. The real screen will be delivered as a screen change
1888 // when resolved as part of ordering the window on screen.
1889 if (!resultingScreen)
1890 resultingScreen = targetCocoaScreen;
1891
1892 if (resultingScreen->screen() != window()->screen()) {
1893 QWindowSystemInterface::handleWindowScreenChanged<
1894 QWindowSystemInterface::SynchronousDelivery>(window(), resultingScreen->screen());
1895 }
1896
1897 static QSharedPointer<QNSWindowDelegate> sharedDelegate([[QNSWindowDelegate alloc] init],
1898 [](QNSWindowDelegate *delegate) { [delegate release]; });
1899 nsWindow.delegate = sharedDelegate.get();
1900
1901 // Prevent Cocoa from releasing the window on close. Qt
1902 // handles the close event asynchronously and we want to
1903 // make sure that NSWindow stays valid until the
1904 // QCocoaWindow is deleted by Qt.
1905 [nsWindow setReleasedWhenClosed:NO];
1906
1907 if (alwaysShowToolWindow()) {
1908 static dispatch_once_t onceToken;
1909 dispatch_once(&onceToken, ^{
1910 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
1911 [center addObserver:[QNSWindow class] selector:@selector(applicationActivationChanged:)
1912 name:NSApplicationWillResignActiveNotification object:nil];
1913 [center addObserver:[QNSWindow class] selector:@selector(applicationActivationChanged:)
1914 name:NSApplicationWillBecomeActiveNotification object:nil];
1915 });
1916 }
1917
1918 nsWindow.restorable = NO;
1919 nsWindow.level = windowLevel(flags);
1920 nsWindow.tabbingMode = NSWindowTabbingModeDisallowed;
1921
1922 if (shouldBePanel) {
1923 // Qt::Tool windows hide on app deactivation, unless Qt::WA_MacAlwaysShowToolWindow is set
1924 nsWindow.hidesOnDeactivate = ((type & Qt::Tool) == Qt::Tool) && !alwaysShowToolWindow();
1925
1926 // Make popup windows show on the same desktop as the parent window
1927 nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary
1928 | NSWindowCollectionBehaviorMoveToActiveSpace;
1929
1930 if ((type & Qt::Popup) == Qt::Popup) {
1931 nsWindow.hasShadow = YES;
1932 nsWindow.animationBehavior = NSWindowAnimationBehaviorUtilityWindow;
1933 if (QGuiApplication::applicationState() != Qt::ApplicationActive)
1934 setupPopupMonitor();
1935 }
1936 }
1937
1938 // Persist modality so we can detect changes later on
1939 m_windowModality = QPlatformWindow::window()->modality();
1940
1941 applyContentBorderThickness(nsWindow);
1942
1943 // We propagate the view's color space granulary to both the IOSurfaces
1944 // used for QSurface::RasterSurface, as well as the CAMetalLayer used for
1945 // QSurface::MetalSurface, but for QSurface::OpenGLSurface we don't have
1946 // that option as we use NSOpenGLContext instead of CAOpenGLLayer. As a
1947 // workaround we set the NSWindow's color space, which affects GL drawing
1948 // with NSOpenGLContext as well. This does not conflict with the granular
1949 // modifications we do to each surface for raster or Metal.
1950 if (auto *qtView = qnsview_cast(m_view))
1951 nsWindow.colorSpace = qtView.colorSpace;
1952
1953 return nsWindow;
1954}
1955
1956bool QCocoaWindow::alwaysShowToolWindow() const
1957{
1958 return qt_mac_resolveOption(false, window(), "_q_macAlwaysShowToolWindow", "");
1959}
1960
1961bool QCocoaWindow::setWindowModified(bool modified)
1962{
1963 QMacAutoReleasePool pool;
1964
1965 if (!isContentView())
1966 return false;
1967
1968 m_view.window.documentEdited = modified;
1969 return true;
1970}
1971
1972void QCocoaWindow::setMenubar(QCocoaMenuBar *mb)
1973{
1974 m_menubar = mb;
1975}
1976
1977QCocoaMenuBar *QCocoaWindow::menubar() const
1978{
1979 return m_menubar;
1980}
1981
1982void QCocoaWindow::setWindowCursor(NSCursor *cursor)
1983{
1984 QMacAutoReleasePool pool;
1985
1986 // Setting a cursor in a foreign view is not supported
1987 if (isForeignWindow())
1988 return;
1989
1990 qCInfo(lcQpaMouse) << "Setting" << this << "cursor to" << cursor;
1991
1992 QNSView *view = qnsview_cast(m_view);
1993 if (cursor == view.cursor)
1994 return;
1995
1996 view.cursor = cursor;
1997
1998 // We're not using the the legacy cursor rects API to manage our
1999 // cursor, but calling this function also invalidates AppKit's
2000 // view of whether or not we need a cursorUpdate callback for
2001 // our tracking area.
2002 [m_view.window invalidateCursorRectsForView:m_view];
2003
2004 // We've informed AppKit that we need a cursorUpdate, but cursor
2005 // updates for tracking areas are deferred in some cases, such as
2006 // when the mouse is down, whereas we want a synchronous update.
2007 // To ensure an updated cursor we synthesize a cursor update event
2008 // now if the window is otherwise allowed to change the cursor.
2009 auto locationInWindow = m_view.window.mouseLocationOutsideOfEventStream;
2010 auto locationInSuperview = [m_view.superview convertPoint:locationInWindow fromView:nil];
2011 bool mouseIsOverView = [m_view hitTest:locationInSuperview] == m_view;
2012 auto utilityMask = NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskTitled;
2013 bool isUtilityWindow = (m_view.window.styleMask & utilityMask) == utilityMask;
2014 if (mouseIsOverView && (m_view.window.keyWindow || isUtilityWindow)) {
2015 qCDebug(lcQpaMouse) << "Synthesizing cursor update";
2016 [m_view cursorUpdate:[NSEvent enterExitEventWithType:NSEventTypeCursorUpdate
2017 location:locationInWindow modifierFlags:0 timestamp:0
2018 windowNumber:m_view.window.windowNumber context:nil
2019 eventNumber:0 trackingNumber:0 userData:0]];
2020 }
2021}
2022
2023void QCocoaWindow::registerTouch(bool enable)
2024{
2025 m_registerTouchCount += enable ? 1 : -1;
2026 if (enable && m_registerTouchCount == 1)
2027 m_view.allowedTouchTypes |= NSTouchTypeMaskIndirect;
2028 else if (m_registerTouchCount == 0)
2029 m_view.allowedTouchTypes &= ~NSTouchTypeMaskIndirect;
2030}
2031
2032void QCocoaWindow::registerContentBorderArea(quintptr identifier, int upper, int lower)
2033{
2034 m_contentBorderAreas.insert(identifier, BorderRange(identifier, upper, lower));
2035 applyContentBorderThickness();
2036}
2037
2038void QCocoaWindow::setContentBorderAreaEnabled(quintptr identifier, bool enable)
2039{
2040 m_enabledContentBorderAreas.insert(identifier, enable);
2041 applyContentBorderThickness();
2042}
2043
2044void QCocoaWindow::setContentBorderEnabled(bool enable)
2045{
2046 m_drawContentBorderGradient = enable;
2047 applyContentBorderThickness();
2048}
2049
2050void QCocoaWindow::applyContentBorderThickness(NSWindow *window)
2051{
2052 QMacAutoReleasePool pool;
2053
2054 if (!window && isContentView())
2055 window = m_view.window;
2056
2057 if (!window)
2058 return;
2059
2060 if (!m_drawContentBorderGradient) {
2061 // FIXME (QTBUG-138829)
2062 window.styleMask = window.styleMask & QT_IGNORE_DEPRECATIONS(~NSWindowStyleMaskTexturedBackground);
2063 setWindowFlags(QPlatformWindow::window()->flags());
2064 [window.contentView.superview setNeedsDisplay:YES];
2065 return;
2066 }
2067
2068 // Find consecutive registered border areas, starting from the top.
2069 std::vector<BorderRange> ranges(m_contentBorderAreas.cbegin(), m_contentBorderAreas.cend());
2070 std::sort(ranges.begin(), ranges.end());
2071 int effectiveTopContentBorderThickness = 0;
2072 for (BorderRange range : ranges) {
2073 // Skip disiabled ranges (typically hidden tool bars)
2074 if (!m_enabledContentBorderAreas.value(range.identifier, false))
2075 continue;
2076
2077 // Is this sub-range adjacent to or overlapping the
2078 // existing total border area range? If so merge
2079 // it into the total range,
2080 if (range.upper <= (effectiveTopContentBorderThickness + 1))
2081 effectiveTopContentBorderThickness = qMax(effectiveTopContentBorderThickness, range.lower);
2082 else
2083 break;
2084 }
2085
2086 int effectiveBottomContentBorderThickness = 0;
2087
2088 // FIXME (QTBUG-138829)
2089 [window setStyleMask:[window styleMask] | QT_IGNORE_DEPRECATIONS(NSWindowStyleMaskTexturedBackground)];
2090 setWindowFlags(QPlatformWindow::window()->flags());
2091
2092 // Setting titlebarAppearsTransparent to YES means that the border thickness has to account
2093 // for the title bar height as well, otherwise sheets will not be presented at the correct
2094 // position, which should be (title bar height + top content border size).
2095 const NSRect frameRect = window.frame;
2096 const NSRect contentRect = [window contentRectForFrameRect:frameRect];
2097 const CGFloat titlebarHeight = frameRect.size.height - contentRect.size.height;
2098 effectiveTopContentBorderThickness += titlebarHeight;
2099
2100 [window setContentBorderThickness:effectiveTopContentBorderThickness forEdge:NSMaxYEdge];
2101 [window setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
2102
2103 [window setContentBorderThickness:effectiveBottomContentBorderThickness forEdge:NSMinYEdge];
2104 [window setAutorecalculatesContentBorderThickness:NO forEdge:NSMinYEdge];
2105
2106 [[[window contentView] superview] setNeedsDisplay:YES];
2107}
2108
2109bool QCocoaWindow::testContentBorderAreaPosition(int position) const
2110{
2111 if (!m_drawContentBorderGradient || !isContentView())
2112 return false;
2113
2114 // Determine if the given y position (relative to the content area) is inside the
2115 // unified toolbar area. Note that the value returned by contentBorderThicknessForEdge
2116 // includes the title bar height; subtract it.
2117 const int contentBorderThickness = [m_view.window contentBorderThicknessForEdge:NSMaxYEdge];
2118 const NSRect frameRect = m_view.window.frame;
2119 const NSRect contentRect = [m_view.window contentRectForFrameRect:frameRect];
2120 const CGFloat titlebarHeight = frameRect.size.height - contentRect.size.height;
2121 return 0 <= position && position < (contentBorderThickness - titlebarHeight);
2122}
2123
2124qreal QCocoaWindow::devicePixelRatio() const
2125{
2126 // The documented way to observe the relationship between device-independent
2127 // and device pixels is to use one for the convertToBacking functions. Other
2128 // methods such as [NSWindow backingScaleFactor] might not give the correct
2129 // result, for example if setWantsBestResolutionOpenGLSurface is not set or
2130 // or ignored by the OpenGL driver.
2131 NSSize backingSize = [m_view convertSizeToBacking:NSMakeSize(1.0, 1.0)];
2132 return backingSize.height;
2133}
2134
2135QWindow *QCocoaWindow::childWindowAt(QPoint windowPoint)
2136{
2137 QWindow *targetWindow = window();
2138 for (QObject *child : targetWindow->children())
2139 if (QWindow *childWindow = qobject_cast<QWindow *>(child))
2140 if (QPlatformWindow *handle = childWindow->handle())
2141 if (handle->isExposed() && childWindow->geometry().contains(windowPoint))
2142 targetWindow = static_cast<QCocoaWindow*>(handle)->childWindowAt(windowPoint - childWindow->position());
2143
2144 return targetWindow;
2145}
2146
2147bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder()
2148{
2149 // This function speaks up if there's any reason
2150 // to refuse key window or first responder state.
2151
2152 if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput))
2153 return true;
2154
2155 // For application modal windows, as well as direct parent windows
2156 // of window modal windows, AppKit takes care of blocking interaction.
2157 // The Qt expectation however, is that all transient parents of a
2158 // window modal window is blocked, as reflected by QGuiApplication.
2159 // We reflect this by returning false from this function for transient
2160 // parents blocked by a modal window, but limit it to the cases not
2161 // covered by AppKit to avoid potential unwanted side effects.
2162 QWindow *modalWindow = nullptr;
2163 if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) {
2164 if (modalWindow->modality() == Qt::WindowModal && modalWindow->transientParent() != window()) {
2165 qCDebug(lcQpaWindow) << "Refusing key window for" << this << "due to being"
2166 << "blocked by" << modalWindow;
2167 return true;
2168 }
2169 }
2170
2171 if (m_inSetVisible) {
2172 QVariant showWithoutActivating = window()->property("_q_showWithoutActivating");
2173 if (showWithoutActivating.isValid() && showWithoutActivating.toBool())
2174 return true;
2175 }
2176
2177 return false;
2178}
2179
2180bool QCocoaWindow::windowEvent(QEvent *event)
2181{
2182 switch (event->type()) {
2183 case QEvent::WindowBlocked:
2184 case QEvent::WindowUnblocked:
2185 updateTitleBarButtons(window()->flags());
2186 break;
2187 default:
2188 break;
2189 }
2190
2191 return QPlatformWindow::windowEvent(event);
2192}
2193
2194QPoint QCocoaWindow::bottomLeftClippedByNSWindowOffset() const
2195{
2196 if (!m_view)
2197 return QPoint();
2198 const NSPoint origin = [m_view isFlipped] ? NSMakePoint(0, [m_view frame].size.height)
2199 : NSMakePoint(0, 0);
2200 const NSRect visibleRect = [m_view visibleRect];
2201
2202 return QPoint(visibleRect.origin.x, -visibleRect.origin.y + (origin.y - visibleRect.size.height));
2203}
2204
2205QMargins QCocoaWindow::frameMargins() const
2206{
2207 QMacAutoReleasePool pool;
2208
2209 if (!isContentView())
2210 return QMargins();
2211
2212 NSRect frameW = m_view.window.frame;
2213 NSRect frameC = [m_view.window contentRectForFrameRect:frameW];
2214
2215 return QMargins(frameW.origin.x - frameC.origin.x,
2216 (frameW.origin.y + frameW.size.height) - (frameC.origin.y + frameC.size.height),
2217 (frameW.origin.x + frameW.size.width) - (frameC.origin.x + frameC.size.width),
2218 frameC.origin.y - frameW.origin.y);
2219}
2220
2221void QCocoaWindow::setFrameStrutEventsEnabled(bool enabled)
2222{
2223 m_frameStrutEventsEnabled = enabled;
2224}
2225
2226QPoint QCocoaWindow::mapToGlobal(const QPoint &point) const
2227{
2228 NSPoint windowPoint = [m_view convertPoint:point.toCGPoint() toView:nil];
2229 NSPoint screenPoint = [m_view.window convertPointToScreen:windowPoint];
2230 return QCocoaScreen::mapFromNative(screenPoint).toPoint();
2231}
2232
2233QPoint QCocoaWindow::mapFromGlobal(const QPoint &point) const
2234{
2235 NSPoint screenPoint = QCocoaScreen::mapToNative(point);
2236 NSPoint windowPoint = [m_view.window convertPointFromScreen:screenPoint];
2237 return QPointF::fromCGPoint([m_view convertPoint:windowPoint fromView:nil]).toPoint();
2238}
2239
2240CGPoint QCocoaWindow::mapToNative(const QPointF &point, NSView *referenceView)
2241{
2242 if (!referenceView || referenceView.flipped)
2243 return point.toCGPoint();
2244 else
2245 return qt_mac_flip(point, QRectF::fromCGRect(referenceView.bounds)).toCGPoint();
2246}
2247
2248CGRect QCocoaWindow::mapToNative(const QRectF &rect, NSView *referenceView)
2249{
2250 if (!referenceView || referenceView.flipped)
2251 return rect.toCGRect();
2252 else
2253 return qt_mac_flip(rect, QRectF::fromCGRect(referenceView.bounds)).toCGRect();
2254}
2255
2256QPointF QCocoaWindow::mapFromNative(CGPoint point, NSView *referenceView)
2257{
2258 if (!referenceView || referenceView.flipped)
2259 return QPointF::fromCGPoint(point);
2260 else
2261 return qt_mac_flip(QPointF::fromCGPoint(point), QRectF::fromCGRect(referenceView.bounds));
2262}
2263
2264QRectF QCocoaWindow::mapFromNative(CGRect rect, NSView *referenceView)
2265{
2266 if (!referenceView || referenceView.flipped)
2267 return QRectF::fromCGRect(rect);
2268 else
2269 return qt_mac_flip(QRectF::fromCGRect(rect), QRectF::fromCGRect(referenceView.bounds));
2270}
2271
2272CALayer *QCocoaWindow::contentLayer() const
2273{
2274 auto *layer = m_view.layer;
2275 if (auto *containerLayer = qt_objc_cast<QContainerLayer*>(layer))
2276 layer = containerLayer.contentLayer;
2277 return layer;
2278}
2279
2280void QCocoaWindow::manageVisualEffectArea(quintptr identifier, const QRect &rect,
2281 NSVisualEffectMaterial material, NSVisualEffectBlendingMode blendMode,
2282 NSVisualEffectState activationState)
2283{
2284 if (!qt_objc_cast<QContainerLayer*>(m_view.layer)) {
2285 qCWarning(lcQpaWindow) << "Can not manage visual effect areas"
2286 << "in views without a container layer";
2287 return;
2288 }
2289
2290 qCDebug(lcQpaWindow) << "Updating visual effect area" << identifier
2291 << "to" << rect << "with material" << material << "blend mode"
2292 << blendMode << "and activation state" << activationState;
2293
2294 NSVisualEffectView *effectView = nullptr;
2295 if (m_effectViews.contains(identifier)) {
2296 effectView = m_effectViews.value(identifier);
2297 if (rect.isEmpty()) {
2298 [effectView removeFromSuperview];
2299 m_effectViews.remove(identifier);
2300 return;
2301 }
2302 } else if (!rect.isEmpty()) {
2303 effectView = [NSVisualEffectView new];
2304 // Ensure that the visual effect layer is stacked well
2305 // below our content layer (which defaults to a z of 0).
2306 effectView.wantsLayer = YES;
2307 effectView.layer.zPosition = -FLT_MAX;
2308 [m_view addSubview:effectView];
2309 m_effectViews.insert(identifier, effectView);
2310 }
2311
2312 if (!effectView)
2313 return;
2314
2315 effectView.frame = rect.toCGRect();
2316 effectView.material = material;
2317 effectView.blendingMode = blendMode;
2318 effectView.state = activationState;
2319}
2320
2321#ifndef QT_NO_DEBUG_STREAM
2322QDebug operator<<(QDebug debug, const QCocoaWindow *window)
2323{
2324 QDebugStateSaver saver(debug);
2325 debug.nospace();
2326 debug << "QCocoaWindow(" << (const void *)window;
2327 if (window)
2328 debug << ", window=" << window->window();
2329 debug << ')';
2330 return debug;
2331}
2332#endif // !QT_NO_DEBUG_STREAM
2333
2334QT_END_NAMESPACE
2335
2336#include "moc_qcocoawindow.cpp"
QMargins safeAreaMargins() const override
The safe area margins of a window represent the area that is safe to place content within,...
QRect normalGeometry() const override
the geometry of the window as it will appear when shown as a normal (not maximized or full screen) to...
bool isForeignWindow() const override
void setCocoaGeometry(const QRect &rect)
QRect geometry() const override
Returns the current geometry of a window.
void updateSafeAreaMarginsIfNeeded()
void setGeometry(const QRect &rect) override
This function is called by Qt whenever a window is moved or resized using the QWindow API.
void updateNormalGeometry()
bool isEmbedded() const override
Returns true if the window is a child of a non-Qt window.
static QPointer< QCocoaWindow > s_windowUnderMouse
bool isContentView() const
Checks if the window is the content view of its immediate NSWindow.
void recreateWindowIfNeeded()
Recreates (or removes) the NSWindow for this QWindow, if needed.
void handleGeometryChange()
void setVisible(bool visible) override
Reimplemented in subclasses to show the surface if visible is true, and hide it if visible is false.
bool startSystemMove() override
Reimplement this method to start a system move operation if the system supports it and return true to...
static const int NoAlertRequest
void initialize() override
Called as part of QWindow::create(), after constructing the window.
QSurfaceFormat format() const override
Returns the actual surface format of the window.
QRect window() const
Returns the window rectangle.
unsigned long NSUInteger
#define Q_NOTIFICATION_PREFIX
static void qRegisterNotificationCallbacks()
const NSNotificationName QCocoaWindowWillReleaseQNSViewNotification
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")