summaryrefslogtreecommitdiffstats
path: root/src/VBox/Frontends/VirtualBox/src/platform/darwin/UICocoaApplication.mm
blob: c73f5c1ec19bda54ee6a2bdcca6f94f7e3850115 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
/* $Id: UICocoaApplication.mm $ */
/** @file
 * VBox Qt GUI - UICocoaApplication class implementation.
 */

/*
 * Copyright (C) 2009-2023 Oracle and/or its affiliates.
 *
 * This file is part of VirtualBox base platform packages, as
 * available from https://www.virtualbox.org.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, in version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses>.
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */

/* GUI includes: */
#include "UICocoaApplication.h"

/* Other VBox includes: */
#include <iprt/assert.h>

/* External includes: */
#import <AppKit/NSApplication.h>
#import <AppKit/NSButton.h>
#import <AppKit/NSEvent.h>
#import <AppKit/NSWindow.h>
#import <Foundation/NSArray.h>


/** Class for tracking a callback. */
@interface CallbackData : NSObject
{
@public
    /** Holds the mask of events to send to this callback. */
    uint32_t           fMask;
    /** Holds the callback. */
    PFNVBOXCACALLBACK  pfnCallback;
    /** Holds the user argument. */
    void              *pvUser;
}
- (id)initWithMask :(uint32)mask callback :(PFNVBOXCACALLBACK)callback user :(void *)user;
@end /* @interface CallbackData  */

@implementation CallbackData
/** Performs initialization. */
- (id)initWithMask :(uint32)mask callback :(PFNVBOXCACALLBACK)callback user :(void *)user
{
    self = [super init];
    if (self)
    {
        fMask = mask;
        pfnCallback = callback;
        pvUser = user;
    }
    return self;
}
@end /* @implementation CallbackData  */


