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
qnswindow.mm
Go to the documentation of this file.
1// Copyright (C) 2017 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#if !defined(QNSWINDOW_PROTOCOL_IMPLMENTATION)
6
7#include <AppKit/AppKit.h>
8
9#include "qnswindow.h"
10#include "qcocoawindow.h"
11#include "qcocoahelpers.h"
14
15#include <qpa/qwindowsysteminterface.h>
16
17#include <QtGui/private/qhighdpiscaling_p.h>
18
19Q_STATIC_LOGGING_CATEGORY(lcQpaEvents, "qt.qpa.events");
20
21static bool isMouseEvent(NSEvent *ev)
22{
23 switch ([ev type]) {
24 case NSEventTypeLeftMouseDown:
25 case NSEventTypeLeftMouseUp:
26 case NSEventTypeRightMouseDown:
27 case NSEventTypeRightMouseUp:
28 case NSEventTypeMouseMoved:
29 case NSEventTypeLeftMouseDragged:
30 case NSEventTypeRightMouseDragged:
31 return true;
32 default:
33 return false;
34 }
35}
36
37@implementation NSWindow (FullScreenProperty)
38
39+ (void)load
40{
41 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
42 [center addObserverForName:NSWindowDidEnterFullScreenNotification object:nil queue:nil
43 usingBlock:^(NSNotification *notification) {
44 objc_setAssociatedObject(notification.object, @selector(qt_fullScreen),
45 @(YES), OBJC_ASSOCIATION_RETAIN);
46 }
47 ];
48 [center addObserverForName:NSWindowDidExitFullScreenNotification object:nil queue:nil
49 usingBlock:^(NSNotification *notification) {
50 objc_setAssociatedObject(notification.object, @selector(qt_fullScreen),
51 nil, OBJC_ASSOCIATION_RETAIN);
52 }
53 ];
54}
55
56- (BOOL)qt_fullScreen
57{
58 NSNumber *number = objc_getAssociatedObject(self, @selector(qt_fullScreen));
59 return [number boolValue];
60}
61@end
62
63
64QT_BEGIN_NAMESPACE
65NSWindow<QNSWindowProtocol> *qnswindow_cast(NSWindow *window)
66{
67 if ([window conformsToProtocol:@protocol(QNSWindowProtocol)])
68 return static_cast<QCocoaNSWindow *>(window);
69 else
70 return nil;
71}
72QT_END_NAMESPACE
73
74@implementation QNSWindow
75#define QNSWINDOW_PROTOCOL_IMPLMENTATION 1
76#include "qnswindow.mm"
77#undef QNSWINDOW_PROTOCOL_IMPLMENTATION
78
79+ (void)applicationActivationChanged:(NSNotification*)notification
80{
81 const id sender = self;
82 NSEnumerator<NSWindow*> *windowEnumerator = nullptr;
83 NSApplication *application = [NSApplication sharedApplication];
84
85 // Unfortunately there's no NSWindowListOrderedBackToFront,
86 // so we have to manually reverse the order using an array.
87 NSMutableArray<NSWindow *> *windows = [[NSMutableArray<NSWindow *> new] autorelease];
88 [application enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack
89 usingBlock:^(NSWindow *window, BOOL *) {
90 // For some reason AppKit will give us nil-windows, skip those
91 if (!window)
92 return;
93
94 [windows addObject:window];
95 }
96 ];
97
98 windowEnumerator = windows.reverseObjectEnumerator;
99
100 for (NSWindow *window in windowEnumerator) {
101 // We're meddling with normal and floating windows, so leave others alone
102 if (!(window.level == NSNormalWindowLevel || window.level == NSFloatingWindowLevel))
103 continue;
104
105 // Windows that hide automatically will keep their NSFloatingWindowLevel,
106 // and hence be on top of the window stack. We don't want to affect these
107 // windows, as otherwise we might end up with key windows being ordered
108 // behind these auto-hidden windows when activating the application by
109 // clicking on a new tool window.
110 if (window.hidesOnDeactivate)
111 continue;
112
113 if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) {
114 if (QCocoaWindow *cocoaWindow = static_cast<QCocoaNSWindow *>(window).platformWindow) {
115 window.level = notification.name == NSApplicationWillResignActiveNotification ?
116 NSNormalWindowLevel : cocoaWindow->windowLevel(cocoaWindow->window()->flags());
117 }
118 }
119
120 // The documentation says that "when a window enters a new level, it’s ordered
121 // in front of all its peers in that level", but that doesn't seem to be the
122 // case in practice. To keep the order correct after meddling with the window
123 // levels, we explicitly order each window to the front. Since we are iterating
124 // the windows in back-to-front order, this is okey. The call also triggers AppKit
125 // to re-evaluate the level in relation to windows from other applications,
126 // working around an issue where our tool windows would stay on top of other
127 // application windows if activation was transferred to another application by
128 // clicking on it instead of via the application switcher or Dock. Finally, we
129 // do this re-ordering for all windows (except auto-hiding ones), otherwise we would
130 // end up triggering a bug in AppKit where the tool windows would disappear behind
131 // the application window.
132 [window orderFront:sender];
133 }
134}
135
136@end
137
138@implementation QNSPanel
139#define QNSWINDOW_PROTOCOL_IMPLMENTATION 1
140#include "qnswindow.mm"
141#undef QNSWINDOW_PROTOCOL_IMPLMENTATION
142
143- (BOOL)worksWhenModal
144{
145 if (!m_platformWindow)
146 return NO;
147
148 // Conceptually there are two sets of windows we need consider:
149 //
150 // - windows 'lower' in the modal session stack
151 // - windows 'within' the current modal session
152 //
153 // The first set of windows should always be blocked by the current
154 // modal session, regardless of window type. The latter set may contain
155 // windows with a transient parent, which from Qt's point of view makes
156 // them 'child' windows, so we treat them as operable within the current
157 // modal session.
158
159 if (!NSApp.modalWindow)
160 return NO;
161
162 // Special case popup windows (menus, completions, etc), as these usually
163 // don't have a transient parent set, and we don't want to block them. The
164 // assumption is that these windows are only opened intermittently, from
165 // within windows that can already be interacted with in this modal session.
166 Qt::WindowType type = m_platformWindow->window()->type();
167 if (type == Qt::Popup)
168 return YES;
169
170 // If the current modal window (top level modal session) is not a Qt window we
171 // have no way of knowing if this window is transient child of the modal window.
172 if (![NSApp.modalWindow conformsToProtocol:@protocol(QNSWindowProtocol)])
173 return NO;
174
175 if (auto *modalWindow = static_cast<QCocoaNSWindow *>(NSApp.modalWindow).platformWindow) {
176 if (modalWindow->window()->isAncestorOf(m_platformWindow->window(), QWindow::IncludeTransients))
177 return YES;
178 }
179
180 return NO;
181}
182@end
183
184#else // QNSWINDOW_PROTOCOL_IMPLMENTATION
185
186// The following content is mixed in to the QNSWindow and QNSPanel classes via includes
187
188{
189 // Member variables
190 QPointer<QCocoaWindow> m_platformWindow;
191 bool m_isMinimizing;
192}
193
194- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style
195 backing:(NSBackingStoreType)backingStoreType defer:(BOOL)defer screen:(NSScreen *)screen
196 platformWindow:(QCocoaWindow*)window
197{
198 // Initializing the window will end up in [NSWindow _commonAwake], which calls many
199 // of the getters below. We need to set up the platform window reference first, so
200 // we can properly reflect the window's state during initialization.
201 m_platformWindow = window;
202
203 m_isMinimizing = false;
204
205 return [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:defer screen:screen];
206}
207
208- (QCocoaWindow *)platformWindow
209{
210 return m_platformWindow;
211}
212
213- (void)setContentView:(NSView*)view
214{
215 [super setContentView:view];
216
217 if (!qnsview_cast(self.contentView))
218 return;
219
220 // Now that we're the content view, we can apply the properties of
221 // the QWindow. We do this here, instead of in init, so that we can
222 // use the same code paths for setting these properties during
223 // NSWindow initialization as we do when setting them later on.
224 const QWindow *window = m_platformWindow->window();
225 qCDebug(lcQpaWindow) << "Reflecting" << window << "state to" << self;
226
227 m_platformWindow->propagateSizeHints();
228 m_platformWindow->setWindowFlags(window->flags());
229 m_platformWindow->setWindowTitle(window->title());
230 m_platformWindow->setWindowFilePath(window->filePath()); // Also sets window icon
231 m_platformWindow->setWindowState(window->windowState());
232 m_platformWindow->setOpacity(window->opacity());
233
234 // At the time of creation the QNSWindow is given a geometry based
235 // on the client geometry of the QWindow. But at that point we don't
236 // know anything about the size of the NSWindow frame, which means
237 // that the logic in QCocoaWindow::setGeometry for adjusting the
238 // client geometry based on the QWindow's positionPolicy is a noop.
239 // Now that we have a NSWindow to read the frame from we re-apply
240 // the QWindow geometry, which will move the NSWindow if needed.
241 m_platformWindow->setGeometry(QHighDpi::toNativeWindowGeometry(window->geometry(), window));
242
243
244 m_platformWindow->setVisible(window->isVisible());
245}
246
247- (NSString *)description
248{
249 NSMutableString *description = [NSMutableString stringWithString:[super description]];
250
251#ifndef QT_NO_DEBUG_STREAM
252 QString contentViewDescription;
253 QDebug debug(&contentViewDescription);
254 debug.nospace() << "; contentView=" << qnsview_cast(self.contentView) << ">";
255
256 NSRange lastCharacter = [description rangeOfComposedCharacterSequenceAtIndex:description.length - 1];
257 [description replaceCharactersInRange:lastCharacter withString:contentViewDescription.toNSString()];
258#endif
259
260 return description;
261}
262
263- (BOOL)canBecomeKeyWindow
264{
265 if (!m_platformWindow)
266 return NO;
267
268 if (m_platformWindow->shouldRefuseKeyWindowAndFirstResponder())
269 return NO;
270
271 if ([self isKindOfClass:[QNSPanel class]]) {
272 // Only tool or dialog windows should become key:
273 Qt::WindowType type = m_platformWindow->window()->type();
274 if (type == Qt::Tool || type == Qt::Dialog)
275 return YES;
276
277 return NO;
278 } else {
279 // The default implementation returns NO for title-bar less windows,
280 // override and return yes here to make sure popup windows such as
281 // the combobox popup can become the key window.
282 return YES;
283 }
284}
285
286- (BOOL)canBecomeMainWindow
287{
288 // Windows with a transient parent (such as combobox popup windows)
289 // cannot become the main window:
290 if (!m_platformWindow || m_platformWindow->window()->transientParent())
291 return NO;
292
293 return [super canBecomeMainWindow];
294}
295
296- (BOOL)isOpaque
297{
298 return m_platformWindow ? m_platformWindow->isOpaque() : [super isOpaque];
299}
300
301- (NSColor *)backgroundColor
302{
303 // FIXME: Plumb to a WA_NoSystemBackground-like window flag,
304 // or a QWindow::backgroundColor() property. In the meantime
305 // we assume that if you have translucent content, without a
306 // frame then you intend to do all background drawing yourself.
307 const QWindow *window = m_platformWindow ? m_platformWindow->window() : nullptr;
308 if (!self.opaque && window) {
309 // Qt::Popup also requires clearColor - in qmacstyle
310 // we fill background using a special path with rounded corners.
311 if (window->flags().testFlag(Qt::FramelessWindowHint)
312 || (window->flags() & Qt::WindowType_Mask) == Qt::Popup)
313 return [NSColor clearColor];
314 }
315
316 // This still allows you to have translucent content with a frame,
317 // where the system background (or color set via NSWindow) will
318 // shine through.
319 return [super backgroundColor];
320}
321
322- (void)sendEvent:(NSEvent*)theEvent
323{
324 qCDebug(lcQpaEvents) << "Sending" << theEvent << "to" << self;
325
326 // We might get events for a NSWindow after the corresponding platform
327 // window has been deleted, as the NSWindow can outlive the QCocoaWindow
328 // e.g. if being retained by other parts of AppKit, or in an auto-release
329 // pool. We guard against this in QNSView as well, as not all callbacks
330 // come via events, but if they do there's no point in propagating them.
331 if (!m_platformWindow)
332 return;
333
334 // Prevent deallocation of this NSWindow during event delivery, as we
335 // have logic further below that depends on the window being alive.
336 [[self retain] autorelease];
337
338 const char *eventType = object_getClassName(theEvent);
339 if (QWindowSystemInterface::handleNativeEvent(m_platformWindow->window(),
340 QByteArray::fromRawData(eventType, qstrlen(eventType)), theEvent, nullptr)) {
341 return;
342 }
343
344 const bool mouseEventInFrameStrut = [theEvent, self]{
345 if (isMouseEvent(theEvent)) {
346 const NSPoint loc = theEvent.locationInWindow;
347 const NSRect windowFrame = [self convertRectFromScreen:self.frame];
348 const NSRect contentFrame = self.contentView.frame;
349 if (NSMouseInRect(loc, windowFrame, NO) && !NSMouseInRect(loc, contentFrame, NO))
350 return true;
351 }
352 return false;
353 }();
354 // Any mouse-press in the frame of the window, including the title bar buttons, should
355 // close open popups. Presses within the window's content are handled to do that in the
356 // NSView::mouseDown implementation.
357 if (theEvent.type == NSEventTypeLeftMouseDown && mouseEventInFrameStrut)
358 QGuiApplicationPrivate::instance()->closeAllPopups();
359
360 [super sendEvent:theEvent];
361
362 if (!m_platformWindow)
363 return; // Platform window went away while processing event
364
365 // Cocoa will not deliver mouse events to a window that is modally blocked (by Cocoa,
366 // not Qt). However, an active popup is expected to grab any mouse event within the
367 // application, so we need to handle those explicitly and trust Qt's isWindowBlocked
368 // implementation to eat events that shouldn't be delivered anyway.
369 if (isMouseEvent(theEvent) && QGuiApplicationPrivate::instance()->activePopupWindow()
370 && QGuiApplicationPrivate::instance()->isWindowBlocked(m_platformWindow->window(), nullptr)) {
371 qCDebug(lcQpaWindow) << "Mouse event over modally blocked window" << m_platformWindow->window()
372 << "while popup is open - redirecting";
373 [qnsview_cast(m_platformWindow->view()) handleMouseEvent:theEvent];
374 }
375 if (m_platformWindow->frameStrutEventsEnabled() && mouseEventInFrameStrut)
376 [qnsview_cast(m_platformWindow->view()) handleFrameStrutMouseEvent:theEvent];
377}
378
379- (void)miniaturize:(id)sender
380{
381 QScopedValueRollback miniaturizeTracker(m_isMinimizing, true);
382 [super miniaturize:sender];
383}
384
385- (NSButton *)standardWindowButton:(NSWindowButton)buttonType
386{
387 NSButton *button = [super standardWindowButton:buttonType];
388
389 // When an NSWindow is asked to minimize it will check the
390 // NSWindowMiniaturizeButton for enablement before continuing,
391 // even if the style mask includes NSWindowStyleMaskMiniaturizable.
392 // To ensure that a window can be minimized, even when the
393 // minimize button has been disabled in response to the user
394 // setting CustomizeWindowHint, we temporarily return a default
395 // minimize-button that we haven't modified in updateTitleBarButtons.
396 // This ensures the window can be minimized, without visually
397 // toggling the actual minimize-button on and off.
398 if (buttonType == NSWindowMiniaturizeButton && m_isMinimizing && !button.enabled)
399 return [NSWindow standardWindowButton:buttonType forStyleMask:self.styleMask];
400
401 return button;
402}
403
404- (void)closeAndRelease
405{
406 qCDebug(lcQpaWindow) << "Closing and releasing" << self;
407 [self close];
408 [self release];
409}
410
411- (void)dealloc
412{
413 qCDebug(lcQpaWindow) << "Deallocating" << self;
414 self.delegate = nil;
415
416 [super dealloc];
417}
418
419#endif
static bool isMouseEvent(NSEvent *ev)
Definition qnswindow.mm:21