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 (QCocoaWindow::s_applicationActivationObserver) {
253 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:QCocoaWindow::s_applicationActivationObserver];
254 QCocoaWindow::s_applicationActivationObserver = nil;
255 }
256
257 if ([reflectionDelegate respondsToSelector:_cmd])
258 [reflectionDelegate applicationDidBecomeActive:notification];
259
260 QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationActive);
261
262 if (QCocoaWindow::s_windowUnderMouse) {
263 QPointF windowPoint;
264 QPointF screenPoint;
265 QNSView *view = qnsview_cast(QCocoaWindow::s_windowUnderMouse->m_view);
266 [view convertFromScreen:[NSEvent mouseLocation] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
267 QWindow *windowUnderMouse = QCocoaWindow::s_windowUnderMouse->window();
268 qCInfo(lcQpaMouse) << "Application activated with mouse at" << windowPoint << "; sending" << QEvent::Enter << "to" << windowUnderMouse;
269 QWindowSystemInterface::handleEnterEvent(windowUnderMouse, windowPoint, screenPoint);
270 }
271}
272
273- (void)applicationDidResignActive:(NSNotification *)notification
274{
275 if ([reflectionDelegate respondsToSelector:_cmd])
276 [reflectionDelegate applicationDidResignActive:notification];
277
278 QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationInactive);
279
280 if (QCocoaWindow::s_windowUnderMouse) {
281 QWindow *windowUnderMouse = QCocoaWindow::s_windowUnderMouse->window();
282 qCInfo(lcQpaMouse) << "Application deactivated; sending" << QEvent::Leave << "to" << windowUnderMouse;
283 QWindowSystemInterface::handleLeaveEvent(windowUnderMouse);
284 }
285}
286
287/*!
288 Returns a Boolean value that indicates if the app responds
289 to reopen AppleEvents.
290
291 These events are sent whenever the Finder reactivates an already
292 running application because someone double-clicked it again or used
293 the dock to activate it.
294
295 We pass the activation on to Qt, and return YES, to let AppKit
296 follow its normal flow.
297 */
298- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag
299{
300 if ([reflectionDelegate respondsToSelector:_cmd])
301 return [reflectionDelegate applicationShouldHandleReopen:theApplication hasVisibleWindows:flag];
302
303 /*
304 true to force delivery of the event even if the application state is already active,
305 because rapp (handle reopen) events are sent each time the dock icon is clicked regardless
306 of the active state of the application or number of visible windows. For example, a browser
307 app that has no windows opened would need the event be to delivered even if it was already
308 active in order to create a new window as per OS X conventions.
309 */
310 QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationActive, true /*forcePropagate*/);
311
312 return YES;
313}
314
315- (void)setReflectionDelegate:(NSObject <NSApplicationDelegate> *)oldDelegate
316{
317 [oldDelegate retain];
318 [reflectionDelegate release];
319 reflectionDelegate = oldDelegate;
320}
321
322- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
323{
324 NSMethodSignature *result = [super methodSignatureForSelector:aSelector];
325 if (!result && reflectionDelegate) {
326 result = [reflectionDelegate methodSignatureForSelector:aSelector];
327 }
328 return result;
329}
330
331- (BOOL)respondsToSelector:(SEL)aSelector
332{
333 return [super respondsToSelector:aSelector] || [reflectionDelegate respondsToSelector:aSelector];
334}
335
336- (void)forwardInvocation:(NSInvocation *)invocation
337{
338 SEL invocationSelector = [invocation selector];
339 if ([reflectionDelegate respondsToSelector:invocationSelector])
340 [invocation invokeWithTarget:reflectionDelegate];
341 else
342 [self doesNotRecognizeSelector:invocationSelector];
343}
344
345/*!
346 Callback for when the application is asked to pick up a user activity
347 from another app (also known as Handoff, which is part of the bigger
348 Continuity story for Apple operating systems).
349
350 This is normally managed by two apps by the same vendor explicitly
351 initiating a custom NSUserActivity and picking it up in another app
352 on the same or another device, which we don't have APIs for.
353
354 This is also how the system supports Universal Links, where a web page
355 can deep-link into an app. In this case the app needs to claim and
356 validate an associated domain. The resulting link will be delivered
357 as a special NSUserActivityTypeBrowsingWeb activity type, which we
358 treat as QDesktopServices::handleUrl().
359
360 Finally, for NS/UIDocument based apps (which Qt is not), the system
361 automatically handles document hand-off if the application includes
362 the NSUbiquitousDocumentUserActivityType key in its Info.plist.
363 */
364- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity
365 restorationHandler:(void(^)(NSArray<id<NSUserActivityRestoring>> *restorableObjects))restorationHandler
366{
367 // Check if eg. user has installed an app delegate capable of handling this
368 if ([reflectionDelegate respondsToSelector:_cmd]
369 && [reflectionDelegate application:application continueUserActivity:userActivity
370 restorationHandler:restorationHandler] == YES) {
371 return YES;
372 }
373
374 if (!QGuiApplication::instance())
375 return NO;
376
377 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
378 QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance();
379 Q_ASSERT(cocoaIntegration);
380 return cocoaIntegration->services()->handleUrl(QUrl::fromNSURL(userActivity.webpageURL));
381 }
382
383 return NO;
384}
385
386/*!
387 Callback for when the app is asked to open custom URL schemes.
388
389 We register a handler for events of type kInternetEventClass with the
390 NSAppleEventManager during application start.
391
392 The application must include the schemes in the CFBundleURLTypes
393 key of the Info.plist.
394
395 \note This callback is not used for http/https URLs, see
396 continueUserActivity above for how we handle that.
397 */
398- (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
399{
400 Q_UNUSED(replyEvent);
401
402 NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
403 const QString qurlString = QString::fromNSString(urlString);
404
405 if (event.eventClass == kInternetEventClass && event.eventID == kAEGetURL) {
406 // 'GURL' (Get URL) event this application should handle
407 if (!QGuiApplication::instance())
408 return;
409 QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance();
410 Q_ASSERT(cocoaIntegration);
411 if (cocoaIntegration->services()->handleUrl(QUrl(qurlString)))
412 return;
413 }
414
415 // The string we get from the requesting application might not necessarily meet
416 // QUrl's requirement for a IDN-compliant host. So if we can't parse into a QUrl,
417 // then we pass the string on to the application as the name of a file (and
418 // QFileOpenEvent::file is not guaranteed to be the path to a local, open'able
419 // file anyway).
420 if (const QUrl url(qurlString); url.isValid())
421 QWindowSystemInterface::handleFileOpenEvent(url);
422 else
423 QWindowSystemInterface::handleFileOpenEvent(qurlString);
424}
425
426- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)application
427{
428 if (@available(macOS 12, *)) {
429 if ([reflectionDelegate respondsToSelector:_cmd])
430 return [reflectionDelegate applicationSupportsSecureRestorableState:application];
431 }
432
433 // We don't support or implement state restorations via the AppKit
434 // state restoration APIs, but if we did, we would/should support
435 // secure state restoration. This is the default for apps linked
436 // against the macOS 14 SDK, but as we target versions below that
437 // as well we need to return YES here explicitly to silence a runtime
438 // warning.
439 return YES;
440}
441
442@end
443
444@implementation QCocoaApplicationDelegate (Menus)
445
446- (BOOL)validateMenuItem:(NSMenuItem*)item
447{
448 qCDebug(lcQpaMenus) << "Validating" << item << "for" << self;
449
450 auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item);
451 if (!nativeItem)
452 return item.enabled; // FIXME Test with with Qt as plugin or embedded QWindow.
453
454 auto *platformItem = nativeItem.platformMenuItem;
455 if (!platformItem) // Try a bit harder with orphan menu items
456 return item.hasSubmenu || (item.enabled && (item.action != @selector(qt_itemFired:)));
457
458 // Menu-holding items are always enabled, as it's conventional in Cocoa
459 if (platformItem->menu())
460 return YES;
461
462 return platformItem->isEnabled();
463}
464
465@end
466
467@implementation QCocoaApplicationDelegate (MenuAPI)
468
469- (void)qt_itemFired:(QCocoaNSMenuItem *)item
470{
471 qCDebug(lcQpaMenus) << "Activating" << item;
472
473 if (item.hasSubmenu)
474 return;
475
476 auto *nativeItem = qt_objc_cast<QCocoaNSMenuItem *>(item);
477 Q_ASSERT_X(nativeItem, qPrintable(__FUNCTION__), "Triggered menu item is not a QCocoaNSMenuItem.");
478 auto *platformItem = nativeItem.platformMenuItem;
479 // Menu-holding items also get a target to play nicely
480 // with NSMenuValidation but should not trigger.
481 if (!platformItem || platformItem->menu())
482 return;
483
484 QGuiApplicationPrivate::modifier_buttons = QAppleKeyMapper::fromCocoaModifiers([NSEvent modifierFlags]);
485
486 static QMetaMethod activatedSignal = QMetaMethod::fromSignal(&QCocoaMenuItem::activated);
487 activatedSignal.invoke(platformItem, Qt::QueuedConnection);
488}
489
490@end
Q_FORWARD_DECLARE_OBJC_CLASS(NSMenu)
#define qApp