/** Class for event handling. */
@interface UICocoaApplicationPrivate : NSApplication
{
    /** The event mask for which there currently are callbacks. */
    uint32_t        m_fMask;
    /** Array of callbacks. */
    NSMutableArray *m_pCallbacks;
    /** AppNap preventing activity. */
    id <NSObject>   m_activity;
}
- (id)init;
- (void)sendEvent :(NSEvent *)theEvent;
- (void)setCallback :(uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser;
- (void)unsetCallback :(uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser;

- (void)finishLaunching;
- (void)terminate :(nullable id)sender;

- (void)registerToNotificationOfWorkspace :(NSString *)pstrNotificationName;
- (void)unregisterFromNotificationOfWorkspace :(NSString *)pstrNotificationName;

- (void)registerToNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow;
- (void)unregisterFromNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow;

- (void)notificationCallbackOfObject :(NSNotification *)notification;
- (void)notificationCallbackOfWindow :(NSNotification *)notification;

- (void)registerSelectorForStandardWindowButton :(NSWindow *)pWindow :(StandardWindowButtonType)enmButtonType;
- (void)selectorForStandardWindowButton :(NSButton *)pButton;
@end /* @interface UICocoaApplicationPrivate */

@implementation UICocoaApplicationPrivate
/** Performs initialization. */
- (id) init
{
    self = [super init];
    if (self)
        m_pCallbacks = [[NSMutableArray alloc] init];

    // WORKAROUND:
    // Gently disable El Capitan tries to break everything with the Enter Full Screen action.
    // S.a. https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKit/ for reference.
    [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];

    return self;
}

/** Sends an event.
  * @param  pEvent  Brings the event to be sent. */
- (void) sendEvent :(NSEvent *)pEvent
{
    /* Check if the type matches any of the registered callbacks. */
    uint32_t const fMask = m_fMask;
#if 0 /* for debugging */
    ::darwinPrintEvent("sendEvent: ", pEvent);
#endif
    if (fMask != 0)
    {
        NSEventType EvtType = [pEvent type];
        uint32_t fEvtMask = RT_LIKELY(EvtType < 32) ? RT_BIT_32(EvtType) : 0;
        if (fMask & fEvtMask)
        {
            /* Do the callouts in LIFO order. */
            for (CallbackData *pData in [m_pCallbacks reverseObjectEnumerator])
            {
                if (pData->fMask & fEvtMask)
                {
                    if (pData->pfnCallback(pEvent, [pEvent eventRef], pData->pvUser))
                        return;
                }

            }
        }
    }

    /* Get on with it. */
    [super sendEvent:pEvent];
}

/** Registers an event callback.
  * @param  fMask        Brings the event mask for which the callback is to be invoked.
  * @param  pfnCallback  Brings the callback function.
  * @param  pvUser       Brings the user argument. */
- (void) setCallback :(uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser
{
    /* Add the callback data to the array: */
    CallbackData *pData = [[[CallbackData alloc] initWithMask:fMask callback:pfnCallback user:pvUser] autorelease];
    [m_pCallbacks addObject:pData];

    /* Update the global mask: */
    m_fMask |= fMask;
}

/** Deregisters an event callback.
  * @param  fMask        Brings the event mask for which the callback is to be invoked.
  * @param  pfnCallback  Brings the callback function.
  * @param  pvUser       Brings the user argument. */
- (void) unsetCallback: (uint32_t)fMask :(PFNVBOXCACALLBACK)pfnCallback :(void *)pvUser
{
    /* Loop the event array LIFO fashion searching for a matching callback. */
    for (CallbackData *pData in [m_pCallbacks reverseObjectEnumerator])
    {
        if (   pData->pfnCallback == pfnCallback
            && pData->pvUser      == pvUser
            && pData->fMask       == fMask)
        {
            [m_pCallbacks removeObject:pData];
            break;
        }
    }
    uint32_t fNewMask = 0;
    for (CallbackData *pData in m_pCallbacks)
        fNewMask |= pData->fMask;
    m_fMask = fNewMask;
}

/** Standard handler called right after NSApp finished launching. */
- (void)finishLaunching
{
    /* Call to base-class: */
    [super finishLaunching];

    if (UICocoaApplication::instance()->isPreventAppNap())
    {
        //printf("Start activity preventing AppNap!\n");
        NSActivityOptions options = NSActivityUserInitiatedAllowingIdleSystemSleep;
        NSString *pstrReason = @"VirtualBox napping is prohibited!";
        m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:options
                                                                    reason:pstrReason];
        [m_activity retain];
    }
}

/** Standard handler called right before NSApp starting termination. */
- (void)terminate :(nullable id)sender
{
    if (UICocoaApplication::instance()->isPreventAppNap())
    {
        //printf("Finish activity preventing AppNap!\n");
        [[NSProcessInfo processInfo] endActivity:m_activity];
        [m_activity release];
        m_activity = Nil;
    }

    /* Call to base-class: */
    [super terminate: sender];
}

/** Registers to cocoa notification @a pstrNotificationName. */
- (void) registerToNotificationOfWorkspace :(NSString *)pstrNotificationName
{
    /* Register notification observer: */
    NSNotificationCenter *pNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
    [pNotificationCenter addObserver:self
                            selector:@selector(notificationCallbackOfObject:)
                                name:pstrNotificationName
                              object:nil];
}

/** Unregister @a pWindow from cocoa notification @a pstrNotificationName. */
- (void) unregisterFromNotificationOfWorkspace :(NSString *)pstrNotificationName
{
    /* Uninstall notification observer: */
    NSNotificationCenter *pNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
    [pNotificationCenter removeObserver:self
                                   name:pstrNotificationName
                                 object:nil];
}

/** Register @a pWindow to cocoa notification @a pstrNotificationName. */
- (void) registerToNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow
{
    /* Register notification observer: */
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(notificationCallbackOfWindow:)
                                                 name:pstrNotificationName
                                               object:pWindow];
}

/** Unregister @a pWindow from cocoa notification @a pstrNotificationName. */
- (void) unregisterFromNotificationOfWindow :(NSString *)pstrNotificationName :(NSWindow *)pWindow
{
    /* Uninstall notification observer: */
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:pstrNotificationName
                                                  object:pWindow];
}

