1 | // Copyright (C) 2018 The Qt Company Ltd. |
---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR |
---|
3 | // GPL-2.0-only OR GPL-3.0-only |
---|
4 | |
---|
5 | /**************************************************************************** |
---|
6 | ** |
---|
7 | ** Copyright (c) 2007-2008, Apple, Inc. |
---|
8 | ** |
---|
9 | ** All rights reserved. |
---|
10 | ** |
---|
11 | ** Redistribution and use in source and binary forms, with or without |
---|
12 | ** modification, are permitted provided that the following conditions are met: |
---|
13 | ** |
---|
14 | ** * Redistributions of source code must retain the above copyright notice, |
---|
15 | ** this list of conditions and the following disclaimer. |
---|
16 | ** |
---|
17 | ** * Redistributions in binary form must reproduce the above copyright |
---|
18 | *notice, |
---|
19 | ** this list of conditions and the following disclaimer in the |
---|
20 | *documentation |
---|
21 | ** and/or other materials provided with the distribution. |
---|
22 | ** |
---|
23 | ** * Neither the name of Apple, Inc. nor the names of its contributors |
---|
24 | ** may be used to endorse or promote products derived from this software |
---|
25 | ** without specific prior written permission. |
---|
26 | ** |
---|
27 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
---|
28 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
---|
29 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
---|
30 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER |
---|
31 | *OR |
---|
32 | ** CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
---|
33 | ** EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
---|
34 | ** PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
---|
35 | ** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
---|
36 | ** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
---|
37 | ** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
---|
38 | ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
39 | ** |
---|
40 | ****************************************************************************/ |
---|
41 | |
---|
42 | #include <AppKit/AppKit.h> |
---|
43 | |
---|
44 | #include "qcocoaapplicationdelegate.h" |
---|
45 | #include "qcocoahelpers.h" |
---|
46 | #include "qcocoaintegration.h" |
---|
47 | #include "qcocoamenu.h" |
---|
48 | #include "qcocoamenubar.h" |
---|
49 | #include "qcocoamenuitem.h" |
---|
50 | #include "qcocoamenuloader.h" |
---|
51 | #include "qcocoansmenu.h" |
---|
52 | |
---|
53 | #if QT_CONFIG(sessionmanager) |
---|
54 | #include "qcocoasessionmanager.h" |
---|
55 | #endif |
---|
56 | |
---|
57 | #include <qdebug.h> |
---|
58 | #include <qevent.h> |
---|
59 | #include <qguiapplication.h> |
---|
60 | #include <qpa/qwindowsysteminterface.h> |
---|
61 | #include <qurl.h> |
---|
62 | #include <qwindowdefs.h> |
---|
63 | |
---|
64 | QT_USE_NAMESPACE |
---|
65 | |
---|
66 | @implementation QCocoaApplicationDelegate { |
---|
67 | NSObject<NSApplicationDelegate> *reflectionDelegate; |
---|
68 | bool inLaunch; |
---|
69 | } |
---|
70 | |
---|
71 | + (instancetype)sharedDelegate { |
---|
72 | static QCocoaApplicationDelegate *shared = nil; |
---|
73 | static dispatch_once_t onceToken; |
---|
74 | dispatch_once(&onceToken, ^{ |
---|
75 | shared = [[self alloc] init]; |
---|
76 | atexit_b(^{ |
---|
77 | [shared release]; |
---|
78 | shared = nil; |
---|
79 | }); |
---|
80 | }); |
---|
81 | return shared; |
---|
82 | } |
---|
83 | |
---|
84 | - (instancetype)init { |
---|
85 | self = [super init]; |
---|
86 | if (self) { |
---|
87 | inLaunch = true; |
---|
88 | } |
---|
89 | return self; |
---|
90 | } |
---|
91 | |
---|
92 | - (void)dealloc { |
---|
93 | [_dockMenu release]; |
---|
94 | if (reflectionDelegate) { |
---|
95 | [[NSApplication sharedApplication] setDelegate:reflectionDelegate]; |
---|
96 | [reflectionDelegate release]; |
---|
97 | } |
---|
98 | [[NSNotificationCenter defaultCenter] removeObserver:self]; |
---|
99 | |
---|
100 | [super dealloc]; |
---|
101 | } |
---|
102 | |
---|
103 | - (NSMenu *)applicationDockMenu:(NSApplication *)sender { |
---|
104 | Q_UNUSED(sender); |
---|
105 | // Manually invoke the delegate's -menuWillOpen: method. |
---|
106 | // See QTBUG-39604 (and its fix) for details. |
---|
107 | [self.dockMenu.delegate menuWillOpen:self.dockMenu]; |
---|
108 | return [[self.dockMenu retain] autorelease]; |
---|
109 | } |
---|
110 | |
---|
111 | // This function will only be called when NSApp is actually running. |
---|
112 | - (NSApplicationTerminateReply)applicationShouldTerminate: |
---|
113 | (NSApplication *)sender { |
---|
114 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
115 | return [reflectionDelegate applicationShouldTerminate:sender]; |
---|
116 | |
---|
117 | if (QGuiApplicationPrivate::instance() |
---|
118 | ->threadData.loadRelaxed() |
---|
119 | ->eventLoops.isEmpty()) { |
---|
120 | // No event loop is executing. This probably means that Qt is used as a |
---|
121 | // plugin, or as a part of a native Cocoa application. In any case it should |
---|
122 | // be fine to terminate now. |
---|
123 | qCDebug(lcQpaApplication) << "No running event loops, terminating now"; |
---|
124 | return NSTerminateNow; |
---|
125 | } |
---|
126 | |
---|
127 | #if QT_CONFIG(sessionmanager) |
---|
128 | QCocoaSessionManager *cocoaSessionManager = QCocoaSessionManager::instance(); |
---|
129 | cocoaSessionManager->resetCancellation(); |
---|
130 | cocoaSessionManager->appCommitData(); |
---|
131 | |
---|
132 | if (cocoaSessionManager->wasCanceled()) { |
---|
133 | qCDebug(lcQpaApplication) |
---|
134 | << "Session management canceled application termination"; |
---|
135 | return NSTerminateCancel; |
---|
136 | } |
---|
137 | #endif |
---|
138 | |
---|
139 | if (!QWindowSystemInterface::handleApplicationTermination< |
---|
140 | QWindowSystemInterface::SynchronousDelivery>()) { |
---|
141 | qCDebug(lcQpaApplication) << "Application termination canceled"; |
---|
142 | return NSTerminateCancel; |
---|
143 | } |
---|
144 | |
---|
145 | // Even if the application termination was accepted by the application we |
---|
146 | // can't return NSTerminateNow, as that would trigger AppKit to ultimately |
---|
147 | // call exit(). We need to ensure that the runloop continues spinning so that |
---|
148 | // we can return from our own event loop back to main(), and exit from there. |
---|
149 | qCDebug(lcQpaApplication) << "Termination accepted, but returning to runloop " |
---|
150 | "for exit through main()"; |
---|
151 | return NSTerminateCancel; |
---|
152 | } |
---|
153 | |
---|
154 | - (void)applicationWillFinishLaunching:(NSNotification *)notification { |
---|
155 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
156 | [reflectionDelegate applicationWillFinishLaunching:notification]; |
---|
157 | |
---|
158 | /* |
---|
159 | From the Cocoa documentation: "A good place to install event handlers |
---|
160 | is in the applicationWillFinishLaunching: method of the application |
---|
161 | delegate. At that point, the Application Kit has installed its default |
---|
162 | event handlers, so if you install a handler for one of the same events, |
---|
163 | it will replace the Application Kit version." |
---|
164 | */ |
---|
165 | |
---|
166 | /* |
---|
167 | If Qt is used as a plugin, we let the 3rd party application handle |
---|
168 | events like quit and open file events. Otherwise, if we install our own |
---|
169 | handlers, we easily end up breaking functionality the 3rd party |
---|
170 | application depends on. |
---|
171 | */ |
---|
172 | NSAppleEventManager *eventManager = |
---|
173 | [NSAppleEventManager sharedAppleEventManager]; |
---|
174 | [eventManager setEventHandler:self |
---|
175 | andSelector:@selector(getUrl:withReplyEvent:) |
---|
176 | forEventClass:kInternetEventClass |
---|
177 | andEventID:kAEGetURL]; |
---|
178 | } |
---|
179 | |
---|
180 | // called by QCocoaIntegration's destructor before resetting the application |
---|
181 | // delegate to nil |
---|
182 | - (void)removeAppleEventHandlers { |
---|
183 | NSAppleEventManager *eventManager = |
---|
184 | [NSAppleEventManager sharedAppleEventManager]; |
---|
185 | [eventManager removeEventHandlerForEventClass:kInternetEventClass |
---|
186 | andEventID:kAEGetURL]; |
---|
187 | } |
---|
188 | |
---|
189 | - (bool)inLaunch { |
---|
190 | return inLaunch; |
---|
191 | } |
---|
192 | |
---|
193 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { |
---|
194 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
195 | [reflectionDelegate applicationDidFinishLaunching:aNotification]; |
---|
196 | |
---|
197 | inLaunch = false; |
---|
198 | |
---|
199 | if (qEnvironmentVariableIsEmpty( |
---|
200 | "QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM")) { |
---|
201 | auto frontmostApplication = |
---|
202 | NSWorkspace.sharedWorkspace.frontmostApplication; |
---|
203 | auto currentApplication = NSRunningApplication.currentApplication; |
---|
204 | if (frontmostApplication != currentApplication) { |
---|
205 | // Move the application to front to avoid launching behind the terminal. |
---|
206 | // Ignoring other apps is necessary (we must ignore the terminal), but |
---|
207 | // makes Qt apps play slightly less nice with other apps when launching |
---|
208 | // from Finder (see the activateIgnoringOtherApps docs). FIXME: Try to |
---|
209 | // distinguish between being non-active here because another application |
---|
210 | // stole activation in the time it took us to launch from Finder, and |
---|
211 | // being non-active because we were launched from Terminal or something |
---|
212 | // that doesn't activate us at all. |
---|
213 | qCDebug(lcQpaApplication) << "Launched with" << frontmostApplication |
---|
214 | << "as frontmost application. Activating" |
---|
215 | << currentApplication << "instead."; |
---|
216 | [NSApplication.sharedApplication activateIgnoringOtherApps:YES]; |
---|
217 | } |
---|
218 | |
---|
219 | // Qt windows are typically shown in main(), at which point the application |
---|
220 | // is not active yet. When the application is activated, either externally |
---|
221 | // or via the override above, it will only bring the main and key windows |
---|
222 | // forward, which differs from the behavior if these windows had been shown |
---|
223 | // once the application was already active. To work around this, we |
---|
224 | // explicitly activate the current application again, bringing all windows |
---|
225 | // to the front. |
---|
226 | [currentApplication activateWithOptions:NSApplicationActivateAllWindows]; |
---|
227 | } |
---|
228 | |
---|
229 | QCocoaMenuBar::insertWindowMenu(); |
---|
230 | } |
---|
231 | |
---|
232 | - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { |
---|
233 | Q_UNUSED(filenames); |
---|
234 | Q_UNUSED(sender); |
---|
235 | |
---|
236 | for (NSString *fileName in filenames) { |
---|
237 | QString qtFileName = QString::fromNSString(fileName); |
---|
238 | if (inLaunch) { |
---|
239 | // We need to be careful because Cocoa will be nice enough to take |
---|
240 | // command line arguments and send them to us as events. Given the history |
---|
241 | // of Qt Applications, this will result in behavior people don't want, as |
---|
242 | // they might be doing the opening themselves with the command line |
---|
243 | // parsing. |
---|
244 | if (qApp->arguments().contains(qtFileName)) |
---|
245 | continue; |
---|
246 | } |
---|
247 | QWindowSystemInterface::handleFileOpenEvent(qtFileName); |
---|
248 | } |
---|
249 | |
---|
250 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
251 | [reflectionDelegate application:sender openFiles:filenames]; |
---|
252 | } |
---|
253 | |
---|
254 | - (BOOL)applicationShouldTerminateAfterLastWindowClosed: |
---|
255 | (NSApplication *)sender { |
---|
256 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
257 | return [reflectionDelegate |
---|
258 | applicationShouldTerminateAfterLastWindowClosed:sender]; |
---|
259 | |
---|
260 | return NO; // Someday qApp->quitOnLastWindowClosed(); when QApp and NSApp work |
---|
261 | // closer together. |
---|
262 | } |
---|
263 | |
---|
264 | - (void)applicationDidBecomeActive:(NSNotification *)notification { |
---|
265 | if (QCocoaWindow::s_applicationActivationObserver) { |
---|
266 | [[[NSWorkspace sharedWorkspace] notificationCenter] |
---|
267 | removeObserver:QCocoaWindow::s_applicationActivationObserver]; |
---|
268 | QCocoaWindow::s_applicationActivationObserver = nil; |
---|
269 | } |
---|
270 | |
---|
271 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
272 | [reflectionDelegate applicationDidBecomeActive:notification]; |
---|
273 | |
---|
274 | QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationActive); |
---|
275 | |
---|
276 | if (QCocoaWindow::s_windowUnderMouse) { |
---|
277 | QPointF windowPoint; |
---|
278 | QPointF screenPoint; |
---|
279 | QNSView *view = qnsview_cast(QCocoaWindow::s_windowUnderMouse->m_view); |
---|
280 | [view convertFromScreen:[NSEvent mouseLocation] |
---|
281 | toWindowPoint:&windowPoint |
---|
282 | andScreenPoint:&screenPoint]; |
---|
283 | QWindow *windowUnderMouse = QCocoaWindow::s_windowUnderMouse->window(); |
---|
284 | qCInfo(lcQpaMouse) << "Application activated with mouse at" << windowPoint |
---|
285 | << "; sending" << QEvent::Enter << "to" |
---|
286 | << windowUnderMouse; |
---|
287 | QWindowSystemInterface::handleEnterEvent(windowUnderMouse, windowPoint, |
---|
288 | screenPoint); |
---|
289 | } |
---|
290 | } |
---|
291 | |
---|
292 | - (void)applicationDidResignActive:(NSNotification *)notification { |
---|
293 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
294 | [reflectionDelegate applicationDidResignActive:notification]; |
---|
295 | |
---|
296 | QWindowSystemInterface::handleApplicationStateChanged( |
---|
297 | Qt::ApplicationInactive); |
---|
298 | |
---|
299 | if (QCocoaWindow::s_windowUnderMouse) { |
---|
300 | QWindow *windowUnderMouse = QCocoaWindow::s_windowUnderMouse->window(); |
---|
301 | qCInfo(lcQpaMouse) << "Application deactivated; sending" << QEvent::Leave |
---|
302 | << "to" << windowUnderMouse; |
---|
303 | QWindowSystemInterface::handleLeaveEvent(windowUnderMouse); |
---|
304 | } |
---|
305 | } |
---|
306 | |
---|
307 | - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication |
---|
308 | hasVisibleWindows:(BOOL)flag { |
---|
309 | if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
310 | return [reflectionDelegate applicationShouldHandleReopen:theApplication |
---|
311 | hasVisibleWindows:flag]; |
---|
312 | |
---|
313 | /* |
---|
314 | true to force delivery of the event even if the application state is |
---|
315 | already active, because rapp (handle reopen) events are sent each time the |
---|
316 | dock icon is clicked regardless of the active state of the application or |
---|
317 | number of visible windows. For example, a browser app that has no windows |
---|
318 | opened would need the event be to delivered even if it was already active |
---|
319 | in order to create a new window as per OS X conventions. |
---|
320 | */ |
---|
321 | QWindowSystemInterface::handleApplicationStateChanged( |
---|
322 | Qt::ApplicationActive, true /*forcePropagate*/); |
---|
323 | |
---|
324 | return YES; |
---|
325 | } |
---|
326 | |
---|
327 | - (void)setReflectionDelegate:(NSObject<NSApplicationDelegate> *)oldDelegate { |
---|
328 | [oldDelegate retain]; |
---|
329 | [reflectionDelegate release]; |
---|
330 | reflectionDelegate = oldDelegate; |
---|
331 | } |
---|
332 | |
---|
333 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { |
---|
334 | NSMethodSignature *result = [super methodSignatureForSelector:aSelector]; |
---|
335 | if (!result && reflectionDelegate) { |
---|
336 | result = [reflectionDelegate methodSignatureForSelector:aSelector]; |
---|
337 | } |
---|
338 | return result; |
---|
339 | } |
---|
340 | |
---|
341 | - (BOOL)respondsToSelector:(SEL)aSelector { |
---|
342 | return [super respondsToSelector:aSelector] || |
---|
343 | [reflectionDelegate respondsToSelector:aSelector]; |
---|
344 | } |
---|
345 | |
---|
346 | - (void)forwardInvocation:(NSInvocation *)invocation { |
---|
347 | SEL invocationSelector = [invocation selector]; |
---|
348 | if ([reflectionDelegate respondsToSelector:invocationSelector]) |
---|
349 | [invocation invokeWithTarget:reflectionDelegate]; |
---|
350 | else |
---|
351 | [self doesNotRecognizeSelector:invocationSelector]; |
---|
352 | } |
---|
353 | |
---|
354 | - (void)getUrl:(NSAppleEventDescriptor *)event |
---|
355 | withReplyEvent:(NSAppleEventDescriptor *)replyEvent { |
---|
356 | Q_UNUSED(replyEvent); |
---|
357 | NSString *urlString = |
---|
358 | [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; |
---|
359 | // The string we get from the requesting application might not necessarily |
---|
360 | // meet QUrl's requirement for a IDN-compliant host. So if we can't parse into |
---|
361 | // a QUrl, then we pass the string on to the application as the name of a file |
---|
362 | // (and QFileOpenEvent::file is not guaranteed to be the path to a local, |
---|
363 | // open'able file anyway). |
---|
364 | const QString qurlString = QString::fromNSString(urlString); |
---|
365 | if (const QUrl url(qurlString); url.isValid()) |
---|
366 | QWindowSystemInterface::handleFileOpenEvent(url); |
---|
367 | else |
---|
368 | QWindowSystemInterface::handleFileOpenEvent(qurlString); |
---|
369 | } |
---|
370 | |
---|
371 | - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)application { |
---|
372 | // This bit is causing trouble for me, and is not relevant on this system. |
---|
373 | // Do not even try... |
---|
374 | // if (@available(macOS 12, *)) { |
---|
375 | // if ([reflectionDelegate respondsToSelector:_cmd]) |
---|
376 | // return [reflectionDelegate |
---|
377 | // applicationSupportsSecureRestorableState:application]; |
---|
378 | //} |
---|
379 | |
---|
380 | // We don't support or implement state restorations via the AppKit |
---|
381 | // state restoration APIs, but if we did, we would/should support |
---|
382 | // secure state restoration. This is the default for apps linked |
---|
383 | // against the macOS 14 SDK, but as we target versions below that |
---|
384 | // as well we need to return YES here explicitly to silence a runtime |
---|
385 | // warning. |
---|
386 | return YES; |
---|
387 | } |
---|
388 | |
---|
389 | @end |
---|
390 | |
---|
391 | @implementation QCocoaApplicationDelegate (Menus) |
---|
392 | |
---|
393 | - (BOOL)validateMenuItem:(NSMenuItem *)item { |
---|
394 | qCDebug(lcQpaMenus) << "Validating" << item << "for" << self; |
---|
395 | |
---|
396 | auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item); |
---|
397 | if (!nativeItem) |
---|
398 | return item |
---|
399 | .enabled; // FIXME Test with with Qt as plugin or embedded QWindow. |
---|
400 | |
---|
401 | auto *platformItem = nativeItem.platformMenuItem; |
---|
402 | if (!platformItem) // Try a bit harder with orphan menu items |
---|
403 | return item.hasSubmenu || |
---|
404 | (item.enabled && (item.action != @selector(qt_itemFired:))); |
---|
405 | |
---|
406 | // Menu-holding items are always enabled, as it's conventional in Cocoa |
---|
407 | if (platformItem->menu()) |
---|
408 | return YES; |
---|
409 | |
---|
410 | return platformItem->isEnabled(); |
---|
411 | } |
---|
412 | |
---|
413 | @end |
---|
414 | |
---|
415 | @implementation QCocoaApplicationDelegate (MenuAPI) |
---|
416 | |
---|
417 | - (void)qt_itemFired:(QCocoaNSMenuItem *)item { |
---|
418 | qCDebug(lcQpaMenus) << "Activating" << item; |
---|
419 | |
---|
420 | if (item.hasSubmenu) |
---|
421 | return; |
---|
422 | |
---|
423 | auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item); |
---|
424 | Q_ASSERT_X(nativeItem, qPrintable(__FUNCTION__), |
---|
425 | "Triggered menu item is not a QCocoaNSMenuItem."); |
---|
426 | auto *platformItem = nativeItem.platformMenuItem; |
---|
427 | // Menu-holding items also get a target to play nicely |
---|
428 | // with NSMenuValidation but should not trigger. |
---|
429 | if (!platformItem || platformItem->menu()) |
---|
430 | return; |
---|
431 | |
---|
432 | QScopedScopeLevelCounter scopeLevelCounter( |
---|
433 | QGuiApplicationPrivate::instance()->threadData.loadRelaxed()); |
---|
434 | QGuiApplicationPrivate::modifier_buttons = |
---|
435 | QAppleKeyMapper::fromCocoaModifiers([NSEvent modifierFlags]); |
---|
436 | |
---|
437 | static QMetaMethod activatedSignal = |
---|
438 | QMetaMethod::fromSignal(&QCocoaMenuItem::activated); |
---|
439 | activatedSignal.invoke(platformItem, Qt::QueuedConnection); |
---|
440 | } |
---|
441 | |
---|
442 | @end |
---|