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
qcocoaapplicationdelegate.mm
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// Copyright (c) 2007-2008, Apple, Inc.
3// SPDX-License-Identifier: BSD-3-Clause
4// Qt-Security score:significant reason:default
5
6#include <AppKit/AppKit.h>
7
10#include "qcocoamenubar.h"
11#include "qcocoamenu.h"
13#include "qcocoamenuitem.h"
14#include "qcocoansmenu.h"
15#include "qcocoahelpers.h"
16
17#if QT_CONFIG(sessionmanager)
18# include "qcocoasessionmanager.h"
19#endif
20
21#include <qevent.h>
22#include <qurl.h>
23#include <qdebug.h>
24#include <qguiapplication.h>
25#include <qpa/qwindowsysteminterface.h>
26#include <qwindowdefs.h>
27
28#include <QtCore/private/qdarwinsecurityscopedfileengine_p.h>
29
30QT_USE_NAMESPACE
31
32@implementation QCocoaApplicationDelegate {
33 NSObject <NSApplicationDelegate> *reflectionDelegate;
34 bool inLaunch;
35}
36
37+ (instancetype)sharedDelegate
38{
39 static QCocoaApplicationDelegate *shared = nil;
40 static dispatch_once_t onceToken;
41 dispatch_once(&onceToken, ^{
42 shared = [[self alloc] init];
43 atexit_b(^{
44 [shared release];
45 shared = nil;
46 });
47 });
48 return shared;
49}
50
51- (instancetype)init
52{
53 self = [super init];
54 if (self) {
55 inLaunch = true;
56 }
57 return self;
58}
59
60- (void)dealloc
61{
62 [_dockMenu release];
63 if (reflectionDelegate) {
64 [[NSApplication sharedApplication] setDelegate:reflectionDelegate];
65 [reflectionDelegate release];
66 }
67 [[NSNotificationCenter defaultCenter] removeObserver:self];
68
69 [super dealloc];
70}
71
72- (NSMenu *)applicationDockMenu:(NSApplication *)sender
73{
74 Q_UNUSED(sender);
75 // Manually invoke the delegate's -menuWillOpen: method.
76 // See QTBUG-39604 (and its fix) for details.
77 [self.dockMenu.delegate menuWillOpen:self.dockMenu];
78 return [[self.dockMenu retain] autorelease];
79}
80
81// This function will only be called when NSApp is actually running.
82- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
83{
84 if ([reflectionDelegate respondsToSelector:_cmd])
85 return [reflectionDelegate applicationShouldTerminate:sender];
86
87 if (QGuiApplicationPrivate::instance()->threadData.loadRelaxed()->eventLoops.isEmpty()) {
88 // No event loop is executing. This probably means that Qt is used as a plugin,
89 // or as a part of a native Cocoa application. In any case it should be fine to
90 // terminate now.
91 qCDebug(lcQpaApplication) << "No running event loops, terminating now";
92 return NSTerminateNow;
93 }
94
95#if QT_CONFIG(sessionmanager)
96 QCocoaSessionManager *cocoaSessionManager = QCocoaSessionManager::instance();
97 cocoaSessionManager->resetCancellation();
98 cocoaSessionManager->appCommitData();
99
100 if (cocoaSessionManager->wasCanceled()) {
101 qCDebug(lcQpaApplication) << "Session management canceled application termination";
102 return NSTerminateCancel;
103 }
104#endif
105
106 if (!QWindowSystemInterface::handleApplicationTermination<QWindowSystemInterface::SynchronousDelivery>()) {
107 qCDebug(lcQpaApplication) << "Application termination canceled";
108 return NSTerminateCancel;
109 }
110
111 // Even if the application termination was accepted by the application we can't
112 // return NSTerminateNow, as that would trigger AppKit to ultimately call exit().
113 // We need to ensure that the runloop continues spinning so that we can return
114 // from our own event loop back to main(), and exit from there.
115 qCDebug(lcQpaApplication) << "Termination accepted, but returning to runloop for exit through main()";
116 return NSTerminateCancel;
117}
118
119- (void)applicationWillFinishLaunching:(NSNotification *)notification
120{
121 if ([reflectionDelegate respondsToSelector:_cmd])
122 [reflectionDelegate applicationWillFinishLaunching:notification];
123
124 /*
125 From the Cocoa documentation: "A good place to install event handlers
126 is in the applicationWillFinishLaunching: method of the application
127 delegate. At that point, the Application Kit has installed its default
128 event handlers, so if you install a handler for one of the same events,
129 it will replace the Application Kit version."
130 */
131
132 /*
133 If Qt is used as a plugin, we let the 3rd party application handle
134 events like quit and open file events. Otherwise, if we install our own
135 handlers, we easily end up breaking functionality the 3rd party
136 application depends on.
137 */
138 NSAppleEventManager *eventManager = [NSAppleEventManager sharedAppleEventManager];
139 [eventManager setEventHandler:self
140 andSelector:@selector(getUrl:withReplyEvent:)
141 forEventClass:kInternetEventClass
142 andEventID:kAEGetURL];
143}
144
145// called by QCocoaIntegration's destructor before resetting the application delegate to nil
146- (void)removeAppleEventHandlers
147{
148 NSAppleEventManager *eventManager = [NSAppleEventManager sharedAppleEventManager];
149 [eventManager removeEventHandlerForEventClass:kInternetEventClass andEventID:kAEGetURL];
150}
151
152- (bool)inLaunch
153{
154 return inLaunch;
155}
156
157- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
158{
159 if ([reflectionDelegate respondsToSelector:_cmd])
160 [reflectionDelegate applicationDidFinishLaunching:aNotification];
161
162 inLaunch = false;
163
164 if (qEnvironmentVariableIsEmpty("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM")) {
165 auto currentApplication = NSRunningApplication.currentApplication;
166 if (!currentApplication.active) {
167 // Move the application to front to avoid launching behind the terminal.
168 // Ignoring other apps is necessary (we must ignore the terminal), but makes
169 // Qt apps play slightly less nice with other apps when launching from Finder
170 // (see the activateIgnoringOtherApps docs). FIXME: Try to distinguish between
171 // being non-active here because another application stole activation in the
172 // time it took us to launch from Finder, and being non-active because we were
173 // launched from Terminal or something that doesn't activate us at all.
174 auto frontmostApplication = NSWorkspace.sharedWorkspace.frontmostApplication;
175 qCDebug(lcQpaApplication) << "Launched with" << frontmostApplication
176 << "as frontmost application. Activating" << currentApplication << "instead.";
177 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
178 }
179
180 if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSSonoma) {
181 // Qt windows are typically shown in main(), at which point the application
182 // is not active yet. When the application is activated, either externally
183 // or via the override above, it will only bring the main and key windows
184 // forward, which differs from the behavior if these windows had been shown
185 // once the application was already active. To work around this, we explicitly
186 // activate the current application again, bringing all windows to the front.
187 // We only do this on Sonoma and up, as earlier macOS versions have a bug where
188 // the app will deactivate as part of activating, even if it's active app,
189 // which in turn results in losing key window status for the key window.
190 // FIXME: Consider bringing our windows to the front via orderFront instead,
191 // or deferring the orderFront during setVisible until the app is active.
192 [currentApplication activateWithOptions:NSApplicationActivateAllWindows];
193 }
194 }
195
196 QCocoaMenuBar::insertWindowMenu();
197}
198
199/*!
200 Tells the delegate to open the specified files
201
202 Sent by the system when the user drags a file to the app's icon
203 in places like Finder or the Dock, or opens a file via the "Open
204 With" menu in Finder.
205
206 These actions can happen when the application is not running,
207 in which case the call comes in between willFinishLaunching
208 and didFinishLaunching. In this case we don't pass on the
209 incoming file paths as file open events, as the paths are
210 also part of the command line arguments, and Qt applications
211 normally expect to handle file opening via those.
212
213 \note The app must register itself as a handler for each file
214 type via the CFBundleDocumentTypes key in the Info.plist.
215 */
216- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
217{
218 Q_UNUSED(filenames);
219 Q_UNUSED(sender);
220
221 for (NSString *fileName in filenames) {
222 QString qtFileName = QString::fromNSString(fileName);
223 if (inLaunch) {
224 // We need to be careful because Cocoa will be nice enough to take
225 // command line arguments and send them to us as events. Given the history
226 // of Qt Applications, this will result in behavior people don't want, as
227 // they might be doing the opening themselves with the command line parsing.
228 if (qApp->arguments().contains(qtFileName))
229 continue;
230 }
231 QUrl url = qt_apple_urlFromPossiblySecurityScopedURL([NSURL fileURLWithPath:fileName]);
232 QWindowSystemInterface::handleFileOpenEvent(url);
233 // FIXME: We're supposed to call [NSApp replyToOpenOrPrint:] here, but we
234 // don't know if the open operation succeeded, failed, or was cancelled.
235 }
236
237 if ([reflectionDelegate respondsToSelector:_cmd])
238 [reflectionDelegate application:sender openFiles:filenames];
239
240}
241
242- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
243{
244 if ([reflectionDelegate respondsToSelector:_cmd])
245 return [reflectionDelegate applicationShouldTerminateAfterLastWindowClosed:sender];
246
247 return NO; // Someday qApp->quitOnLastWindowClosed(); when QApp and NSApp work closer together.
248}
249
250- (void)applicationDidBecomeActive:(NSNotification *)notification
251{
252 if ([reflectionDelegate respondsToSelector:_cmd])
253 [reflectionDelegate applicationDidBecomeActive:notification];
254
255 QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationActive);
256
257 if (QCocoaWindow::s_windowUnderMouse) {
258 QPointF windowPoint;
259 QPointF screenPoint;
260 QNSView *view = qnsview_cast(QCocoaWindow::s_windowUnderMouse->m_view);
261 [view convertFromScreen:[NSEvent mouseLocation] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
262 QWindow *windowUnderMouse = QCocoaWindow::s_windowUnderMouse->window();
263 qCInfo(lcQpaMouse) << "Application activated with mouse at" << windowPoint << "; sending" << QEvent::Enter << "to" << windowUnderMouse;
264 QWindowSystemInterface::handleEnterEvent(windowUnderMouse, windowPoint, screenPoint);
265 }
266}
267
268- (void)applicationDidResignActive:(NSNotification *)notification
269{
270 if ([reflectionDelegate respondsToSelector:_cmd])
271 [reflectionDelegate applicationDidResignActive:notification];
272
273 QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationInactive);
274
275 if (QCocoaWindow::s_windowUnderMouse) {
276 QWindow *windowUnderMouse = QCocoaWindow::s_windowUnderMouse->window();
277 qCInfo(lcQpaMouse) << "Application deactivated; sending" << QEvent::Leave << "to" << windowUnderMouse;
278 QWindowSystemInterface::handleLeaveEvent(windowUnderMouse);
279 }
280}
281
282/*!
283 Returns a Boolean value that indicates if the app responds
284 to reopen AppleEvents.
285
286 These events are sent whenever the Finder reactivates an already
287 running application because someone double-clicked it again or used
288 the dock to activate it.
289
290 We pass the activation on to Qt, and return YES, to let AppKit
291 follow its normal flow.
292 */
293- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag
294{
295 if ([reflectionDelegate respondsToSelector:_cmd])
296 return [reflectionDelegate applicationShouldHandleReopen:theApplication hasVisibleWindows:flag];
297
298 /*
299 true to force delivery of the event even if the application state is already active,
300 because rapp (handle reopen) events are sent each time the dock icon is clicked regardless
301 of the active state of the application or number of visible windows. For example, a browser
302 app that has no windows opened would need the event be to delivered even if it was already
303 active in order to create a new window as per OS X conventions.
304 */
305 QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationActive, true /*forcePropagate*/);
306
307 return YES;
308}
309
310- (void)setReflectionDelegate:(NSObject <NSApplicationDelegate> *)oldDelegate
311{
312 [oldDelegate retain];
313 [reflectionDelegate release];
314 reflectionDelegate = oldDelegate;
315}
316
317- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
318{
319 NSMethodSignature *result = [super methodSignatureForSelector:aSelector];
320 if (!result && reflectionDelegate) {
321 result = [reflectionDelegate methodSignatureForSelector:aSelector];
322 }
323 return result;
324}
325
326- (BOOL)respondsToSelector:(SEL)aSelector
327{
328 return [super respondsToSelector:aSelector] || [reflectionDelegate respondsToSelector:aSelector];
329}
330
331- (void)forwardInvocation:(NSInvocation *)invocation
332{
333 SEL invocationSelector = [invocation selector];
334 if ([reflectionDelegate respondsToSelector:invocationSelector])
335 [invocation invokeWithTarget:reflectionDelegate];
336 else
337 [self doesNotRecognizeSelector:invocationSelector];
338}
339
340/*!
341 Callback for when the application is asked to pick up a user activity
342 from another app (also known as Handoff, which is part of the bigger
343 Continuity story for Apple operating systems).
344
345 This is normally managed by two apps by the same vendor explicitly
346 initiating a custom NSUserActivity and picking it up in another app
347 on the same or another device, which we don't have APIs for.
348
349 This is also how the system supports Universal Links, where a web page
350 can deep-link into an app. In this case the app needs to claim and
351 validate an associated domain. The resulting link will be delivered
352 as a special NSUserActivityTypeBrowsingWeb activity type, which we
353 treat as QDesktopServices::handleUrl().
354
355 Finally, for NS/UIDocument based apps (which Qt is not), the system
356 automatically handles document hand-off if the application includes
357 the NSUbiquitousDocumentUserActivityType key in its Info.plist.
358 */
359- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity
360 restorationHandler:(void(^)(NSArray<id<NSUserActivityRestoring>> *restorableObjects))restorationHandler
361{
362 // Check if eg. user has installed an app delegate capable of handling this
363 if ([reflectionDelegate respondsToSelector:_cmd]
364 && [reflectionDelegate application:application continueUserActivity:userActivity
365 restorationHandler:restorationHandler] == YES) {
366 return YES;
367 }
368
369 if (!QGuiApplication::instance())
370 return NO;
371
372 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
373 QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance();
374 Q_ASSERT(cocoaIntegration);
375 return cocoaIntegration->services()->handleUrl(QUrl::fromNSURL(userActivity.webpageURL));
376 }
377
378 return NO;
379}
380
381/*!
382 Callback for when the app is asked to open custom URL schemes.
383
384 We register a handler for events of type kInternetEventClass with the
385 NSAppleEventManager during application start.
386
387 The application must include the schemes in the CFBundleURLTypes
388 key of the Info.plist.
389
390 \note This callback is not used for http/https URLs, see
391 continueUserActivity above for how we handle that.
392 */
393- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
394{
395 Q_UNUSED(replyEvent);
396
397 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
398 const QString qurlString = QString::fromNSString(urlString);
399
400 if (event.eventClass == kInternetEventClass && event.eventID == kAEGetURL) {
401 // 'GURL' (Get URL) event this application should handle
402 if (!QGuiApplication::instance())
403 return;
404 QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance();
405 Q_ASSERT(cocoaIntegration);
406 if (cocoaIntegration->services()->handleUrl(QUrl(qurlString)))
407 return;
408 }
409
410 // The string we get from the requesting application might not necessarily meet
411 // QUrl's requirement for a IDN-compliant host. So if we can't parse into a QUrl,
412 // then we pass the string on to the application as the name of a file (and
413 // QFileOpenEvent::file is not guaranteed to be the path to a local, open'able
414 // file anyway).
415 if (const QUrl url(qurlString); url.isValid())
416 QWindowSystemInterface::handleFileOpenEvent(url);
417 else
418 QWindowSystemInterface::handleFileOpenEvent(qurlString);
419}
420
421- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)application
422{
423 if ([reflectionDelegate respondsToSelector:_cmd])
424 return [reflectionDelegate applicationSupportsSecureRestorableState:application];
425
426 // We don't support or implement state restorations via the AppKit
427 // state restoration APIs, but if we did, we would/should support
428 // secure state restoration. This is the default for apps linked
429 // against the macOS 14 SDK, but as we target versions below that
430 // as well we need to return YES here explicitly to silence a runtime
431 // warning.
432 return YES;
433}
434
435@end
436
437@implementation QCocoaApplicationDelegate (Menus)
438
439- (BOOL)validateMenuItem:(NSMenuItem*)item
440{
441 qCDebug(lcQpaMenus) << "Validating" << item << "for" << self;
442
443 auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item);
444 if (!nativeItem)
445 return item.enabled; // FIXME Test with with Qt as plugin or embedded QWindow.
446
447 auto *platformItem = nativeItem.platformMenuItem;
448 if (!platformItem) // Try a bit harder with orphan menu items
449 return item.hasSubmenu || (item.enabled && (item.action != @selector(qt_itemFired:)));
450
451 // Menu-holding items are always enabled, as it's conventional in Cocoa
452 if (platformItem->menu())
453 return YES;
454
455 return platformItem->isEnabled();
456}
457
458@end
459
460@implementation QCocoaApplicationDelegate (MenuAPI)
461
462- (void)qt_itemFired:(QCocoaNSMenuItem *)item
463{
464 qCDebug(lcQpaMenus) << "Activating" << item;
465
466 if (item.hasSubmenu)
467 return;
468
469 auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item);
470 Q_ASSERT_X(nativeItem, qPrintable(__FUNCTION__), "Triggered menu item is not a QCocoaNSMenuItem.");
471 auto *platformItem = nativeItem.platformMenuItem;
472 // Menu-holding items also get a target to play nicely
473 // with NSMenuValidation but should not trigger.
474 if (!platformItem || platformItem->menu())
475 return;
476
477 QGuiApplicationPrivate::modifier_buttons = QAppleKeyMapper::fromCocoaModifiers([NSEvent modifierFlags]);
478
479 static QMetaMethod activatedSignal = QMetaMethod::fromSignal(&QCocoaMenuItem::activated);
480 activatedSignal.invoke(platformItem, Qt::QueuedConnection);
481}
482
483@end
Q_FORWARD_DECLARE_OBJC_CLASS(NSMenu)
#define qApp