/** Redirects cocoa @a notification to UICocoaApplication instance. */
- (void) notificationCallbackOfObject :(NSNotification *)notification
{
    /* Get current notification name: */
    NSString *pstrName = [notification name];

    /* Prepare user-info: */
    QMap<QString, QString> userInfo;

    /* Process known notifications: */
    if (   [pstrName isEqualToString :@"NSWorkspaceDidActivateApplicationNotification"]
        || [pstrName isEqualToString :@"NSWorkspaceDidDeactivateApplicationNotification"])
    {
        NSDictionary *pUserInfo = [notification userInfo];
        NSRunningApplication *pApplication = [pUserInfo valueForKey :@"NSWorkspaceApplicationKey"];
        NSString *pstrBundleIndentifier = [pApplication bundleIdentifier];
        userInfo.insert("BundleIdentifier", darwinFromNativeString((NativeNSStringRef)pstrBundleIndentifier));
    }

    /* Redirect known notifications to objects: */
    UICocoaApplication::instance()->nativeNotificationProxyForObject(pstrName, userInfo);
}

/** Redirects cocoa @a notification to UICocoaApplication instance. */
- (void) notificationCallbackOfWindow :(NSNotification *)notification
{
    /* Get current notification name: */
    NSString *pstrName = [notification name];

    /* Redirect known notifications to widgets: */
    UICocoaApplication::instance()->nativeNotificationProxyForWidget(pstrName, [notification object]);
}

/** Registers selector for standard window @a enmButtonType of the passed @a pWindow. */
- (void)registerSelectorForStandardWindowButton :(NSWindow *)pWindow :(StandardWindowButtonType)enmButtonType
{
    /* Retrieve corresponding button: */
    NSButton *pButton = Nil;
    switch (enmButtonType)
    {
        case StandardWindowButtonType_Close:            pButton = [pWindow standardWindowButton:NSWindowCloseButton]; break;
        case StandardWindowButtonType_Miniaturize:      pButton = [pWindow standardWindowButton:NSWindowMiniaturizeButton]; break;
        case StandardWindowButtonType_Zoom:             pButton = [pWindow standardWindowButton:NSWindowZoomButton]; break;
        case StandardWindowButtonType_Toolbar:          pButton = [pWindow standardWindowButton:NSWindowToolbarButton]; break;
        case StandardWindowButtonType_DocumentIcon:     pButton = [pWindow standardWindowButton:NSWindowDocumentIconButton]; break;
        case StandardWindowButtonType_DocumentVersions: /*pButton = [pWindow standardWindowButton:NSWindowDocumentVersionsButton];*/ break;
        case StandardWindowButtonType_FullScreen:       /*pButton = [pWindow standardWindowButton:NSWindowFullScreenButton];*/ break;
    }

    /* Register selector if button exists: */
    if (pButton != Nil)
    {
        [pButton setTarget:self];
        [pButton setAction:@selector(selectorForStandardWindowButton:)];
    }
}

/** Redirects selector of the standard window @a pButton to UICocoaApplication instance callback. */
- (void)selectorForStandardWindowButton :(NSButton *)pButton
{
    /* Check if Option key is currently held: */
    const bool fWithOptionKey = [NSEvent modifierFlags] & NSAlternateKeyMask;

    /* Redirect selector to callback: */
    UICocoaApplication::instance()->nativeCallbackProxyForStandardWindowButton(pButton, fWithOptionKey);
}
@end /* @implementation UICocoaApplicationPrivate */


/*********************************************************************************************************************************
*   Class UICocoaApplication implementation.                                                                                     *
*********************************************************************************************************************************/

/* static */
UICocoaApplication *UICocoaApplication::s_pInstance = 0;

/* static */
void UICocoaApplication::create(bool fPreventAppNap)
{
    if (!s_pInstance)
        s_pInstance = new UICocoaApplication(fPreventAppNap);
}

/* static */
UICocoaApplication *UICocoaApplication::instance()
{
    return s_pInstance;
}

UICocoaApplication::UICocoaApplication(bool fPreventAppNap)
    : m_fPreventAppNap(fPreventAppNap)
{
    /* Make sure our private NSApplication object is created: */
    m_pNative = (UICocoaApplicationPrivate*)[UICocoaApplicationPrivate sharedApplication];
    // WORKAROUND":
    // Create one auto release pool which is in place for all the
    // initialization and deinitialization stuff. That is when the
    // NSApplication is not running the run loop (there is a separate
    // auto release pool defined).
    m_pPool = [[NSAutoreleasePool alloc] init];
}

