1/****************************************************************************
2**
3** Copyright (C) 2017 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the plugins of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#if !defined(QNSWINDOW_PROTOCOL_IMPLMENTATION)
41
42#include "qnswindow.h"
43#include "qcocoawindow.h"
44#include "qcocoahelpers.h"
45#include "qcocoaeventdispatcher.h"
46
47#include <qpa/qwindowsysteminterface.h>
48#include <qoperatingsystemversion.h>
49
50Q_LOGGING_CATEGORY(lcQpaEvents, "qt.qpa.events");
51
52static bool isMouseEvent(NSEvent *ev)
53{
54    switch ([ev type]) {
55    case NSEventTypeLeftMouseDown:
56    case NSEventTypeLeftMouseUp:
57    case NSEventTypeRightMouseDown:
58    case NSEventTypeRightMouseUp:
59    case NSEventTypeMouseMoved:
60    case NSEventTypeLeftMouseDragged:
61    case NSEventTypeRightMouseDragged:
62        return true;
63    default:
64        return false;
65    }
66}
67
68@implementation NSWindow (FullScreenProperty)
69
70+ (void)load
71{
72    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
73    [center addObserverForName:NSWindowDidEnterFullScreenNotification object:nil queue:nil
74        usingBlock:^(NSNotification *notification) {
75            objc_setAssociatedObject(notification.object, @selector(qt_fullScreen),
76                @(YES), OBJC_ASSOCIATION_RETAIN);
77        }
78    ];
79    [center addObserverForName:NSWindowDidExitFullScreenNotification object:nil queue:nil
80        usingBlock:^(NSNotification *notification) {
81            objc_setAssociatedObject(notification.object, @selector(qt_fullScreen),
82                nil, OBJC_ASSOCIATION_RETAIN);
83        }
84    ];
85}
86
87- (BOOL)qt_fullScreen
88{
89    NSNumber *number = objc_getAssociatedObject(self, @selector(qt_fullScreen));
90    return [number boolValue];
91}
92@end
93
94@implementation QNSWindow
95#define QNSWINDOW_PROTOCOL_IMPLMENTATION 1
96#include "qnswindow.mm"
97#undef QNSWINDOW_PROTOCOL_IMPLMENTATION
98
99+ (void)applicationActivationChanged:(NSNotification*)notification
100{
101    const id sender = self;
102    NSEnumerator<NSWindow*> *windowEnumerator = nullptr;
103    NSApplication *application = [NSApplication sharedApplication];
104
105    // Unfortunately there's no NSWindowListOrderedBackToFront,
106    // so we have to manually reverse the order using an array.
107    NSMutableArray<NSWindow *> *windows = [NSMutableArray<NSWindow *> new];
108    [application enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack
109        usingBlock:^(NSWindow *window, BOOL *) {
110            // For some reason AppKit will give us nil-windows, skip those
111            if (!window)
112                return;
113
114            [windows addObject:window];
115        }
116    ];
117
118    windowEnumerator = windows.reverseObjectEnumerator;
119
120    for (NSWindow *window in windowEnumerator) {
121        // We're meddling with normal and floating windows, so leave others alone
122        if (!(window.level == NSNormalWindowLevel || window.level == NSFloatingWindowLevel))
123            continue;
124
125        // Windows that hide automatically will keep their NSFloatingWindowLevel,
126        // and hence be on top of the window stack. We don't want to affect these
127        // windows, as otherwise we might end up with key windows being ordered
128        // behind these auto-hidden windows when activating the application by
129        // clicking on a new tool window.
130        if (window.hidesOnDeactivate)
131            continue;
132
133        if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) {
134            QCocoaWindow *cocoaWindow = static_cast<QCocoaNSWindow *>(window).platformWindow;
135            window.level = notification.name == NSApplicationWillResignActiveNotification ?
136                NSNormalWindowLevel : cocoaWindow->windowLevel(cocoaWindow->window()->flags());
137        }
138
139        // The documentation says that "when a window enters a new level, it’s ordered
140        // in front of all its peers in that level", but that doesn't seem to be the
141        // case in practice. To keep the order correct after meddling with the window
142        // levels, we explicitly order each window to the front. Since we are iterating
143        // the windows in back-to-front order, this is okey. The call also triggers AppKit
144        // to re-evaluate the level in relation to windows from other applications,
145        // working around an issue where our tool windows would stay on top of other
146        // application windows if activation was transferred to another application by
147        // clicking on it instead of via the application switcher or Dock. Finally, we
148        // do this re-ordering for all windows (except auto-hiding ones), otherwise we would
149        // end up triggering a bug in AppKit where the tool windows would disappear behind
150        // the application window.
151        [window orderFront:sender];
152    }
153}
154
155@end
156
157@implementation QNSPanel
158#define QNSWINDOW_PROTOCOL_IMPLMENTATION 1
159#include "qnswindow.mm"
160#undef QNSWINDOW_PROTOCOL_IMPLMENTATION
161
162- (BOOL)worksWhenModal
163{
164    if (!m_platformWindow)
165        return NO;
166
167    // Conceptually there are two sets of windows we need consider:
168    //
169    //   - windows 'lower' in the modal session stack
170    //   - windows 'within' the current modal session
171    //
172    // The first set of windows should always be blocked by the current
173    // modal session, regardless of window type. The latter set may contain
174    // windows with a transient parent, which from Qt's point of view makes
175    // them 'child' windows, so we treat them as operable within the current
176    // modal session.
177
178    if (!NSApp.modalWindow)
179        return NO;
180
181    // Special case popup windows (menus, completions, etc), as these usually
182    // don't have a transient parent set, and we don't want to block them. The
183    // assumption is that these windows are only opened intermittently, from
184    // within windows that can already be interacted with in this modal session.
185    Qt::WindowType type = m_platformWindow->window()->type();
186    if (type == Qt::Popup)
187        return YES;
188
189    // If the current modal window (top level modal session) is not a Qt window we
190    // have no way of knowing if this window is transient child of the modal window.
191    if (![NSApp.modalWindow conformsToProtocol:@protocol(QNSWindowProtocol)])
192        return NO;
193
194    if (auto *modalWindow = static_cast<QCocoaNSWindow *>(NSApp.modalWindow).platformWindow) {
195        if (modalWindow->window()->isAncestorOf(m_platformWindow->window(), QWindow::IncludeTransients))
196            return YES;
197    }
198
199    return NO;
200}
201@end
202
203#if !defined(QT_APPLE_NO_PRIVATE_APIS)
204// When creating an NSWindow the worksWhenModal function is queried,
205// and the resulting state is used to set the corresponding window tag,
206// which the window server uses to determine whether or not the window
207// should be allowed to activate via mouse clicks in the title-bar.
208// Unfortunately, prior to macOS 10.15, this window tag was never
209// updated after the initial assignment in [NSWindow _commonAwake],
210// which meant that windows that dynamically change their worksWhenModal
211// state will behave as if they were never allowed to work when modal.
212// We work around this by manually updating the window tag when needed.
213
214typedef uint32_t CGSConnectionID;
215typedef uint32_t CGSWindowID;
216
217extern "C" {
218CGSConnectionID CGSMainConnectionID() __attribute__((weak_import));
219OSStatus CGSSetWindowTags(const CGSConnectionID, const CGSWindowID, int *, int) __attribute__((weak_import));
220OSStatus CGSClearWindowTags(const CGSConnectionID, const CGSWindowID, int *, int) __attribute__((weak_import));
221}
222
223@interface QNSPanel (WorksWhenModalWindowTagWorkaround) @end
224@implementation QNSPanel (WorksWhenModalWindowTagWorkaround)
225- (void)setWorksWhenModal:(BOOL)worksWhenModal
226{
227    [super setWorksWhenModal:worksWhenModal];
228
229    if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSCatalina) {
230        if (CGSMainConnectionID && CGSSetWindowTags && CGSClearWindowTags) {
231            static int kWorksWhenModalWindowTag = 0x40;
232            auto *function = worksWhenModal ? CGSSetWindowTags : CGSClearWindowTags;
233            function(CGSMainConnectionID(), self.windowNumber, &kWorksWhenModalWindowTag, 64);
234        } else {
235            qWarning() << "Missing APIs for window tag handling, can not update worksWhenModal state";
236        }
237    }
238}
239@end
240#endif // QT_APPLE_NO_PRIVATE_APIS
241
242#else // QNSWINDOW_PROTOCOL_IMPLMENTATION
243
244// The following content is mixed in to the QNSWindow and QNSPanel classes via includes
245
246{
247    // Member variables
248    QPointer<QCocoaWindow> m_platformWindow;
249}
250
251- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style
252    backing:(NSBackingStoreType)backingStoreType defer:(BOOL)defer screen:(NSScreen *)screen
253    platformWindow:(QCocoaWindow*)window
254{
255    // Initializing the window will end up in [NSWindow _commonAwake], which calls many
256    // of the getters below. We need to set up the platform window reference first, so
257    // we can properly reflect the window's state during initialization.
258    m_platformWindow = window;
259
260    return [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:defer screen:screen];
261}
262
263- (QCocoaWindow *)platformWindow
264{
265    return m_platformWindow;
266}
267
268- (NSString *)description
269{
270    NSMutableString *description = [NSMutableString stringWithString:[super description]];
271
272#ifndef QT_NO_DEBUG_STREAM
273    QString contentViewDescription;
274    QDebug debug(&contentViewDescription);
275    debug.nospace() << "; contentView=" << qnsview_cast(self.contentView) << ">";
276
277    NSRange lastCharacter = [description rangeOfComposedCharacterSequenceAtIndex:description.length - 1];
278    [description replaceCharactersInRange:lastCharacter withString:contentViewDescription.toNSString()];
279#endif
280
281    return description;
282}
283
284- (BOOL)canBecomeKeyWindow
285{
286    if (!m_platformWindow)
287        return NO;
288
289    if (m_platformWindow->shouldRefuseKeyWindowAndFirstResponder())
290        return NO;
291
292    if ([self isKindOfClass:[QNSPanel class]]) {
293        // Only tool or dialog windows should become key:
294        Qt::WindowType type = m_platformWindow->window()->type();
295        if (type == Qt::Tool || type == Qt::Dialog)
296            return YES;
297
298        return NO;
299    } else {
300        // The default implementation returns NO for title-bar less windows,
301        // override and return yes here to make sure popup windows such as
302        // the combobox popup can become the key window.
303        return YES;
304    }
305}
306
307- (BOOL)canBecomeMainWindow
308{
309    // Windows with a transient parent (such as combobox popup windows)
310    // cannot become the main window:
311    if (!m_platformWindow || m_platformWindow->window()->transientParent())
312        return NO;
313
314    return [super canBecomeMainWindow];
315}
316
317- (BOOL)isOpaque
318{
319    return m_platformWindow ? m_platformWindow->isOpaque() : [super isOpaque];
320}
321
322- (NSColor *)backgroundColor
323{
324    return self.styleMask == NSWindowStyleMaskBorderless ?
325        [NSColor clearColor] : [super backgroundColor];
326}
327
328- (void)sendEvent:(NSEvent*)theEvent
329{
330    qCDebug(lcQpaEvents) << "Sending" << theEvent << "to" << self;
331
332    // We might get events for a NSWindow after the corresponding platform
333    // window has been deleted, as the NSWindow can outlive the QCocoaWindow
334    // e.g. if being retained by other parts of AppKit, or in an auto-release
335    // pool. We guard against this in QNSView as well, as not all callbacks
336    // come via events, but if they do there's no point in propagating them.
337    if (!m_platformWindow)
338        return;
339
340    // Prevent deallocation of this NSWindow during event delivery, as we
341    // have logic further below that depends on the window being alive.
342    [[self retain] autorelease];
343
344    const char *eventType = object_getClassName(theEvent);
345    if (QWindowSystemInterface::handleNativeEvent(m_platformWindow->window(),
346        QByteArray::fromRawData(eventType, qstrlen(eventType)), theEvent, nullptr)) {
347        return;
348    }
349
350    [super sendEvent:theEvent];
351
352    if (!m_platformWindow)
353        return; // Platform window went away while processing event
354
355    if (m_platformWindow->frameStrutEventsEnabled() && isMouseEvent(theEvent)) {
356        NSPoint loc = [theEvent locationInWindow];
357        NSRect windowFrame = [self convertRectFromScreen:self.frame];
358        NSRect contentFrame = self.contentView.frame;
359        if (NSMouseInRect(loc, windowFrame, NO) && !NSMouseInRect(loc, contentFrame, NO))
360            [qnsview_cast(m_platformWindow->view()) handleFrameStrutMouseEvent:theEvent];
361    }
362}
363
364- (void)closeAndRelease
365{
366    qCDebug(lcQpaWindow) << "Closing and releasing" << self;
367    [self close];
368    [self release];
369}
370
371- (void)dealloc
372{
373    qCDebug(lcQpaWindow) << "Deallocating" << self;
374    self.delegate = nil;
375
376    [super dealloc];
377}
378
379#endif
380