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