UICocoaApplication::~UICocoaApplication()
{
    [m_pNative release];
    [m_pPool release];
}

bool UICocoaApplication::isActive() const
{
    return [m_pNative isActive];
}

void UICocoaApplication::hide()
{
    [m_pNative hide:m_pNative];
}

void UICocoaApplication::hideUserElements()
{
    [m_pNative setPresentationOptions:NSApplicationPresentationHideMenuBar | NSApplicationPresentationHideDock];
}

void UICocoaApplication::registerForNativeEvents(uint32_t fMask, PFNVBOXCACALLBACK pfnCallback, void *pvUser)
{
    [m_pNative setCallback:fMask :pfnCallback :pvUser];
}

void UICocoaApplication::unregisterForNativeEvents(uint32_t fMask, PFNVBOXCACALLBACK pfnCallback, void *pvUser)
{
    [m_pNative unsetCallback:fMask :pfnCallback :pvUser];
}

void UICocoaApplication::registerToNotificationOfWorkspace(const QString &strNativeNotificationName, QObject *pObject,
                                                           PfnNativeNotificationCallbackForQObject pCallback)
{
    /* Make sure it is not registered yet: */
    AssertReturnVoid(!m_objectCallbacks.contains(pObject) || !m_objectCallbacks[pObject].contains(strNativeNotificationName));

    /* Remember callback: */
    m_objectCallbacks[pObject][strNativeNotificationName] = pCallback;

    /* Register observer: */
    NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
    [m_pNative registerToNotificationOfWorkspace :pstrNativeNotificationName];
}

void UICocoaApplication::unregisterFromNotificationOfWorkspace(const QString &strNativeNotificationName, QObject *pObject)
{
    /* Make sure it is registered yet: */
    AssertReturnVoid(m_objectCallbacks.contains(pObject) && m_objectCallbacks[pObject].contains(strNativeNotificationName));

    /* Forget callback: */
    m_objectCallbacks[pObject].remove(strNativeNotificationName);
    if (m_objectCallbacks[pObject].isEmpty())
        m_objectCallbacks.remove(pObject);

    /* Unregister observer: */
    NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
    [m_pNative unregisterFromNotificationOfWorkspace :pstrNativeNotificationName];
}

void UICocoaApplication::registerToNotificationOfWindow(const QString &strNativeNotificationName, QWidget *pWidget,
                                                        PfnNativeNotificationCallbackForQWidget pCallback)
{
    /* Make sure it is not registered yet: */
    AssertReturnVoid(!m_widgetCallbacks.contains(pWidget) || !m_widgetCallbacks[pWidget].contains(strNativeNotificationName));

    /* Remember callback: */
    m_widgetCallbacks[pWidget][strNativeNotificationName] = pCallback;

    /* Register observer: */
    NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
    NativeNSWindowRef pWindow = darwinToNativeWindow(pWidget);
    [m_pNative registerToNotificationOfWindow :pstrNativeNotificationName :pWindow];
}

void UICocoaApplication::unregisterFromNotificationOfWindow(const QString &strNativeNotificationName, QWidget *pWidget)
{
    /* Make sure it is registered yet: */
    AssertReturnVoid(m_widgetCallbacks.contains(pWidget) && m_widgetCallbacks[pWidget].contains(strNativeNotificationName));

    /* Forget callback: */
    m_widgetCallbacks[pWidget].remove(strNativeNotificationName);
    if (m_widgetCallbacks[pWidget].isEmpty())
        m_widgetCallbacks.remove(pWidget);

    /* Unregister observer: */
    NativeNSStringRef pstrNativeNotificationName = darwinToNativeString(strNativeNotificationName.toLatin1().constData());
    NativeNSWindowRef pWindow = darwinToNativeWindow(pWidget);
    [m_pNative unregisterFromNotificationOfWindow :pstrNativeNotificationName :pWindow];
}

