VirtualBox

source: vbox/trunk/src/VBox/Frontends/VirtualBox/src/platform/darwin/UICocoaApplication.mm

Last change on this file was 103963, checked in by vboxsync, 8 weeks ago

FE/Qt: bugref:10623: macOS: Prevent AppNap for VirtualBoxVM.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.2 KB
Line 
1/* $Id: UICocoaApplication.mm 103963 2024-03-20 14:49:38Z vboxsync $ */
2/** @file
3 * VBox Qt GUI - UICocoaApplication class implementation.
4 */
5
6/*
7 * Copyright (C) 2009-2023 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28/* GUI includes: */
29#include "UICocoaApplication.h"
30
31/* Other VBox includes: */
32#include <iprt/assert.h>
33
34/* External includes: */
35#import <AppKit/NSApplication.h>
36#import <AppKit/NSButton.h>
37#import <AppKit/NSEvent.h>
38#import <AppKit/NSWindow.h>
39#import <Foundation/NSArray.h>
40
41
42/** Class for tracking a callback. */
43@interface CallbackData : NSObject
44{
45@public
46 /** Holds the mask of events to send to this callback. */
47 uint32_t fMask;
48 /** Holds the callback. */
49 PFNVBOXCACALLBACK pfnCallback;
50 /** Holds the user argument. */
51 void *pvUser;
52}
53- (id)initWithMask :(uint32)mask callback :(PFNVBOXCACALLBACK)callback user :(void *)user;
54@end /* @interface CallbackData */
55
56@implementation CallbackData
57/** Performs initialization. */
58- (id)initWithMask :(uint32)mask callback :(PFNVBOXCACALLBACK)callback user :(void *)user
59{
60 self = [super init];
61 if (self)
62 {
63 fMask = mask;
64 pfnCallback = callback;
65 pvUser = user;
66 }
67 return self;
68}
69@end /* @implementation CallbackData */
70
71
72/** Class for event handling. */
73@interface UICocoaApplicationPrivate : NSApplication
74{
75 /** The event mask for which there currently are callbacks. */
76 uint32_t m_fMask;
77 /** Array of callbacks. */
78 NSMutableArray *m_pCallbacks;
79 /** AppNap preventing activity. */
80 id <NSObject> m_activity;
81}
82- (id)init;
83- (void)sendEvent :(NSEvent *)theEvent;
84- (void)setCallback :(uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser;
85- (void)unsetCallback :(uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser;
86
87- (void)finishLaunching;
88- (void)terminate :(nullable id)sender;
89
90- (void)registerToNotificationOfWorkspace :(NSString *)pstrNotificationName;
91- (void)unregisterFromNotificationOfWorkspace :(NSString *)pstrNotificationName;
92
93- (void)registerToNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow;
94- (void)unregisterFromNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow;
95
96- (void)notificationCallbackOfObject :(NSNotification *)notification;
97- (void)notificationCallbackOfWindow :(NSNotification *)notification;
98
99- (void)registerSelectorForStandardWindowButton :(NSWindow *)pWindow :(StandardWindowButtonType)enmButtonType;
100- (void)selectorForStandardWindowButton :(NSButton *)pButton;
101@end /* @interface UICocoaApplicationPrivate */
102
103@implementation UICocoaApplicationPrivate
104/** Performs initialization. */
105- (id) init
106{
107 self = [super init];
108 if (self)
109 m_pCallbacks = [[NSMutableArray alloc] init];
110
111 // WORKAROUND:
112 // Gently disable El Capitan tries to break everything with the Enter Full Screen action.
113 // S.a. https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKit/ for reference.
114 [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
115
116 return self;
117}
118
119/** Sends an event.
120 * @param pEvent Brings the event to be sent. */
121- (void) sendEvent :(NSEvent *)pEvent
122{
123 /* Check if the type matches any of the registered callbacks. */
124 uint32_t const fMask = m_fMask;
125#if 0 /* for debugging */
126 ::darwinPrintEvent("sendEvent: ", pEvent);
127#endif
128 if (fMask != 0)
129 {
130 NSEventType EvtType = [pEvent type];
131 uint32_t fEvtMask = RT_LIKELY(EvtType < 32) ? RT_BIT_32(EvtType) : 0;
132 if (fMask & fEvtMask)
133 {
134 /* Do the callouts in LIFO order. */
135 for (CallbackData *pData in [m_pCallbacks reverseObjectEnumerator])
136 {
137 if (pData->fMask & fEvtMask)
138 {
139 if (pData->pfnCallback(pEvent, [pEvent eventRef], pData->pvUser))
140 return;
141 }
142
143 }
144 }
145 }
146
147 /* Get on with it. */
148 [super sendEvent:pEvent];
149}
150
151/** Registers an event callback.
152 * @param fMask Brings the event mask for which the callback is to be invoked.
153 * @param pfnCallback Brings the callback function.
154 * @param pvUser Brings the user argument. */
155- (void) setCallback :(uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser
156{
157 /* Add the callback data to the array: */
158 CallbackData *pData = [[[CallbackData alloc] initWithMask:fMask callback:pfnCallback user:pvUser] autorelease];
159 [m_pCallbacks addObject:pData];
160
161 /* Update the global mask: */
162 m_fMask |= fMask;
163}
164
165/** Deregisters an event callback.
166 * @param fMask Brings the event mask for which the callback is to be invoked.
167 * @param pfnCallback Brings the callback function.
168 * @param pvUser Brings the user argument. */
169- (void) unsetCallback: (uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser
170{
171 /* Loop the event array LIFO fashion searching for a matching callback. */
172 for (CallbackData *pData in [m_pCallbacks reverseObjectEnumerator])
173 {
174 if ( pData->pfnCallback == pfnCallback
175 && pData->pvUser == pvUser
176 && pData->fMask == fMask)
177 {
178 [m_pCallbacks removeObject:pData];
179 break;
180 }
181 }
182 uint32_t fNewMask = 0;
183 for (CallbackData *pData in m_pCallbacks)
184 fNewMask |= pData->fMask;
185 m_fMask = fNewMask;
186}
187
188/** Standard handler called right after NSApp finished launching. */
189- (void)finishLaunching
190{
191 /* Call to base-class: */
192 [super finishLaunching];
193
194 if (UICocoaApplication::instance()->isPreventAppNap())
195 {
196 //printf("Start activity preventing AppNap!\n");
197 NSActivityOptions options = NSActivityUserInitiatedAllowingIdleSystemSleep;
198 NSString *pstrReason = @"VirtualBox napping is prohibited!";
199 m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:options
200 reason:pstrReason];
201 [m_activity retain];
202 }
203}
204
205/** Standard handler called right before NSApp starting termination. */
206- (void)terminate :(nullable id)sender
207{
208 if (UICocoaApplication::instance()->isPreventAppNap())
209 {
210 //printf("Finish activity preventing AppNap!\n");
211 [[NSProcessInfo processInfo] endActivity:m_activity];
212 [m_activity release];
213 m_activity = Nil;
214 }
215
216 /* Call to base-class: */
217 [super terminate: sender];
218}
219
220/** Registers to cocoa notification @a pstrNotificationName. */
221- (void) registerToNotificationOfWorkspace :(NSString *)pstrNotificationName
222{
223 /* Register notification observer: */
224 NSNotificationCenter *pNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
225 [pNotificationCenter addObserver:self
226 selector:@selector(notificationCallbackOfObject:)
227 name:pstrNotificationName
228 object:nil];
229}
230
231/** Unregister @a pWindow from cocoa notification @a pstrNotificationName. */
232- (void) unregisterFromNotificationOfWorkspace :(NSString *)pstrNotificationName
233{
234 /* Uninstall notification observer: */
235 NSNotificationCenter *pNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
236 [pNotificationCenter removeObserver:self
237 name:pstrNotificationName
238 object:nil];
239}
240
241/** Register @a pWindow to cocoa notification @a pstrNotificationName. */
242- (void) registerToNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow
243{
244 /* Register notification observer: */
245 [[NSNotificationCenter defaultCenter] addObserver:self
246 selector:@selector(notificationCallbackOfWindow:)
247 name:pstrNotificationName
248 object:pWindow];
249}
250
251/** Unregister @a pWindow from cocoa notification @a pstrNotificationName. */
252- (void) unregisterFromNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow
253{
254 /* Uninstall notification observer: */
255 [[NSNotificationCenter defaultCenter] removeObserver:self
256 name:pstrNotificationName
257 object:pWindow];
258}
259
260/** Redirects cocoa @a notification to UICocoaApplication instance. */
261- (void) notificationCallbackOfObject :(NSNotification *)notification
262{
263 /* Get current notification name: */
264 NSString *pstrName = [notification name];
265
266 /* Prepare user-info: */
267 QMap<QString, QString> userInfo;
268
269 /* Process known notifications: */
270 if ( [pstrName isEqualToString :@"NSWorkspaceDidActivateApplicationNotification"]
271 || [pstrName isEqualToString :@"NSWorkspaceDidDeactivateApplicationNotification"])
272 {
273 NSDictionary *pUserInfo = [notification userInfo];
274 NSRunningApplication *pApplication = [pUserInfo valueForKey :@"NSWorkspaceApplicationKey"];
275 NSString *pstrBundleIndentifier = [pApplication bundleIdentifier];
276 userInfo.insert("BundleIdentifier", darwinFromNativeString((NativeNSStringRef)pstrBundleIndentifier));
277 }
278
279 /* Redirect known notifications to objects: */
280 UICocoaApplication::instance()->nativeNotificationProxyForObject(pstrName, userInfo);
281}
282
283/** Redirects cocoa @a notification to UICocoaApplication instance. */
284- (void) notificationCallbackOfWindow :(NSNotification *)notification
285{
286 /* Get current notification name: */
287 NSString *pstrName = [notification name];
288
289 /* Redirect known notifications to widgets: */
290 UICocoaApplication::instance()->nativeNotificationProxyForWidget(pstrName, [notification object]);
291}
292
293/** Registers selector for standard window @a enmButtonType of the passed @a pWindow. */
294- (void)registerSelectorForStandardWindowButton :(NSWindow *)pWindow :(StandardWindowButtonType)enmButtonType
295{
296 /* Retrieve corresponding button: */
297 NSButton *pButton = Nil;
298 switch (enmButtonType)
299 {
300 case StandardWindowButtonType_Close: pButton = [pWindow standardWindowButton:NSWindowCloseButton]; break;
301 case StandardWindowButtonType_Miniaturize: pButton = [pWindow standardWindowButton:NSWindowMiniaturizeButton]; break;
302 case StandardWindowButtonType_Zoom: pButton = [pWindow standardWindowButton:NSWindowZoomButton]; break;
303 case StandardWindowButtonType_Toolbar: pButton = [pWindow standardWindowButton:NSWindowToolbarButton]; break;
304 case StandardWindowButtonType_DocumentIcon: pButton = [pWindow standardWindowButton:NSWindowDocumentIconButton]; break;
305 case StandardWindowButtonType_DocumentVersions: /*pButton = [pWindow standardWindowButton:NSWindowDocumentVersionsButton];*/ break;
306 case StandardWindowButtonType_FullScreen: /*pButton = [pWindow standardWindowButton:NSWindowFullScreenButton];*/ break;
307 }
308
309 /* Register selector if button exists: */
310 if (pButton != Nil)
311 {
312 [pButton setTarget:self];
313 [pButton setAction:@selector(selectorForStandardWindowButton:)];
314 }
315}
316
317/** Redirects selector of the standard window @a pButton to UICocoaApplication instance callback. */
318- (void)selectorForStandardWindowButton :(NSButton *)pButton
319{
320 /* Check if Option key is currently held: */
321 const bool fWithOptionKey = [NSEvent modifierFlags] & NSAlternateKeyMask;
322
323 /* Redirect selector to callback: */
324 UICocoaApplication::instance()->nativeCallbackProxyForStandardWindowButton(pButton, fWithOptionKey);
325}
326@end /* @implementation UICocoaApplicationPrivate */
327
328
329/*********************************************************************************************************************************
330* Class UICocoaApplication implementation. *
331*********************************************************************************************************************************/
332
333/* static */
334UICocoaApplication *UICocoaApplication::s_pInstance = 0;
335
336/* static */
337void UICocoaApplication::create(bool fPreventAppNap)
338{
339 if (!s_pInstance)
340 s_pInstance = new UICocoaApplication(fPreventAppNap);
341}
342
343/* static */
344UICocoaApplication *UICocoaApplication::instance()
345{
346 return s_pInstance;
347}
348
349UICocoaApplication::UICocoaApplication(bool fPreventAppNap)
350 : m_fPreventAppNap(fPreventAppNap)
351{
352 /* Make sure our private NSApplication object is created: */
353 m_pNative = (UICocoaApplicationPrivate*)[UICocoaApplicationPrivate sharedApplication];
354 // WORKAROUND":
355 // Create one auto release pool which is in place for all the
356 // initialization and deinitialization stuff. That is when the
357 // NSApplication is not running the run loop (there is a separate
358 // auto release pool defined).
359 m_pPool = [[NSAutoreleasePool alloc] init];
360}
361
362UICocoaApplication::~UICocoaApplication()
363{
364 [m_pNative release];
365 [m_pPool release];
366}
367
368bool UICocoaApplication::isActive() const
369{
370 return [m_pNative isActive];
371}
372
373bool UICocoaApplication::isDarkMode() const
374{
375 NativeNSStringRef pstrInterfaceMode = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"];
376 return [pstrInterfaceMode isEqualToString :@"Dark"];
377}
378
379void UICocoaApplication::hide()
380{
381 [m_pNative hide:m_pNative];
382}
383
384void UICocoaApplication::hideUserElements()
385{
386 [m_pNative setPresentationOptions:NSApplicationPresentationHideMenuBar | NSApplicationPresentationHideDock];
387}
388
389void UICocoaApplication::registerForNativeEvents(uint32_t fMask, PFNVBOXCACALLBACK pfnCallback, void *pvUser)
390{
391 [m_pNative setCallback:fMask :pfnCallback :pvUser];
392}
393
394void UICocoaApplication::unregisterForNativeEvents(uint32_t fMask, PFNVBOXCACALLBACK pfnCallback, void *pvUser)
395{
396 [m_pNative unsetCallback:fMask :pfnCallback :pvUser];
397}
398
399void UICocoaApplication::registerToNotificationOfWorkspace(const QString &strNativeNotificationName, QObject *pObject,
400 PfnNativeNotificationCallbackForQObject pCallback)
401{
402 /* Make sure it is not registered yet: */
403 AssertReturnVoid(!m_objectCallbacks.contains(pObject) || !m_objectCallbacks[pObject].contains(strNativeNotificationName));
404
405 /* Remember callback: */
406 m_objectCallbacks[pObject][strNativeNotificationName] = pCallback;
407
408 /* Register observer: */
409 NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
410 [m_pNative registerToNotificationOfWorkspace :pstrNativeNotificationName];
411}
412
413void UICocoaApplication::unregisterFromNotificationOfWorkspace(const QString &strNativeNotificationName, QObject *pObject)
414{
415 /* Make sure it is registered yet: */
416 AssertReturnVoid(m_objectCallbacks.contains(pObject) && m_objectCallbacks[pObject].contains(strNativeNotificationName));
417
418 /* Forget callback: */
419 m_objectCallbacks[pObject].remove(strNativeNotificationName);
420 if (m_objectCallbacks[pObject].isEmpty())
421 m_objectCallbacks.remove(pObject);
422
423 /* Unregister observer: */
424 NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
425 [m_pNative unregisterFromNotificationOfWorkspace :pstrNativeNotificationName];
426}
427
428void UICocoaApplication::registerToNotificationOfWindow(const QString &strNativeNotificationName, QWidget *pWidget,
429 PfnNativeNotificationCallbackForQWidget pCallback)
430{
431 /* Make sure it is not registered yet: */
432 AssertReturnVoid(!m_widgetCallbacks.contains(pWidget) || !m_widgetCallbacks[pWidget].contains(strNativeNotificationName));
433
434 /* Remember callback: */
435 m_widgetCallbacks[pWidget][strNativeNotificationName] = pCallback;
436
437 /* Register observer: */
438 NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
439 NativeNSWindowRef pWindow = darwinToNativeWindow(pWidget);
440 [m_pNative registerToNotificationOfWindow :pstrNativeNotificationName :pWindow];
441}
442
443void UICocoaApplication::unregisterFromNotificationOfWindow(const QString &strNativeNotificationName, QWidget *pWidget)
444{
445 /* Make sure it is registered yet: */
446 AssertReturnVoid(m_widgetCallbacks.contains(pWidget) && m_widgetCallbacks[pWidget].contains(strNativeNotificationName));
447
448 /* Forget callback: */
449 m_widgetCallbacks[pWidget].remove(strNativeNotificationName);
450 if (m_widgetCallbacks[pWidget].isEmpty())
451 m_widgetCallbacks.remove(pWidget);
452
453 /* Unregister observer: */
454 NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
455 NativeNSWindowRef pWindow = darwinToNativeWindow(pWidget);
456 [m_pNative unregisterFromNotificationOfWindow :pstrNativeNotificationName :pWindow];
457}
458
459void UICocoaApplication::nativeNotificationProxyForObject(NativeNSStringRef pstrNotificationName,
460 const QMap<QString,
461 QString> &userInfo)
462{
463 /* Get notification name: */
464 QString strNotificationName = darwinFromNativeString(pstrNotificationName);
465
466 /* Check if existing object(s) have corresponding notification handler: */
467 foreach (QObject *pObject, m_objectCallbacks.keys())
468 {
469 const QMap<QString, PfnNativeNotificationCallbackForQObject> &callbacks = m_objectCallbacks[pObject];
470 if (callbacks.contains(strNotificationName))
471 callbacks[strNotificationName](pObject, userInfo);
472 }
473}
474
475void UICocoaApplication::nativeNotificationProxyForWidget(NativeNSStringRef pstrNotificationName, NativeNSWindowRef pWindow)
476{
477 /* Get notification name: */
478 QString strNotificationName = darwinFromNativeString(pstrNotificationName);
479
480 /* Check if existing widget(s) have corresponding notification handler: */
481 foreach (QWidget *pWidget, m_widgetCallbacks.keys())
482 {
483 if (darwinToNativeWindow(pWidget) == pWindow)
484 {
485 const QMap<QString, PfnNativeNotificationCallbackForQWidget> &callbacks = m_widgetCallbacks[pWidget];
486 if (callbacks.contains(strNotificationName))
487 callbacks[strNotificationName](strNotificationName, pWidget);
488 }
489 }
490}
491
492void UICocoaApplication::registerCallbackForStandardWindowButton(QWidget *pWidget, StandardWindowButtonType enmButtonType,
493 PfnStandardWindowButtonCallbackForQWidget pCallback)
494{
495 /* Make sure it is not registered yet: */
496 AssertReturnVoid( !m_stdWindowButtonCallbacks.contains(pWidget)
497 || !m_stdWindowButtonCallbacks.value(pWidget).contains(enmButtonType));
498
499 /* Remember callback: */
500 m_stdWindowButtonCallbacks[pWidget][enmButtonType] = pCallback;
501
502 /* Register selector: */
503 NativeNSWindowRef pWindow = darwinToNativeWindow(pWidget);
504 [m_pNative registerSelectorForStandardWindowButton :pWindow :enmButtonType];
505}
506
507void UICocoaApplication::unregisterCallbackForStandardWindowButton(QWidget *pWidget, StandardWindowButtonType enmButtonType)
508{
509 /* Make sure it is registered yet: */
510 AssertReturnVoid( m_stdWindowButtonCallbacks.contains(pWidget)
511 && m_stdWindowButtonCallbacks.value(pWidget).contains(enmButtonType));
512
513 /* Forget callback: */
514 m_stdWindowButtonCallbacks[pWidget].remove(enmButtonType);
515 if (m_stdWindowButtonCallbacks.value(pWidget).isEmpty())
516 m_stdWindowButtonCallbacks.remove(pWidget);
517}
518
519void UICocoaApplication::nativeCallbackProxyForStandardWindowButton(NativeNSButtonRef pButton, bool fWithOptionKey)
520{
521 // WORKAROUND:
522 // Why not using nested foreach, will you ask?
523 // It's because Qt 4.x has shadowing issue in Q_FOREACH macro.
524 // Bug record QTBUG-33585 opened for Qt 4.8.4 and closed as _won't fix_ by one of Qt devs.
525
526 /* Check if passed button is one of the buttons of the registered widget(s): */
527 const QList<QWidget*> widgets = m_stdWindowButtonCallbacks.keys();
528 for (int iWidgetIndex = 0; iWidgetIndex < widgets.size(); ++iWidgetIndex)
529 {
530 QWidget *pWidget = widgets.at(iWidgetIndex);
531 const QMap<StandardWindowButtonType, PfnStandardWindowButtonCallbackForQWidget> callbacks
532 = m_stdWindowButtonCallbacks.value(pWidget);
533 const QList<StandardWindowButtonType> buttonTypes = callbacks.keys();
534 for (int iButtonTypeIndex = 0; iButtonTypeIndex < buttonTypes.size(); ++iButtonTypeIndex)
535 {
536 StandardWindowButtonType enmButtonType = buttonTypes.at(iButtonTypeIndex);
537 if (darwinNativeButtonOfWindow(pWidget, enmButtonType) == pButton)
538 return callbacks.value(enmButtonType)(enmButtonType, fWithOptionKey, pWidget);
539 }
540 }
541}
542
Note: See TracBrowser for help on using the repository browser.

© 2023 Oracle
ContactPrivacy policyTerms of Use