Ticket #70655: qcocoaapplicationdelegate.mm

File qcocoaapplicationdelegate.mm, 16.5 KB (added by Gandoon (Erik Hedlund), 3 months ago)

Workaround for (some) systems older than 12

Line 
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
64QT_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