void UICocoaApplication::nativeNotificationProxyForObject(NativeNSStringRef pstrNotificationName,
                                                          const QMap<QString,
                                                          QString> &userInfo)
{
    /* Get notification name: */
    QString strNotificationName = darwinFromNativeString(pstrNotificationName);

    /* Check if existing object(s) have corresponding notification handler: */
    foreach (QObject *pObject, m_objectCallbacks.keys())
    {
        const QMap<QString, PfnNativeNotificationCallbackForQObject> &callbacks = m_objectCallbacks[pObject];
        if (callbacks.contains(strNotificationName))
            callbacks[strNotificationName](pObject, userInfo);
    }
}

void UICocoaApplication::nativeNotificationProxyForWidget(NativeNSStringRef pstrNotificationName, NativeNSWindowRef pWindow)
{
    /* Get notification name: */
    QString strNotificationName = darwinFromNativeString(pstrNotificationName);

    /* Check if existing widget(s) have corresponding notification handler: */
    foreach (QWidget *pWidget, m_widgetCallbacks.keys())
    {
        if (darwinToNativeWindow(pWidget) == pWindow)
        {
            const QMap<QString, PfnNativeNotificationCallbackForQWidget> &callbacks = m_widgetCallbacks[pWidget];
            if (callbacks.contains(strNotificationName))
                callbacks[strNotificationName](strNotificationName, pWidget);
        }
    }
}

void UICocoaApplication::registerCallbackForStandardWindowButton(QWidget *pWidget, StandardWindowButtonType enmButtonType,
                                                                 PfnStandardWindowButtonCallbackForQWidget pCallback)
{
    /* Make sure it is not registered yet: */
    AssertReturnVoid(   !m_stdWindowButtonCallbacks.contains(pWidget)
                     || !m_stdWindowButtonCallbacks.value(pWidget).contains(enmButtonType));

    /* Remember callback: */
    m_stdWindowButtonCallbacks[pWidget][enmButtonType] = pCallback;

    /* Register selector: */
    NativeNSWindowRef pWindow = darwinToNativeWindow(pWidget);
    [m_pNative registerSelectorForStandardWindowButton :pWindow :enmButtonType];
}

void UICocoaApplication::unregisterCallbackForStandardWindowButton(QWidget *pWidget, StandardWindowButtonType enmButtonType)
{
    /* Make sure it is registered yet: */
    AssertReturnVoid(   m_stdWindowButtonCallbacks.contains(pWidget)
                     && m_stdWindowButtonCallbacks.value(pWidget).contains(enmButtonType));

    /* Forget callback: */
    m_stdWindowButtonCallbacks[pWidget].remove(enmButtonType);
    if (m_stdWindowButtonCallbacks.value(pWidget).isEmpty())
        m_stdWindowButtonCallbacks.remove(pWidget);
}

void UICocoaApplication::nativeCallbackProxyForStandardWindowButton(NativeNSButtonRef pButton, bool fWithOptionKey)
{
    // WORKAROUND:
    // Why not using nested foreach, will you ask?
    // It's because Qt 4.x has shadowing issue in Q_FOREACH macro.
    // Bug record QTBUG-33585 opened for Qt 4.8.4 and closed as _won't fix_ by one of Qt devs.

    /* Check if passed button is one of the buttons of the registered widget(s): */
    const QList<QWidget*> widgets = m_stdWindowButtonCallbacks.keys();
    for (int iWidgetIndex = 0; iWidgetIndex < widgets.size(); ++iWidgetIndex)
    {
        QWidget *pWidget = widgets.at(iWidgetIndex);
        const QMap<StandardWindowButtonType, PfnStandardWindowButtonCallbackForQWidget> callbacks
            = m_stdWindowButtonCallbacks.value(pWidget);
        const QList<StandardWindowButtonType> buttonTypes = callbacks.keys();
        for (int iButtonTypeIndex = 0; iButtonTypeIndex < buttonTypes.size(); ++iButtonTypeIndex)
        {
            StandardWindowButtonType enmButtonType = buttonTypes.at(iButtonTypeIndex);
            if (darwinNativeButtonOfWindow(pWidget, enmButtonType) == pButton)
                return callbacks.value(enmButtonType)(enmButtonType, fWithOptionKey, pWidget);
        }
    }
}