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 setFrame:NSMakeRect(rect.x(), rect.y(), rect.width(), rect.height())];
308 }
309
310 // will call QPlatformWindow::setGeometry(rect) during resize confirmation (see qnsview.mm)
311}
312
314{
315 // The safe area of the view reflects the area not covered by navigation
316 // bars, tab bars, toolbars, and other ancestor views that might obscure
317 // the current view (by setting additionalSafeAreaInsets). If the window
318 // uses NSWindowStyleMaskFullSizeContentView this also includes the area
319 // of the view covered by the title bar.
320 QMarginsF viewSafeAreaMargins = {
321 m_view.safeAreaInsets.left,
322 m_view.safeAreaInsets.top,
323 m_view.safeAreaInsets.right,
324 m_view.safeAreaInsets.bottom
325 };
326
327 // The screen's safe area insets represent the distances from the screen's
328 // edges at which content isn't obscured. The view's safe area margins do
329 // not include the screen's insets automatically, so we need to manually
330 // merge them.
331 auto screenRect = m_view.window.screen.frame;
332 auto screenInsets = m_view.window.screen.safeAreaInsets;
333 auto screenRelativeViewBounds = QCocoaScreen::mapFromNative(
334 [m_view.window convertRectToScreen:
335 [m_view convertRect:m_view.bounds toView:nil]]
336 );
337
338 // The margins are relative to the screen the window is on.
339 // Note that we do not want represent the area outside of the
340 // screen as being outside of the safe area.
341 QMarginsF screenSafeAreaMargins = {
342 screenInsets.left ?
343 qMax(0.0f, screenInsets.left - screenRelativeViewBounds.left())
344 : 0.0f,
345 screenInsets.top ?
346 qMax(0.0f, screenInsets.top - screenRelativeViewBounds.top())
347 : 0.0f,
348 screenInsets.right ?
349 qMax(0.0f, screenInsets.right
350 - (screenRect.size.width - screenRelativeViewBounds.right()))
351 : 0.0f,
352 screenInsets.bottom ?
353 qMax(0.0f, screenInsets.bottom
354 - (screenRect.size.height - screenRelativeViewBounds.bottom()))
355 : 0.0f
356 };
357
358 return (screenSafeAreaMargins | viewSafeAreaMargins).toMargins();
359}
360
362{
363 if (safeAreaMargins() != m_lastReportedSafeAreaMargins) {
364 m_lastReportedSafeAreaMargins = safeAreaMargins();
366 }
367}
368
370{
371 switch (NSApp.currentEvent.type) {
372 case NSEventTypeLeftMouseDown:
373 case NSEventTypeRightMouseDown:
374 case NSEventTypeOtherMouseDown:
375 case NSEventTypeMouseMoved:
376 case NSEventTypeLeftMouseDragged:
377 case NSEventTypeRightMouseDragged:
378 case NSEventTypeOtherMouseDragged:
379 // The documentation only describes starting a system move
380 // based on mouse down events, but move events also work.
381 [m_view.window performWindowDragWithEvent:NSApp.currentEvent];
382 return true;
383 default:
384 return false;
385 }
386}
387
388void QCocoaWindow::setVisible(bool visible)
389{
390 qCDebug(lcQpaWindow) << "QCocoaWindow::setVisible" << window() << visible;
391
392 // Our implementation of setVisible below is not idempotent, as for
393 // modal windows it calls beginSheet/endSheet or starts/ends modal
394 // sessions. However we can't simply guard for m_view.hidden already
395 // having the right state, as the behavior of this function differs
396 // based on whether the window has been initialized or not, as
397 // handleGeometryChange will bail out if the window is still
398 // initializing. Since we know we'll get a second setVisible
399 // call after creation, we can check for that case specifically,
400 // which means we can then safely guard on m_view.hidden changing.
401
402 if (!m_initialized) {
403 qCDebug(lcQpaWindow) << "Window still initializing, skipping setting visibility";
404 return; // We'll get another setVisible call after create is done
405 }
406
407 if (visible == !m_view.hidden && (!isContentView() || visible == m_view.window.visible)) {
408 qCDebug(lcQpaWindow) << "No change in visible status. Ignoring.";
409 return;
410 }
411
412 if (m_inSetVisible) {
413 qCWarning(lcQpaWindow) << "Already setting window visible!";
414 return;
415 }
416
417 QScopedValueRollback<bool> rollback(m_inSetVisible, true);
418
419 QMacAutoReleasePool pool;
420 QCocoaWindow *parentCocoaWindow = nullptr;
421 if (window()->transientParent())
422 parentCocoaWindow = static_cast<QCocoaWindow *>(window()->transientParent()->handle());
423
424 auto eventDispatcher = [] {
425 return static_cast<QCocoaEventDispatcherPrivate *>(QObjectPrivate::get(qApp->eventDispatcher()));
426 };
427
428 if (visible) {
429 // We need to recreate if the modality has changed as the style mask will need updating
431
432 // We didn't send geometry changes during creation, as that would have confused
433 // Qt, which expects a show-event to be sent before any resize events. But now
434 // that the window is made visible, we know that the show-event has been sent
435 // so we can send the geometry change. FIXME: Get rid of this workaround.
437
438 if (parentCocoaWindow) {
439 // The parent window might have moved while this window was hidden,
440 // update the window geometry if there is a parent.
441 setGeometry(windowGeometry());
442
443 if (window()->type() == Qt::Popup) {
444 // QTBUG-30266: a window should not be resizable while a transient popup is open
445 // Since this isn't a native popup, the window manager doesn't close the popup when you click outside
446 NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
447 NSUInteger parentStyleMask = nativeParentWindow.styleMask;
448 if ((m_resizableTransientParent = (parentStyleMask & NSWindowStyleMaskResizable))
449 && !(nativeParentWindow.styleMask & NSWindowStyleMaskFullScreen))
450 nativeParentWindow.styleMask &= ~NSWindowStyleMaskResizable;
451 }
452
453 }
454
455 // Make the NSView visible first, before showing the NSWindow (in case of top level windows)
456 m_view.hidden = NO;
457
458 if (isContentView()) {
459 QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents);
460
461 // setWindowState might have been called while the window was hidden and
462 // will not change the NSWindow state in that case. Sync up here:
463 applyWindowState(window()->windowStates());
464
465 if (window()->windowState() != Qt::WindowMinimized) {
466 if (parentCocoaWindow && (window()->modality() == Qt::WindowModal || window()->type() == Qt::Sheet)) {
467 // Show the window as a sheet
468 NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
469 if (!nativeParentWindow.attachedSheet)
470 [nativeParentWindow beginSheet:m_view.window completionHandler:nil];
471 else
472 [nativeParentWindow beginCriticalSheet:m_view.window completionHandler:nil];
473 } else if (window()->modality() == Qt::ApplicationModal) {
474 // Show the window as application modal
475 eventDispatcher()->beginModalSession(window());
476 } else if (m_view.window.canBecomeKeyWindow) {
477 bool shouldBecomeKeyNow = !NSApp.modalWindow
478 || m_view.window.worksWhenModal
479 || !NSApp.modalWindow.visible;
480
481 // Panels with becomesKeyOnlyIfNeeded set should not activate until a view
482 // with needsPanelToBecomeKey, for example a line edit, is clicked.
483 if ([m_view.window isKindOfClass:[NSPanel class]])
484 shouldBecomeKeyNow &= !(static_cast<NSPanel*>(m_view.window).becomesKeyOnlyIfNeeded);
485
486 if (shouldBecomeKeyNow)
487 [m_view.window makeKeyAndOrderFront:nil];
488 else
489 [m_view.window orderFront:nil];
490 } else {
491 [m_view.window orderFront:nil];
492 }
493 }
494 }
495 } else {
496 // Window not visible, hide it
497 if (isContentView()) {
498 if (eventDispatcher()->hasModalSession())
499 eventDispatcher()->endModalSession(window());
500 else if ([m_view.window isSheet])
501 [m_view.window.sheetParent endSheet:m_view.window];
502
503 // Note: We do not guard the order out by checking NSWindow.visible, as AppKit will
504 // in some cases, such as when hiding the application, order out and make a window
505 // invisible, but keep it in a list of "hidden windows", that it then restores again
506 // when the application is unhidden. We need to call orderOut explicitly, to bring
507 // the window out of this "hidden list".
508 [m_view.window orderOut:nil];
509
510 if (m_view.window == [NSApp keyWindow] && !eventDispatcher()->hasModalSession()) {
511 // Probably because we call runModalSession: outside [NSApp run] in QCocoaEventDispatcher
512 // (e.g., when show()-ing a modal QDialog instead of exec()-ing it), it can happen that
513 // the current NSWindow is still key after being ordered out. Then, after checking we
514 // don't have any other modal session left, it's safe to make the main window key again.
515 NSWindow *mainWindow = [NSApp mainWindow];
516 if (mainWindow && [mainWindow canBecomeKeyWindow])
517 [mainWindow makeKeyWindow];
518 }
519 }
520
521 // AppKit will in some cases set up the key view loop for child views, even if we
522 // don't set autorecalculatesKeyViewLoop, nor call recalculateKeyViewLoop ourselves.
523 // When a child window is promoted to a top level, AppKit will maintain the key view
524 // loop between the views, even if these views now cross NSWindows, even after we
525 // explicitly call recalculateKeyViewLoop. When the top level is then hidden, AppKit
526 // will complain when -[NSView _setHidden:setNeedsDisplay:] tries to transfer first
527 // responder by reading the nextValidKeyView, and it turns out to live in a different
528 // window. We mitigate this by a last second reset of the first responder, which is
529 // what AppKit also falls back to. It's unclear if the original situation of views
530 // having their nextKeyView pointing to views in other windows is kosher or not.
531 if (m_view.window.firstResponder == m_view && m_view.nextValidKeyView
532 && m_view.nextValidKeyView.window != m_view.window) {
533 qCDebug(lcQpaWindow) << "Detected nextValidKeyView" << m_view.nextValidKeyView
534 << "in different window" << m_view.nextValidKeyView.window
535 << "Resetting" << m_view.window << "first responder to nil.";
536 [m_view.window makeFirstResponder:nil];
537 }
538
539 m_view.hidden = YES;
540
541 if (parentCocoaWindow && window()->type() == Qt::Popup) {
542 NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow();
543 if (m_resizableTransientParent
544 && !(nativeParentWindow.styleMask & NSWindowStyleMaskFullScreen))
545 // A window should not be resizable while a transient popup is open
546 nativeParentWindow.styleMask |= NSWindowStyleMaskResizable;
547 }
548 }
549}
550
551NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags)
552{
553 Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
554
555 NSInteger windowLevel = NSNormalWindowLevel;
556
557 if (type == Qt::Tool)
558 windowLevel = NSFloatingWindowLevel;
559 else if ((type & Qt::Popup) == Qt::Popup)
560 windowLevel = NSPopUpMenuWindowLevel;
561
562 // StayOnTop window should appear above Tool windows.
563 if (flags & Qt::WindowStaysOnTopHint)
564 windowLevel = NSModalPanelWindowLevel;
565 // Tooltips should appear above StayOnTop windows.
566 if (type == Qt::ToolTip)
567 windowLevel = NSScreenSaverWindowLevel;
568
569 auto *transientParent = window()->transientParent();
570 if (transientParent && transientParent->handle()) {
571 // We try to keep windows in at least the same window level as
572 // their transient parent. Unfortunately this only works when the
573 // window is created. If the window level changes after that, as
574 // a result of a call to setWindowFlags, or by changing the level
575 // of the native window, we will not pick this up, and the window
576 // will be left behind (or in a different window level than) its
577 // parent. We could KVO-observe the window level of our transient
578 // parent, but that requires us to know when the parent goes away
579 // so that we can unregister the observation before the parent is
580 // dealloced, something we can't do for generic NSWindows. Another
581 // way would be to override [NSWindow setLevel:] and notify child
582 // windows about the change, but that doesn't work for foreign
583 // windows, which can still be transient parents via fromWinId().
584 // One area where this problem is apparent is when AppKit tweaks
585 // the window level of modal windows during application activation
586 // and deactivation. Since we don't pick up on these window level
587 // changes in a generic way, we need to add logic explicitly to
588 // re-evaluate the window level after AppKit has done its tweaks.
589
590 auto *transientCocoaWindow = static_cast<QCocoaWindow *>(transientParent->handle());
591 auto *nsWindow = transientCocoaWindow->nativeWindow();
592
593 // We only upgrade the window level for "special" windows, to work
594 // around Qt Widgets Designer parenting the designer windows to the widget
595 // palette window (QTBUG-31779). This should be fixed in designer.
596 if (type != Qt::Window)
597 windowLevel = qMax(windowLevel, nsWindow.level);
598 }
599
600 return windowLevel;
601}
602
603NSUInteger QCocoaWindow::windowStyleMask(Qt::WindowFlags flags)
604{
605 const Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
606
607 // Determine initial style mask based on whether the window should
608 // have a frame and title or not. The NSWindowStyleMaskBorderless
609 // and NSWindowStyleMaskTitled styles are mutually exclusive, with
610 // values of 0 and 1 correspondingly.
611 NSUInteger styleMask = [&]{
612 // Honor explicit requests for borderless windows
613 if (flags & Qt::FramelessWindowHint)
614 return NSWindowStyleMaskBorderless;
615
616 // Popup windows should always be borderless
617 if (windowIsPopupType(type))
618 return NSWindowStyleMaskBorderless;
619
620 if (flags & Qt::CustomizeWindowHint) {
621 // CustomizeWindowHint turns off the default window title hints,
622 // so the choice is then up to the user via Qt::WindowTitleHint.
623 return flags & Qt::WindowTitleHint
624 ? NSWindowStyleMaskTitled
625 : NSWindowStyleMaskBorderless;
626 } else {
627 // Otherwise, default to using titled windows
628 return NSWindowStyleMaskTitled;
629 }
630 }();
631
632 // We determine which buttons to show in updateTitleBarButtons,
633 // so we can enable all the relevant style masks here to ensure
634 // that behaviors that don't involve the title bar buttons are
635 // working (for example minimizing frameless windows, or resizing
636 // windows that don't have zoom or fullscreen titlebar buttons).
637 styleMask |= NSWindowStyleMaskClosable
638 | NSWindowStyleMaskMiniaturizable;
639
640 if (type != Qt::Popup) // We only care about popups exactly.
641 styleMask |= NSWindowStyleMaskResizable;
642
643 if (type == Qt::Tool)
644 styleMask |= NSWindowStyleMaskUtilityWindow;
645
646 // FIXME (QTBUG-138829)
647 if (m_drawContentBorderGradient)
648 styleMask |= QT_IGNORE_DEPRECATIONS(NSWindowStyleMaskTexturedBackground);
649
650 if (flags & Qt::ExpandedClientAreaHint)
651 styleMask |= NSWindowStyleMaskFullSizeContentView;
652
653 // Don't wipe existing states for style flags we don't control here
654 styleMask |= (m_view.window.styleMask & (
655 NSWindowStyleMaskFullScreen
656 | NSWindowStyleMaskUnifiedTitleAndToolbar
657 | NSWindowStyleMaskDocModalWindow
658 | NSWindowStyleMaskNonactivatingPanel
659 | NSWindowStyleMaskHUDWindow));
660
661 return styleMask;
662}
663
664bool QCocoaWindow::isFixedSize() const
665{
666 return windowMinimumSize().isValid() && windowMaximumSize().isValid()
667 && windowMinimumSize() == windowMaximumSize();
668}
669
670void QCocoaWindow::updateTitleBarButtons(Qt::WindowFlags windowFlags)
671{
672 if (!isContentView())
673 return;
674
675 static constexpr std::pair<NSWindowButton, Qt::WindowFlags> buttons[] = {
676 { NSWindowCloseButton, Qt::WindowCloseButtonHint },
677 { NSWindowMiniaturizeButton, Qt::WindowMinimizeButtonHint},
678 { NSWindowZoomButton, Qt::WindowMaximizeButtonHint | Qt::WindowFullscreenButtonHint }
679 };
680
681 bool hideButtons = true;
682 for (const auto &[button, buttonHint] : buttons) {
683 // Set up Qt defaults based on window type
684 bool enabled = true;
685 if (button == NSWindowMiniaturizeButton)
686 enabled = window()->type() != Qt::Dialog;
687
688 // Let users override via CustomizeWindowHint
689 if (windowFlags & Qt::CustomizeWindowHint)
690 enabled = windowFlags & buttonHint;
691
692 // Then do some final sanitizations
693
694 if (button == NSWindowZoomButton && isFixedSize())
695 enabled = false;
696
697 // Mimic what macOS natively does for parent windows of modal
698 // sheets, which is to disable the close button, but leave the
699 // other buttons as they were.
700 if (button == NSWindowCloseButton && enabled
701 && QWindowPrivate::get(window())->blockedByModalWindow) {
702 enabled = false;
703 // If we end up having no enabled buttons, our workaround
704 // should not be a reason for hiding all of them.
705 hideButtons = false;
706 }
707
708 [m_view.window standardWindowButton:button].enabled = enabled;
709 hideButtons &= !enabled;
710 }
711
712 // Hide buttons in case we disabled all of them
713 for (const auto &[button, buttonHint] : buttons)
714 [m_view.window standardWindowButton:button].hidden = hideButtons;
715}
716
717void QCocoaWindow::setWindowFlags(Qt::WindowFlags flags)
718{
719 // Updating the window flags may affect the window's theme frame, which
720 // in the process retains and then autoreleases the NSWindow. To make
721 // sure this doesn't leave lingering releases when there is no pool in
722 // place (e.g. during main(), before exec), we add one locally here.
723 QMacAutoReleasePool pool;
724
725 if (!isContentView())
726 return;
727
728 // While setting style mask we can have handleGeometryChange calls on a content
729 // view with null geometry, reporting an invalid coordinates as a result.
730 m_inSetStyleMask = true;
731 m_view.window.styleMask = windowStyleMask(flags);
732 m_inSetStyleMask = false;
733
734 Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
735 if ((type & Qt::Popup) != Qt::Popup && (type & Qt::Dialog) != Qt::Dialog) {
736 NSWindowCollectionBehavior behavior = m_view.window.collectionBehavior;
737 const bool enableFullScreen = m_view.window.qt_fullScreen
738 || !(flags & Qt::CustomizeWindowHint)
739 || (flags & Qt::WindowFullscreenButtonHint);
740 if (enableFullScreen) {
741 behavior |= NSWindowCollectionBehaviorFullScreenPrimary;
742 behavior &= ~NSWindowCollectionBehaviorFullScreenAuxiliary;
743 } else {
744 behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
745 behavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
746 }
747 m_view.window.collectionBehavior = behavior;
748 }
749
750 // Set styleMask and collectionBehavior before applying window level, as
751 // the window level change will trigger verification of the two properties.
752 m_view.window.level = this->windowLevel(flags);
753
754 m_view.window.hasShadow = !(flags & Qt::NoDropShadowWindowHint);
755
756 if (!(flags & Qt::FramelessWindowHint))
757 setWindowTitle(window()->title());
758
759 updateTitleBarButtons(flags);
760
761 // Make window ignore mouse events if WindowTransparentForInput is set.
762 // Note that ignoresMouseEvents has a special initial state where events
763 // are ignored (passed through) based on window transparency, and that
764 // setting the property to false does not return us to that state. Instead,
765 // this makes the window capture all mouse events. Take care to only
766 // set the property if needed. FIXME: recreate window if needed or find
767 // some other way to implement WindowTransparentForInput.
768 bool ignoreMouse = flags & Qt::WindowTransparentForInput;
769 if (m_view.window.ignoresMouseEvents != ignoreMouse)
770 m_view.window.ignoresMouseEvents = ignoreMouse;
771
772 // FIXME (QTBUG-138829)
773 m_view.window.titlebarAppearsTransparent = (flags & Qt::NoTitleBarBackgroundHint)
774 || (m_view.window.styleMask & QT_IGNORE_DEPRECATIONS(NSWindowStyleMaskTexturedBackground));
775}
776
777// ----------------------- Window state -----------------------
778
779/*!
780 Changes the state of the NSWindow, going in/out of minimize/zoomed/fullscreen
781
782 When this is called from QWindow::setWindowState(), the QWindow state has not been
783 updated yet, so window()->windowState() will reflect the previous state that was
784 reported to QtGui.
785*/
786void QCocoaWindow::setWindowState(Qt::WindowStates state)
787{
788 if (window()->isVisible())
789 applyWindowState(state); // Window state set for hidden windows take effect when show() is called
790}
791
792void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState)
793{
794 if (!isContentView())
795 return;
796
797 const Qt::WindowState currentState = QWindowPrivate::effectiveState(windowState());
798 const Qt::WindowState newState = QWindowPrivate::effectiveState(requestedState);
799
800 if (newState == currentState)
801 return;
802
803 qCDebug(lcQpaWindow) << "Applying" << newState << "to" << window() << "in" << currentState;
804
805 const NSSize contentSize = m_view.frame.size;
806 if (contentSize.width <= 0 || contentSize.height <= 0) {
807 // If content view width or height is 0 then the window animations will crash so
808 // do nothing. We report the current state back to reflect the failed operation.
809 qWarning("invalid window content view size, check your window geometry");
810 handleWindowStateChanged(HandleUnconditionally);
811 return;
812 }
813
814 const NSWindow *nsWindow = m_view.window;
815
816 if (nsWindow.styleMask & NSWindowStyleMaskUtilityWindow
817 && newState & (Qt::WindowMinimized | Qt::WindowFullScreen)) {
818 qWarning() << window()->type() << "windows cannot be made" << newState;
819 handleWindowStateChanged(HandleUnconditionally);
820 return;
821 }
822
823 const id sender = nsWindow;
824
825 // First we need to exit states that can't transition directly to other states
826 switch (currentState) {
827 case Qt::WindowMinimized:
828 [nsWindow deminiaturize:sender];
829 // Deminiaturizing is not synchronous, so we need to wait for the
830 // NSWindowDidMiniaturizeNotification before continuing to apply
831 // the new state.
832 return;
833 case Qt::WindowFullScreen: {
834 toggleFullScreen();
835 // Exiting fullscreen is not synchronous, so we need to wait for the
836 // NSWindowDidExitFullScreenNotification before continuing to apply
837 // the new state.
838 return;
839 }
840 default:;
841 }
842
843 // Then we apply the new state if needed
844 if (newState == windowState())
845 return;
846
847 switch (newState) {
848 case Qt::WindowFullScreen:
849 toggleFullScreen();
850 break;
851 case Qt::WindowMaximized:
852 toggleMaximized();
853 break;
854 case Qt::WindowMinimized:
855 [nsWindow miniaturize:sender];
856 break;
857 case Qt::WindowNoState:
858 if (windowState() == Qt::WindowMaximized)
859 toggleMaximized();
860 break;
861 default:
862 Q_UNREACHABLE();
863 }
864}
865
866Qt::WindowStates QCocoaWindow::windowState() const
867{
868 Qt::WindowStates states = Qt::WindowNoState;
869 NSWindow *window = m_view.window;
870
871 if (window.miniaturized)
872 states |= Qt::WindowMinimized;
873
874 // Full screen and maximized are mutually exclusive, as macOS
875 // will report a full screen window as zoomed.
876 if (window.qt_fullScreen) {
877 states |= Qt::WindowFullScreen;
878 } else if ((window.zoomed && !isTransitioningToFullScreen())
879 || (m_lastReportedWindowState == Qt::WindowMaximized && isTransitioningToFullScreen())) {
880 states |= Qt::WindowMaximized;
881 }
882
883 // Note: We do not report Qt::WindowActive, even if isActive()
884 // is true, as QtGui does not expect this window state to be set.
885
886 return states;
887}
888
889void QCocoaWindow::toggleMaximized()
890{
891 const NSWindow *window = m_view.window;
892
893 // The NSWindow needs to be resizable, otherwise the window will
894 // not be possible to zoom back to non-zoomed state.
895 const bool wasResizable = window.styleMask & NSWindowStyleMaskResizable;
896 window.styleMask |= NSWindowStyleMaskResizable;
897
898 const id sender = window;
899 [window zoom:sender];
900
901 if (!wasResizable)
902 window.styleMask &= ~NSWindowStyleMaskResizable;
903}
904
905void QCocoaWindow::windowWillZoom()
906{
907 updateNormalGeometry();
908}
909
910void QCocoaWindow::toggleFullScreen()
911{
912 const NSWindow *window = m_view.window;
913
914 // The window needs to have the correct collection behavior for the
915 // toggleFullScreen call to have an effect. The collection behavior
916 // will be reset in windowDidEnterFullScreen/windowDidLeaveFullScreen.
917 window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
918
919 const id sender = window;
920 [window toggleFullScreen:sender];
921}
922
923void QCocoaWindow::windowWillEnterFullScreen()
924{
925 if (!isContentView())
926 return;
927
928 updateNormalGeometry();
929
930 // The NSWindow needs to be resizable, otherwise we'll end up with
931 // the normal window geometry, centered in the middle of the screen
932 // on a black background. The styleMask will be reset below.
933 m_view.window.styleMask |= NSWindowStyleMaskResizable;
934}
935
936bool QCocoaWindow::isTransitioningToFullScreen() const
937{
938 NSWindow *window = m_view.window;
939 return window.styleMask & NSWindowStyleMaskFullScreen && !window.qt_fullScreen;
940}
941
942void QCocoaWindow::windowDidEnterFullScreen()
943{
944 if (!isContentView())
945 return;
946
947 Q_ASSERT_X(m_view.window.qt_fullScreen, "QCocoaWindow",
948 "FullScreen category processes window notifications first");
949
950 // Reset to original styleMask
951 setWindowFlags(window()->flags());
952
953 handleWindowStateChanged();
954}
955
956void QCocoaWindow::windowWillExitFullScreen()
957{
958 if (!isContentView())
959 return;
960
961 // The NSWindow needs to be resizable, otherwise we'll end up with
962 // a weird zoom animation. The styleMask will be reset below.
963 m_view.window.styleMask |= NSWindowStyleMaskResizable;
964}
965
966void QCocoaWindow::windowDidExitFullScreen()
967{
968 if (!isContentView())
969 return;
970
971 Q_ASSERT_X(!m_view.window.qt_fullScreen, "QCocoaWindow",
972 "FullScreen category processes window notifications first");
973
974 // Reset to original styleMask
975 setWindowFlags(window()->flags());
976
977 Qt::WindowState requestedState = window()->windowState();
978
979 // Deliver update of QWindow state
980 handleWindowStateChanged();
981
982 if (requestedState != windowState() && requestedState != Qt::WindowFullScreen) {
983 // We were only going out of full screen as an intermediate step before
984 // progressing into the final step, so re-sync the desired state.
985 applyWindowState(requestedState);
986 }
987}
988
989void QCocoaWindow::windowDidMiniaturize()
990{
991 if (!isContentView())
992 return;
993
994 handleWindowStateChanged();
995}
996
997void QCocoaWindow::windowDidDeminiaturize()
998{
999 if (!isContentView())
1000 return;
1001
1002 Qt::WindowState requestedState = window()->windowState();
1003
1004 handleWindowStateChanged();
1005
1006 if (requestedState != windowState() && requestedState != Qt::WindowMinimized) {
1007 // We were only going out of minimized as an intermediate step before
1008 // progressing into the final step, so re-sync the desired state.
1009 applyWindowState(requestedState);
1010 }
1011}
1012
1013void QCocoaWindow::handleWindowStateChanged(HandleFlags flags)
1014{
1015 Qt::WindowStates currentState = windowState();
1016 if (!(flags & HandleUnconditionally) && currentState == m_lastReportedWindowState)
1017 return;
1018
1019 qCDebug(lcQpaWindow) << "QCocoaWindow::handleWindowStateChanged" <<
1020 m_lastReportedWindowState << "-->" << currentState;
1021
1022 QWindowSystemInterface::handleWindowStateChanged<QWindowSystemInterface::SynchronousDelivery>(
1023 window(), currentState, m_lastReportedWindowState);
1024 m_lastReportedWindowState = currentState;
1025}
1026
1027// ------------------------------------------------------------
1028
1029void QCocoaWindow::setWindowTitle(const QString &title)
1030{
1031 if (!isContentView())
1032 return;
1033
1034 QMacAutoReleasePool pool;
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 if (!isContentView())
1046 return;
1047
1048 QMacAutoReleasePool pool;
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 if (!isContentView())
1062 return;
1063
1064 NSButton *iconButton = [m_view.window standardWindowButton:NSWindowDocumentIconButton];
1065 if (!iconButton) {
1066 // Window icons are only supported on macOS in combination with a document filePath
1067 return;
1068 }
1069
1070 QMacAutoReleasePool pool;
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
1266void QCocoaWindow::setEmbeddedInForeignView()
1267{
1268 // Release any previously created NSWindow.
1269 [m_nsWindow closeAndRelease];
1270 m_nsWindow = 0;
1271}
1272
1273// ----------------------- NSView notifications -----------------------
1274
1275void QCocoaWindow::viewDidChangeFrame()
1276{
1277 // Note: When the view is the content view, it would seem redundant
1278 // to deliver geometry changes both from windowDidResize and this
1279 // callback, but in some cases such as when macOS native tabbed
1280 // windows are enabled we may end up with the wrong geometry in
1281 // the initial windowDidResize callback when a new tab is created.
1282 handleGeometryChange();
1283}
1284
1285/*!
1286 Callback for NSViewGlobalFrameDidChangeNotification.
1287
1288 Posted whenever an NSView object that has attached surfaces (that is,
1289 NSOpenGLContext objects) moves to a different screen, or other cases
1290 where the NSOpenGLContext object needs to be updated.
1291*/
1292void QCocoaWindow::viewDidChangeGlobalFrame()
1293{
1294 [m_view setNeedsDisplay:YES];
1295}
1296
1297// ----------------------- NSWindow notifications -----------------------
1298
1299// Note: The following notifications are delivered to every QCocoaWindow
1300// that is a child of the NSWindow that triggered the notification. Each
1301// callback should make sure to filter out notifications if they do not
1302// apply to that QCocoaWindow, e.g. if the window is not a content view.
1303
1304void QCocoaWindow::windowDidMove()
1305{
1306 if (!isContentView())
1307 return;
1308
1309 handleGeometryChange();
1310
1311 // Moving a window might bring it out of maximized state
1312 handleWindowStateChanged();
1313}
1314
1315void QCocoaWindow::windowDidResize()
1316{
1317 if (!isContentView())
1318 return;
1319
1320 handleGeometryChange();
1321
1322 if (!m_view.inLiveResize)
1323 handleWindowStateChanged();
1324}
1325
1326void QCocoaWindow::windowWillStartLiveResize()
1327{
1328 // Track live resizing for all windows, including
1329 // child windows, so we know if it's safe to update
1330 // the window unthrottled outside of the main thread.
1331 m_inLiveResize = true;
1332}
1333
1334bool QCocoaWindow::allowsIndependentThreadedRendering() const
1335{
1336 // Use member variable to track this instead of reflecting
1337 // NSView.inLiveResize directly, so it can be called from
1338 // non-main threads.
1339 return !m_inLiveResize;
1340}
1341
1342void QCocoaWindow::windowDidEndLiveResize()
1343{
1344 m_inLiveResize = false;
1345
1346 if (!isContentView())
1347 return;
1348
1349 handleWindowStateChanged();
1350}
1351
1352void QCocoaWindow::windowDidBecomeKey()
1353{
1354 // The NSWindow we're part of become key. Check if we're the first
1355 // responder, and if so, deliver focus window change to our window.
1356 if (m_view.window.firstResponder != m_view)
1357 return;
1358
1359 qCDebug(lcQpaWindow) << m_view.window << "became key window."
1360 << "Updating focus window to" << this << "with view" << m_view;
1361
1362 if (windowIsPopupType()) {
1363 qCDebug(lcQpaWindow) << "Window is popup. Skipping focus window change.";
1364 return;
1365 }
1366
1367 // See also [QNSView becomeFirstResponder]
1368 QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
1369 window(), Qt::ActiveWindowFocusReason);
1370}
1371
1372void QCocoaWindow::windowDidResignKey()
1373{
1374 // The NSWindow we're part of lost key. Check if we're the first
1375 // responder, and if so, deliver window deactivation to our window.
1376 if (m_view.window.firstResponder != m_view)
1377 return;
1378
1379 qCDebug(lcQpaWindow) << m_view.window << "resigned key window."
1380 << "Clearing focus window" << this << "with view" << m_view;
1381
1382 // Make sure popups are closed before we deliver activation changes, which are
1383 // otherwise ignored by QApplication.
1384 closeAllPopups();
1385
1386 // The current key window will be non-nil if another window became key. If that
1387 // window is a Qt window, we delay the window activation event until the didBecomeKey
1388 // notification is delivered to the active window, to ensure an atomic update.
1389 NSWindow *newKeyWindow = [NSApp keyWindow];
1390 if (newKeyWindow && newKeyWindow != m_view.window
1391 && [newKeyWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) {
1392 qCDebug(lcQpaWindow) << "New key window" << newKeyWindow
1393 << "is Qt window. Deferring focus window change.";
1394 return;
1395 }
1396
1397 // Lost key window, go ahead and set the active window to zero
1398 if (!windowIsPopupType()) {
1399 QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>(
1400 nullptr, Qt::ActiveWindowFocusReason);
1401 }
1402}
1403
1404void QCocoaWindow::windowDidOrderOnScreen()
1405{
1406 // The current mouse window needs to get a leave event when a popup window opens.
1407 // For modal dialogs, QGuiApplicationPrivate::showModalWindow takes care of this.
1408 if (QWindowPrivate::get(window())->isPopup()) {
1409 QWindowSystemInterface::handleLeaveEvent<QWindowSystemInterface::SynchronousDelivery>
1410 (QGuiApplicationPrivate::currentMouseWindow);
1411 }
1412
1413 [m_view setNeedsDisplay:YES];
1414}
1415
1416void QCocoaWindow::windowDidOrderOffScreen()
1417{
1418 handleExposeEvent(QRegion());
1419 // We are closing a window, so the window that is now under the mouse
1420 // might need to get an Enter event if it isn't already the mouse window.
1421 if (window()->type() & Qt::Window) {
1422 const QPointF screenPoint = QCocoaScreen::mapFromNative([NSEvent mouseLocation]);
1423 if (QWindow *windowUnderMouse = QGuiApplication::topLevelAt(screenPoint.toPoint())) {
1424 if (windowUnderMouse != QGuiApplicationPrivate::instance()->currentMouseWindow) {
1425 const auto windowPoint = windowUnderMouse->mapFromGlobal(screenPoint);
1426 // asynchronous delivery on purpose
1427 QWindowSystemInterface::handleEnterEvent<QWindowSystemInterface::AsynchronousDelivery>
1428 (windowUnderMouse, windowPoint, screenPoint);
1429 }
1430 }
1431 }
1432}
1433
1434void QCocoaWindow::windowDidChangeOcclusionState()
1435{
1436 // Note, we don't take the view's hiddenOrHasHiddenAncestor state into
1437 // account here, but instead leave that up to handleExposeEvent, just
1438 // like all the other signals that could potentially change the exposed
1439 // state of the window.
1440 bool visible = m_view.window.occlusionState & NSWindowOcclusionStateVisible;
1441 qCDebug(lcQpaWindow) << "Occlusion state of" << m_view.window << "for"
1442 << window() << "changed to" << (visible ? "visible" : "occluded");
1443
1444 if (visible)
1445 [m_view setNeedsDisplay:YES];
1446 else
1447 handleExposeEvent(QRegion());
1448}
1449
1450void QCocoaWindow::windowDidChangeScreen()
1451{
1452 if (!window())
1453 return;
1454
1455 // Note: When a window is resized to 0x0 Cocoa will report the window's screen as nil
1456 NSScreen *nsScreen = m_view.window.screen;
1457
1458 qCDebug(lcQpaWindow) << window() << "did change" << nsScreen;
1459 QCocoaScreen::updateScreens();
1460
1461 auto *previousScreen = static_cast<QCocoaScreen*>(screen());
1462 auto *currentScreen = QCocoaScreen::get(nsScreen);
1463
1464 qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen;
1465
1466 // Note: The previous screen may be the same as the current screen, either because
1467 // a) the screen was just reconfigured, which still results in AppKit sending an
1468 // NSWindowDidChangeScreenNotification, b) because the previous screen was removed,
1469 // and we ended up calling QWindow::setScreen to move the window, which doesn't
1470 // actually move the window to the new screen, or c) because we've delivered the
1471 // screen change to the top level window, which will make all the child windows
1472 // of that window report the new screen when requested via QWindow::screen().
1473 // We still need to deliver the screen change in all these cases, as the
1474 // device-pixel ratio may have changed, and needs to be delivered to all
1475 // windows, both top level and child windows.
1476
1477 QWindowSystemInterface::handleWindowScreenChanged<QWindowSystemInterface::SynchronousDelivery>(
1478 window(), currentScreen ? currentScreen->screen() : nullptr);
1479
1480 if (currentScreen && hasPendingUpdateRequest()) {
1481 // Restart display-link on new screen. We need to do this unconditionally,
1482 // since we can't rely on the previousScreen reflecting whether or not the
1483 // window actually moved from one screen to another, or just stayed on the
1484 // same screen.
1485 currentScreen->requestUpdate();
1486 }
1487}
1488
1489// ----------------------- NSWindowDelegate callbacks -----------------------
1490
1491bool QCocoaWindow::windowShouldClose()
1492{
1493 qCDebug(lcQpaWindow) << "QCocoaWindow::windowShouldClose" << window();
1494
1495 // This callback should technically only determine if the window
1496 // should (be allowed to) close, but since our QPA API to determine
1497 // that also involves actually closing the window we do both at the
1498 // same time, instead of doing the latter in windowWillClose.
1499
1500 // If the window is closed, we will release and deallocate the NSWindow.
1501 // But frames higher up in the stack might still expect the window to
1502 // be alive, since the windowShouldClose: callback is technically only
1503 // supposed to answer YES or NO. To ensure the window is still alive
1504 // we put an autorelease in the closest pool (typically the runloop).
1505 [[m_view.window retain] autorelease];
1506
1507 return QWindowSystemInterface::handleCloseEvent<QWindowSystemInterface::SynchronousDelivery>(window());
1508}
1509
1510// ----------------------------- QPA forwarding -----------------------------
1511
1512void QCocoaWindow::handleGeometryChange()
1513{
1514 QRect newGeometry;
1515 if (isContentView() && !isEmbedded()) {
1516 // Content views are positioned at (0, 0) in the window, so we resolve via the window
1517 CGRect contentRect = [m_view.window contentRectForFrameRect:m_view.window.frame];
1518
1519 // The result above is in native screen coordinates, so remap to the Qt coordinate system
1520 newGeometry = QCocoaScreen::mapFromNative(contentRect).toRect();
1521 } else {
1522 // QNSView has isFlipped set, so no need to remap the geometry
1523 newGeometry = QRectF::fromCGRect(m_view.frame).toRect();
1524 }
1525
1526 qCDebug(lcQpaWindow) << "QCocoaWindow::handleGeometryChange" << window()
1527 << "current" << geometry() << "new" << newGeometry;
1528
1529 // It can happen that the current NSWindow is nil (if we are changing styleMask
1530 // from/to borderless, and the content view is being re-parented), which results
1531 // in invalid coordinates.
1532 if (m_inSetStyleMask && !m_view.window) {
1533 qCDebug(lcQpaWindow) << "Lacking window during style mask update, ignoring geometry change";
1534 return;
1535 }
1536
1537 // Prevent geometry change during initialization, as that will result
1538 // in a resize event, and Qt expects those to come after the show event.
1539 // FIXME: Remove once we've clarified the Qt behavior for this.
1540 if (!m_initialized) {
1541 // But update the QPlatformWindow reality
1542 QPlatformWindow::setGeometry(newGeometry);
1543 qCDebug(lcQpaWindow) << "Window still initializing, skipping event";
1544 return;
1545 }
1546
1547 QWindowSystemInterface::handleGeometryChange(window(), newGeometry);
1548
1549 // Changing the window geometry may affect the safe area margins
1550 updateSafeAreaMarginsIfNeeded();
1551
1552 // Guard against processing window system events during QWindow::setGeometry
1553 // calls, which Qt and Qt applications do not expect.
1554 if (!m_inSetGeometry)
1555 QWindowSystemInterface::flushWindowSystemEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
1556}
1557
1558void QCocoaWindow::handleExposeEvent(const QRegion &region)
1559{
1560 // Ideally we'd implement isExposed() in terms of these properties,
1561 // plus the occlusionState of the NSWindow, and let the expose event
1562 // pull the exposed state out when needed. However, when the window
1563 // is first shown we receive a drawRect call where the occlusionState
1564 // of the window is still hidden, but we still want to prepare the
1565 // window for display by issuing an expose event to Qt. To work around
1566 // this we don't use the occlusionState directly, but instead base
1567 // the exposed state on the region we get in, which in the case of
1568 // a window being obscured is an empty region, and in the case of
1569 // a drawRect call is a non-null region, even if occlusionState
1570 // is still hidden. This ensures the window is prepared for display.
1571 if (m_view.window.visible && m_view.window.screen
1572 && !geometry().size().isEmpty() && !region.isEmpty()
1573 && !m_view.hiddenOrHasHiddenAncestor) {
1574 m_exposedRect = region.boundingRect();
1575 } else {
1576 m_exposedRect = QRect();
1577 }
1578
1579 qCDebug(lcQpaDrawing) << "QCocoaWindow::handleExposeEvent" << window() << region << "isExposed" << isExposed();
1580 QWindowSystemInterface::handleExposeEvent<QWindowSystemInterface::SynchronousDelivery>(window(), region);
1581
1582 if (!isExposed())
1583 static_cast<QCocoaScreen *>(screen())->maybeStopDisplayLink();
1584}
1585
1586// --------------------------------------------------------------------------
1587
1588bool QCocoaWindow::windowIsPopupType(Qt::WindowType type) const
1589{
1590 if (type == Qt::Widget)
1591 type = window()->type();
1592 if (type == Qt::Tool)
1593 return false; // Qt::Tool has the Popup bit set but isn't, at least on Mac.
1594
1595 return ((type & Qt::Popup) == Qt::Popup);
1596}
1597
1598/*!
1599 Checks if the window is the content view of its immediate NSWindow.
1600
1601 Being the content view of a NSWindow means the QWindow is
1602 the highest accessible NSView object in the window's view
1603 hierarchy.
1604
1605 This is the case if the QWindow is a top level window.
1606*/
1607bool QCocoaWindow::isContentView() const
1608{
1609 return m_view.window.contentView == m_view;
1610}
1611
1612/*!
1613 Recreates (or removes) the NSWindow for this QWindow, if needed.
1614
1615 A QWindow may need a corresponding NSWindow/NSPanel, depending on
1616 whether or not it's a top level or not, window flags, etc.
1617*/
1618void QCocoaWindow::recreateWindowIfNeeded()
1619{
1620 QMacAutoReleasePool pool;
1621
1622 QPlatformWindow *parentWindow = QPlatformWindow::parent();
1623 auto *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow);
1624
1625 QCocoaWindow *oldParentCocoaWindow = nullptr;
1626 if (QNSView *qnsView = qnsview_cast(m_view.superview))
1627 oldParentCocoaWindow = qnsView.platformWindow;
1628
1629 if (isForeignWindow()) {
1630 // A foreign window is created as such, and can never move between being
1631 // foreign and not, so we don't need to get rid of any existing NSWindows,
1632 // nor create new ones, as a foreign window is a single simple NSView.
1633 qCDebug(lcQpaWindow) << "Skipping NSWindow management for foreign window" << this;
1634
1635 // We do however need to manage the parent relationship
1636 if (parentCocoaWindow)
1637 [parentCocoaWindow->m_view addSubview:m_view];
1638 else if (oldParentCocoaWindow)
1639 [m_view removeFromSuperview];
1640
1641 return;
1642 }
1643
1644 const bool isEmbeddedView = isEmbedded();
1645 RecreationReasons recreateReason = RecreationNotNeeded;
1646
1647 if (parentWindow != oldParentCocoaWindow)
1648 recreateReason |= ParentChanged;
1649
1650 if (!m_view.window)
1651 recreateReason |= MissingWindow;
1652
1653 // If the modality has changed the style mask will need updating
1654 if (m_windowModality != window()->modality())
1655 recreateReason |= WindowModalityChanged;
1656
1657 Qt::WindowType type = window()->type();
1658
1659 const bool shouldBeContentView = !parentWindow
1660 && !((type & Qt::SubWindow) == Qt::SubWindow)
1661 && !isEmbeddedView;
1662 if (isContentView() != shouldBeContentView)
1663 recreateReason |= ContentViewChanged;
1664
1665 const bool isPanel = isContentView() && [m_view.window isKindOfClass:[QNSPanel class]];
1666 const bool shouldBePanel = shouldBeContentView &&
1667 ((type & Qt::Popup) == Qt::Popup || (type & Qt::Dialog) == Qt::Dialog);
1668
1669 if (isPanel != shouldBePanel)
1670 recreateReason |= PanelChanged;
1671
1672 qCDebug(lcQpaWindow) << "QCocoaWindow::recreateWindowIfNeeded" << window() << recreateReason;
1673
1674 if (recreateReason == RecreationNotNeeded)
1675 return;
1676
1677 // Remove current window (if any)
1678 if ((isContentView() && !shouldBeContentView) || (recreateReason & PanelChanged)) {
1679 if (m_nsWindow) {
1680 qCDebug(lcQpaWindow) << "Getting rid of existing window" << m_nsWindow;
1681 [m_nsWindow closeAndRelease];
1682 if (isContentView() && !isEmbeddedView) {
1683 // We explicitly disassociate m_view from the window's contentView,
1684 // as AppKit does not automatically do this in response to removing
1685 // the view from the NSThemeFrame subview list, so we might end up
1686 // with a NSWindow contentView pointing to a deallocated NSView.
1687 m_view.window.contentView = nil;
1688 }
1689 m_nsWindow = nil;
1690 }
1691 }
1692
1693 if (shouldBeContentView && !m_nsWindow) {
1694 // Move view to new NSWindow if needed
1695 if (auto *newWindow = createNSWindow(shouldBePanel)) {
1696 qCDebug(lcQpaWindow) << "Ensuring that" << m_view << "is content view for" << newWindow;
1697 [m_view setPostsFrameChangedNotifications:NO];
1698 [newWindow setContentView:m_view];
1699 [m_view setPostsFrameChangedNotifications:YES];
1700
1701 m_nsWindow = newWindow;
1702 Q_ASSERT(m_view.window == m_nsWindow);
1703 }
1704 }
1705
1706 if (parentCocoaWindow) {
1707 // Child windows have no NSWindow, re-parent to superview instead
1708 [parentCocoaWindow->m_view addSubview:m_view];
1709 }
1710}
1711
1712void QCocoaWindow::requestUpdate()
1713{
1714 qCDebug(lcQpaDrawing) << "QCocoaWindow::requestUpdate" << window()
1715 << "using" << (updatesWithDisplayLink() ? "display-link" : "timer");
1716
1717 if (updatesWithDisplayLink()) {
1718 if (!static_cast<QCocoaScreen *>(screen())->requestUpdate()) {
1719 qCDebug(lcQpaDrawing) << "Falling back to timer-based update request";
1720 QPlatformWindow::requestUpdate();
1721 }
1722 } else {
1723 // Fall back to the un-throttled timer-based callback
1724 QPlatformWindow::requestUpdate();
1725 }
1726}
1727
1728bool QCocoaWindow::updatesWithDisplayLink() const
1729{
1730 // Update via CVDisplayLink if Vsync is enabled
1731 return format().swapInterval() != 0;
1732}
1733
1734void QCocoaWindow::deliverUpdateRequest()
1735{
1736 qCDebug(lcQpaDrawing) << "Delivering update request to" << window();
1737
1738 if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(contentLayer())) {
1739 // We attempt a read lock here, so that the animation/render thread is
1740 // prioritized lower than the main thread's displayLayer processing.
1741 // Without this the two threads might fight over the next drawable,
1742 // starving the main thread's presentation of the resized layer.
1743 if (!qtMetalLayer.displayLock.tryLockForRead()) {
1744 qCDebug(lcQpaDrawing) << "Deferring update request"
1745 << "due to" << qtMetalLayer << "needing display";
1746 return;
1747 }
1748
1749 // But we don't hold the lock, as the update request can recurse
1750 // back into setNeedsDisplay, which would deadlock.
1751 qtMetalLayer.displayLock.unlock();
1752 }
1753
1754 QPlatformWindow::deliverUpdateRequest();
1755}
1756
1757void QCocoaWindow::requestActivateWindow()
1758{
1759 QMacAutoReleasePool pool;
1760 [m_view.window makeFirstResponder:m_view];
1761 [m_view.window makeKeyWindow];
1762}
1763
1764/*
1765 Closes all popups, and removes observers and monitors.
1766*/
1767void QCocoaWindow::closeAllPopups()
1768{
1769 QGuiApplicationPrivate::instance()->closeAllPopups();
1770
1771 removePopupMonitor();
1772}
1773
1774void QCocoaWindow::removePopupMonitor()
1775{
1776 if (s_globalMouseMonitor) {
1777 [NSEvent removeMonitor:s_globalMouseMonitor];
1778 s_globalMouseMonitor = nil;
1779 }
1780 if (s_applicationActivationObserver) {
1781 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:s_applicationActivationObserver];
1782 s_applicationActivationObserver = nil;
1783 }
1784}
1785
1786void QCocoaWindow::setupPopupMonitor()
1787{
1788 // we open a popup window while we are not active. None of our existing event
1789 // handlers will get called if the user now clicks anywhere outside the application
1790 // or activates another window. Use a global event monitor to watch for mouse
1791 // presses, and close popups. We also want mouse tracking in the popup to work, so
1792 // also watch for MouseMoved.
1793 if (!s_globalMouseMonitor) {
1794 // we only get LeftMouseDown events when we also set LeftMouseUp.
1795 constexpr NSEventMask mouseButtonMask = NSEventTypeLeftMouseDown | NSEventTypeLeftMouseUp
1796 | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown
1797 | NSEventMaskMouseMoved;
1798 s_globalMouseMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mouseButtonMask
1799 handler:^(NSEvent *e){
1800 if (!QGuiApplicationPrivate::instance()->activePopupWindow()) {
1801 removePopupMonitor();
1802 return;
1803 }
1804 const auto eventType = cocoaEvent2QtMouseEvent(e);
1805 if (eventType == QEvent::MouseMove) {
1806 if (s_windowUnderMouse) {
1807 QWindow *window = s_windowUnderMouse->window();
1808 const auto button = cocoaButton2QtButton(e);
1809 const auto buttons = currentlyPressedMouseButtons();
1810 const auto globalPoint = QCocoaScreen::mapFromNative(NSEvent.mouseLocation);
1811 const auto localPoint = window->mapFromGlobal(globalPoint.toPoint());
1812 QWindowSystemInterface::handleMouseEvent(window, localPoint, globalPoint,
1813 buttons, button, eventType);
1814 }
1815 } else {
1816 closeAllPopups();
1817 }
1818 }];
1819 }
1820 // The activation observer also gets called when we become active because the user clicks
1821 // into the popup. This should not close the popup, so QCocoaApplicationDelegate's
1822 // applicationDidBecomeActive implementation removes this observer.
1823 if (!s_applicationActivationObserver) {
1824 s_applicationActivationObserver = [[[NSWorkspace sharedWorkspace] notificationCenter]
1825 addObserverForName:NSWorkspaceDidActivateApplicationNotification
1826 object:nil queue:nil
1827 usingBlock:^(NSNotification *){
1828 closeAllPopups();
1829 }];
1830 }
1831}
1832
1833QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel)
1834{
1835 QMacAutoReleasePool pool;
1836
1837 Qt::WindowType type = window()->type();
1838 Qt::WindowFlags flags = window()->flags();
1839
1840 QRect rect = geometry();
1841
1842 QScreen *targetScreen = nullptr;
1843 for (QScreen *screen : QGuiApplication::screens()) {
1844 if (screen->geometry().contains(rect.topLeft())) {
1845 targetScreen = screen;
1846 break;
1847 }
1848 }
1849
1850 NSWindowStyleMask styleMask = windowStyleMask(flags);
1851
1852 if (!targetScreen) {
1853 qCWarning(lcQpaWindow) << "Window position" << rect << "outside any known screen, using primary screen";
1854 targetScreen = QGuiApplication::primaryScreen();
1855 // Unless the window is created as borderless AppKit won't find a position and
1856 // screen that's close to the requested invalid position, and will always place
1857 // the window on the primary screen.
1858 styleMask = NSWindowStyleMaskBorderless;
1859 }
1860
1861 rect.translate(-targetScreen->geometry().topLeft());
1862 auto *targetCocoaScreen = static_cast<QCocoaScreen *>(targetScreen->handle());
1863 NSRect contentRect = QCocoaScreen::mapToNative(rect, targetCocoaScreen);
1864
1865 if (targetScreen->primaryOrientation() == Qt::PortraitOrientation) {
1866 // The macOS window manager has a bug, where if a screen is rotated, it will not allow
1867 // a window to be created within the area of the screen that has a Y coordinate (I quadrant)
1868 // higher than the height of the screen in its non-rotated state (including a magic padding
1869 // of 24 points), unless the window is created with the NSWindowStyleMaskBorderless style mask.
1870 if (styleMask && (contentRect.origin.y + 24 > targetScreen->geometry().width())) {
1871 qCDebug(lcQpaWindow) << "Window positioned on portrait screen."
1872 << "Adjusting style mask during creation";
1873 styleMask = NSWindowStyleMaskBorderless;
1874 }
1875 }
1876
1877 // Create NSWindow
1878 Class windowClass = shouldBePanel ? [QNSPanel class] : [QNSWindow class];
1879 QCocoaNSWindow *nsWindow = [[windowClass alloc] initWithContentRect:contentRect
1880 // Mask will be updated in setWindowFlags if not the final mask
1881 styleMask:styleMask
1882 // Deferring window creation breaks OpenGL (the GL context is
1883 // set up before the window is shown and needs a proper window)
1884 backing:NSBackingStoreBuffered defer:NO
1885 screen:targetCocoaScreen->nativeScreen()
1886 platformWindow:this];
1887
1888 // The resulting screen can be different from the screen requested if
1889 // for example the application has been assigned to a specific display.
1890 auto resultingScreen = QCocoaScreen::get(nsWindow.screen);
1891
1892 // But may not always be resolved at this point, in which case we fall back
1893 // to the target screen. The real screen will be delivered as a screen change
1894 // when resolved as part of ordering the window on screen.
1895 if (!resultingScreen)
1896 resultingScreen = targetCocoaScreen;
1897
1898 if (resultingScreen->screen() != window()->screen()) {
1899 QWindowSystemInterface::handleWindowScreenChanged<
1900 QWindowSystemInterface::SynchronousDelivery>(window(), resultingScreen->screen());
1901 }
1902
1903 static QSharedPointer<QNSWindowDelegate> sharedDelegate([[QNSWindowDelegate alloc] init],
1904 [](QNSWindowDelegate *delegate) { [delegate release]; });
1905 nsWindow.delegate = sharedDelegate.get();
1906
1907 // Prevent Cocoa from releasing the window on close. Qt
1908 // handles the close event asynchronously and we want to
1909 // make sure that NSWindow stays valid until the
1910 // QCocoaWindow is deleted by Qt.
1911 [nsWindow setReleasedWhenClosed:NO];
1912
1913 if (alwaysShowToolWindow()) {
1914 static dispatch_once_t onceToken;
1915 dispatch_once(&onceToken, ^{
1916 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
1917 [center addObserver:[QNSWindow class] selector:@selector(applicationActivationChanged:)
1918 name:NSApplicationWillResignActiveNotification object:nil];
1919 [center addObserver:[QNSWindow class] selector:@selector(applicationActivationChanged:)
1920 name:NSApplicationWillBecomeActiveNotification object:nil];
1921 });
1922 }
1923
1924 nsWindow.restorable = NO;
1925 nsWindow.level = windowLevel(flags);
1926 nsWindow.tabbingMode = NSWindowTabbingModeDisallowed;
1927
1928 if (shouldBePanel) {
1929 // Qt::Tool windows hide on app deactivation, unless Qt::WA_MacAlwaysShowToolWindow is set
1930 nsWindow.hidesOnDeactivate = ((type & Qt::Tool) == Qt::Tool) && !alwaysShowToolWindow();
1931
1932 // Make popup windows show on the same desktop as the parent window
1933 nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary
1934 | NSWindowCollectionBehaviorMoveToActiveSpace;
1935
1936 if ((type & Qt::Popup) == Qt::Popup) {
1937 nsWindow.hasShadow = YES;
1938 nsWindow.animationBehavior = NSWindowAnimationBehaviorUtilityWindow;
1939 if (QGuiApplication::applicationState() != Qt::ApplicationActive)
1940 setupPopupMonitor();
1941 }
1942 }
1943
1944 // Persist modality so we can detect changes later on
1945 m_windowModality = QPlatformWindow::window()->modality();
1946
1947 applyContentBorderThickness(nsWindow);
1948
1949 // We propagate the view's color space granulary to both the IOSurfaces
1950 // used for QSurface::RasterSurface, as well as the CAMetalLayer used for
1951 // QSurface::MetalSurface, but for QSurface::OpenGLSurface we don't have
1952 // that option as we use NSOpenGLContext instead of CAOpenGLLayer. As a
1953 // workaround we set the NSWindow's color space, which affects GL drawing
1954 // with NSOpenGLContext as well. This does not conflict with the granular
1955 // modifications we do to each surface for raster or Metal.
1956 if (auto *qtView = qnsview_cast(m_view))
1957 nsWindow.colorSpace = qtView.colorSpace;
1958
1959 return nsWindow;
1960}
1961
1962bool QCocoaWindow::alwaysShowToolWindow() const
1963{
1964 return qt_mac_resolveOption(false, window(), "_q_macAlwaysShowToolWindow", "");
1965}
1966
1967bool QCocoaWindow::setWindowModified(bool modified)
1968{
1969 if (!isContentView())
1970 return false;
1971
1972 m_view.window.documentEdited = modified;
1973 return true;
1974}
1975
1976void QCocoaWindow::setMenubar(QCocoaMenuBar *mb)
1977{
1978 m_menubar = mb;
1979}
1980
1981QCocoaMenuBar *QCocoaWindow::menubar() const
1982{
1983 return m_menubar;
1984}
1985
1986void QCocoaWindow::setWindowCursor(NSCursor *cursor)
1987{
1988 // Setting a cursor in a foreign view is not supported
1989 if (isForeignWindow())
1990 return;
1991
1992 qCInfo(lcQpaMouse) << "Setting" << this << "cursor to" << cursor;
1993
1994 QNSView *view = qnsview_cast(m_view);
1995 if (cursor == view.cursor)
1996 return;
1997
1998 view.cursor = cursor;
1999
2000 // We're not using the the legacy cursor rects API to manage our
2001 // cursor, but calling this function also invalidates AppKit's
2002 // view of whether or not we need a cursorUpdate callback for
2003 // our tracking area.
2004 [m_view.window invalidateCursorRectsForView:m_view];
2005
2006 // We've informed AppKit that we need a cursorUpdate, but cursor
2007 // updates for tracking areas are deferred in some cases, such as
2008 // when the mouse is down, whereas we want a synchronous update.
2009 // To ensure an updated cursor we synthesize a cursor update event
2010 // now if the window is otherwise allowed to change the cursor.
2011 auto locationInWindow = m_view.window.mouseLocationOutsideOfEventStream;
2012 auto locationInSuperview = [m_view.superview convertPoint:locationInWindow fromView:nil];
2013 bool mouseIsOverView = [m_view hitTest:locationInSuperview] == m_view;
2014 auto utilityMask = NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskTitled;
2015 bool isUtilityWindow = (m_view.window.styleMask & utilityMask) == utilityMask;
2016 if (mouseIsOverView && (m_view.window.keyWindow || isUtilityWindow)) {
2017 qCDebug(lcQpaMouse) << "Synthesizing cursor update";
2018 [m_view cursorUpdate:[NSEvent enterExitEventWithType:NSEventTypeCursorUpdate
2019 location:locationInWindow modifierFlags:0 timestamp:0
2020 windowNumber:m_view.window.windowNumber context:nil
2021 eventNumber:0 trackingNumber:0 userData:0]];
2022 }
2023}
2024
2025void QCocoaWindow::registerTouch(bool enable)
2026{
2027 m_registerTouchCount += enable ? 1 : -1;
2028 if (enable && m_registerTouchCount == 1)
2029 m_view.allowedTouchTypes |= NSTouchTypeMaskIndirect;
2030 else if (m_registerTouchCount == 0)
2031 m_view.allowedTouchTypes &= ~NSTouchTypeMaskIndirect;
2032}
2033
2034void QCocoaWindow::registerContentBorderArea(quintptr identifier, int upper, int lower)
2035{
2036 m_contentBorderAreas.insert(identifier, BorderRange(identifier, upper, lower));
2037 applyContentBorderThickness();
2038}
2039
2040void QCocoaWindow::setContentBorderAreaEnabled(quintptr identifier, bool enable)
2041{
2042 m_enabledContentBorderAreas.insert(identifier, enable);
2043 applyContentBorderThickness();
2044}
2045
2046void QCocoaWindow::setContentBorderEnabled(bool enable)
2047{
2048 m_drawContentBorderGradient = enable;
2049 applyContentBorderThickness();
2050}
2051
2052void QCocoaWindow::applyContentBorderThickness(NSWindow *window)
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 if (!isContentView())
2208 return QMargins();
2209
2210 NSRect frameW = m_view.window.frame;
2211 NSRect frameC = [m_view.window contentRectForFrameRect:frameW];
2212
2213 return QMargins(frameW.origin.x - frameC.origin.x,
2214 (frameW.origin.y + frameW.size.height) - (frameC.origin.y + frameC.size.height),
2215 (frameW.origin.x + frameW.size.width) - (frameC.origin.x + frameC.size.width),
2216 frameC.origin.y - frameW.origin.y);
2217}
2218
2219void QCocoaWindow::setFrameStrutEventsEnabled(bool enabled)
2220{
2221 m_frameStrutEventsEnabled = enabled;
2222}
2223
2224CALayer *QCocoaWindow::contentLayer() const
2225{
2226 auto *layer = m_view.layer;
2227 if (auto *containerLayer = qt_objc_cast<QContainerLayer*>(layer))
2228 layer = containerLayer.contentLayer;
2229 return layer;
2230}
2231
2232void QCocoaWindow::manageVisualEffectArea(quintptr identifier, const QRect &rect,
2233 NSVisualEffectMaterial material, NSVisualEffectBlendingMode blendMode,
2234 NSVisualEffectState activationState)
2235{
2236 if (!qt_objc_cast<QContainerLayer*>(m_view.layer)) {
2237 qCWarning(lcQpaWindow) << "Can not manage visual effect areas"
2238 << "in views without a container layer";
2239 return;
2240 }
2241
2242 qCDebug(lcQpaWindow) << "Updating visual effect area" << identifier
2243 << "to" << rect << "with material" << material << "blend mode"
2244 << blendMode << "and activation state" << activationState;
2245
2246 NSVisualEffectView *effectView = nullptr;
2247 if (m_effectViews.contains(identifier)) {
2248 effectView = m_effectViews.value(identifier);
2249 if (rect.isEmpty()) {
2250 [effectView removeFromSuperview];
2251 m_effectViews.remove(identifier);
2252 return;
2253 }
2254 } else if (!rect.isEmpty()) {
2255 effectView = [NSVisualEffectView new];
2256 // Ensure that the visual effect layer is stacked well
2257 // below our content layer (which defaults to a z of 0).
2258 effectView.wantsLayer = YES;
2259 effectView.layer.zPosition = -FLT_MAX;
2260 [m_view addSubview:effectView];
2261 m_effectViews.insert(identifier, effectView);
2262 }
2263
2264 if (!effectView)
2265 return;
2266
2267 effectView.frame = rect.toCGRect();
2268 effectView.material = material;
2269 effectView.blendingMode = blendMode;
2270 effectView.state = activationState;
2271}
2272
2273#ifndef QT_NO_DEBUG_STREAM
2274QDebug operator<<(QDebug debug, const QCocoaWindow *window)
2275{
2276 QDebugStateSaver saver(debug);
2277 debug.nospace();
2278 debug << "QCocoaWindow(" << (const void *)window;
2279 if (window)
2280 debug << ", window=" << window->window();
2281 debug << ')';
2282 return debug;
2283}
2284#endif // !QT_NO_DEBUG_STREAM
2285
2286QT_END_NAMESPACE
2287
2288#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")