1/*
2 * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26#import "NSApplicationAWT.h"
27
28#import <objc/runtime.h>
29#import <JavaRuntimeSupport/JavaRuntimeSupport.h>
30
31#import "PropertiesUtilities.h"
32#import "ThreadUtilities.h"
33#import "QueuingApplicationDelegate.h"
34#import "AWTIconData.h"
35
36/*
37 * Declare library specific JNI_Onload entry if static build
38 */
39DEF_STATIC_JNI_OnLoad
40
41static BOOL sUsingDefaultNIB = YES;
42static NSString *SHARED_FRAMEWORK_BUNDLE = @"/System/Library/Frameworks/JavaVM.framework";
43static id <NSApplicationDelegate> applicationDelegate = nil;
44static QueuingApplicationDelegate * qad = nil;
45
46// Flag used to indicate to the Plugin2 event synthesis code to do a postEvent instead of sendEvent
47BOOL postEventDuringEventSynthesis = NO;
48
49/**
50 * Subtypes of NSApplicationDefined, which are used for custom events.
51 */
52enum {
53    ExecuteBlockEvent = 777, NativeSyncQueueEvent
54};
55
56@implementation NSApplicationAWT
57
58- (id) init
59{
60    // Headless: NO
61    // Embedded: NO
62    // Multiple Calls: NO
63    //  Caller: +[NSApplication sharedApplication]
64
65AWT_ASSERT_APPKIT_THREAD;
66    fApplicationName = nil;
67    dummyEventTimestamp = 0.0;
68    seenDummyEventLock = nil;
69
70
71    // NSApplication will call _RegisterApplication with the application's bundle, but there may not be one.
72    // So, we need to call it ourselves to ensure the app is set up properly.
73    [self registerWithProcessManager];
74
75    return [super init];
76}
77
78- (void)dealloc
79{
80    [fApplicationName release];
81    fApplicationName = nil;
82
83    [super dealloc];
84}
85
86- (void)finishLaunching
87{
88AWT_ASSERT_APPKIT_THREAD;
89
90    JNIEnv *env = [ThreadUtilities getJNIEnv];
91
92    SEL appearanceSel = @selector(setAppearance:); // macOS 10.14+
93    if ([self respondsToSelector:appearanceSel]) {
94        NSString *appearanceProp = [PropertiesUtilities
95                javaSystemPropertyForKey:@"apple.awt.application.appearance"
96                                 withEnv:env];
97        if (![@"system" isEqual:appearanceProp]) {
98            // by default use light mode, because dark mode is not supported yet
99            NSAppearance *appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
100            if (appearanceProp != nil) {
101                NSAppearance *requested = [NSAppearance appearanceNamed:appearanceProp];
102                if (requested != nil) {
103                    appearance = requested;
104                }
105            }
106            // [self setAppearance:appearance];
107            [self performSelector:appearanceSel withObject:appearance];
108        }
109    }
110
111    // Get default nib file location
112    // NOTE: This should learn about the current java.version. Probably best thru
113    //  the Makefile system's -DFRAMEWORK_VERSION define. Need to be able to pass this
114    //  thru to PB from the Makefile system and for local builds.
115    NSString *defaultNibFile = [PropertiesUtilities javaSystemPropertyForKey:@"apple.awt.application.nib" withEnv:env];
116    if (!defaultNibFile) {
117        NSBundle *javaBundle = [NSBundle bundleWithPath:SHARED_FRAMEWORK_BUNDLE];
118        defaultNibFile = [javaBundle pathForResource:@"DefaultApp" ofType:@"nib"];
119    } else {
120        sUsingDefaultNIB = NO;
121    }
122
123    [NSBundle loadNibFile:defaultNibFile externalNameTable: [NSDictionary dictionaryWithObject:self forKey:@"NSOwner"] withZone:nil];
124
125    // Set user defaults to not try to parse application arguments.
126    NSUserDefaults * defs = [NSUserDefaults standardUserDefaults];
127    NSDictionary * noOpenDict = [NSDictionary dictionaryWithObject:@"NO" forKey:@"NSTreatUnknownArgumentsAsOpen"];
128    [defs registerDefaults:noOpenDict];
129
130    // Fix up the dock icon now that we are registered with CAS and the Dock.
131    [self setDockIconWithEnv:env];
132
133    // If we are using our nib (the default application NIB) we need to put the app name into
134    // the application menu, which has placeholders for the name.
135    if (sUsingDefaultNIB) {
136        NSUInteger i, itemCount;
137        NSMenu *theMainMenu = [NSApp mainMenu];
138
139        // First submenu off the main menu is the application menu.
140        NSMenuItem *appMenuItem = [theMainMenu itemAtIndex:0];
141        NSMenu *appMenu = [appMenuItem submenu];
142        itemCount = [appMenu numberOfItems];
143
144        for (i = 0; i < itemCount; i++) {
145            NSMenuItem *anItem = [appMenu itemAtIndex:i];
146            NSString *oldTitle = [anItem title];
147            [anItem setTitle:[NSString stringWithFormat:oldTitle, fApplicationName]];
148        }
149    }
150
151    if (applicationDelegate) {
152        [self setDelegate:applicationDelegate];
153    } else {
154        qad = [QueuingApplicationDelegate sharedDelegate];
155        [self setDelegate:qad];
156    }
157
158    [super finishLaunching];
159}
160
161- (void) registerWithProcessManager
162{
163    // Headless: NO
164    // Embedded: NO
165    // Multiple Calls: NO
166    //  Caller: -[NSApplicationAWT init]
167
168AWT_ASSERT_APPKIT_THREAD;
169    JNIEnv *env = [ThreadUtilities getJNIEnv];
170
171    char envVar[80];
172
173    // The following environment variable is set from the -Xdock:name param. It should be UTF8.
174    snprintf(envVar, sizeof(envVar), "APP_NAME_%d", getpid());
175    char *appName = getenv(envVar);
176    if (appName != NULL) {
177        fApplicationName = [NSString stringWithUTF8String:appName];
178        unsetenv(envVar);
179    }
180
181    // If it wasn't specified as an argument, see if it was specified as a system property.
182    if (fApplicationName == nil) {
183        fApplicationName = [PropertiesUtilities javaSystemPropertyForKey:@"apple.awt.application.name" withEnv:env];
184    }
185
186    // If we STILL don't have it, the app name is retrieved from an environment variable (set in java.c) It should be UTF8.
187    if (fApplicationName == nil) {
188        char mainClassEnvVar[80];
189        snprintf(mainClassEnvVar, sizeof(mainClassEnvVar), "JAVA_MAIN_CLASS_%d", getpid());
190        char *mainClass = getenv(mainClassEnvVar);
191        if (mainClass != NULL) {
192            fApplicationName = [NSString stringWithUTF8String:mainClass];
193            unsetenv(mainClassEnvVar);
194
195            NSRange lastPeriod = [fApplicationName rangeOfString:@"." options:NSBackwardsSearch];
196            if (lastPeriod.location != NSNotFound) {
197                fApplicationName = [fApplicationName substringFromIndex:lastPeriod.location + 1];
198            }
199        }
200    }
201
202    // The dock name is nil for double-clickable Java apps (bundled and Web Start apps)
203    // When that happens get the display name, and if that's not available fall back to
204    // CFBundleName.
205    NSBundle *mainBundle = [NSBundle mainBundle];
206    if (fApplicationName == nil) {
207        fApplicationName = (NSString *)[mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
208
209        if (fApplicationName == nil) {
210            fApplicationName = (NSString *)[mainBundle objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey];
211
212            if (fApplicationName == nil) {
213                fApplicationName = (NSString *)[mainBundle objectForInfoDictionaryKey: (NSString *)kCFBundleExecutableKey];
214
215                if (fApplicationName == nil) {
216                    // Name of last resort is the last part of the applicatoin name without the .app (consistent with CopyProcessName)
217                    fApplicationName = [[mainBundle bundlePath] lastPathComponent];
218
219                    if ([fApplicationName hasSuffix:@".app"]) {
220                        fApplicationName = [fApplicationName stringByDeletingPathExtension];
221                    }
222                }
223            }
224        }
225    }
226
227    // We're all done trying to determine the app name.  Hold on to it.
228    [fApplicationName retain];
229
230    NSDictionary *registrationOptions = [NSMutableDictionary dictionaryWithObject:fApplicationName forKey:@"JRSAppNameKey"];
231
232    NSString *launcherType = [PropertiesUtilities javaSystemPropertyForKey:@"sun.java.launcher" withEnv:env];
233    if ([@"SUN_STANDARD" isEqualToString:launcherType]) {
234        [registrationOptions setValue:[NSNumber numberWithBool:YES] forKey:@"JRSAppIsCommandLineKey"];
235    }
236
237    NSString *uiElementProp = [PropertiesUtilities javaSystemPropertyForKey:@"apple.awt.UIElement" withEnv:env];
238    if ([@"true" isCaseInsensitiveLike:uiElementProp]) {
239        [registrationOptions setValue:[NSNumber numberWithBool:YES] forKey:@"JRSAppIsUIElementKey"];
240    }
241
242    NSString *backgroundOnlyProp = [PropertiesUtilities javaSystemPropertyForKey:@"apple.awt.BackgroundOnly" withEnv:env];
243    if ([@"true" isCaseInsensitiveLike:backgroundOnlyProp]) {
244        [registrationOptions setValue:[NSNumber numberWithBool:YES] forKey:@"JRSAppIsBackgroundOnlyKey"];
245    }
246
247    // TODO replace with direct call
248    // [JRSAppKitAWT registerAWTAppWithOptions:registrationOptions];
249    // and remove below transform/activate/run hack
250
251    id jrsAppKitAWTClass = objc_getClass("JRSAppKitAWT");
252    SEL registerSel = @selector(registerAWTAppWithOptions:);
253    if ([jrsAppKitAWTClass respondsToSelector:registerSel]) {
254        [jrsAppKitAWTClass performSelector:registerSel withObject:registrationOptions];
255        return;
256    }
257
258// HACK BEGIN
259    // The following is necessary to make the java process behave like a
260    // proper foreground application...
261    [ThreadUtilities performOnMainThreadWaiting:NO block:^(){
262        ProcessSerialNumber psn;
263        GetCurrentProcess(&psn);
264        TransformProcessType(&psn, kProcessTransformToForegroundApplication);
265
266        [NSApp activateIgnoringOtherApps:YES];
267        [NSApp run];
268    }];
269// HACK END
270}
271
272- (void) setDockIconWithEnv:(JNIEnv *)env {
273    NSString *theIconPath = nil;
274
275    // The following environment variable is set in java.c. It is probably UTF8.
276    char envVar[80];
277    snprintf(envVar, sizeof(envVar), "APP_ICON_%d", getpid());
278    char *appIcon = getenv(envVar);
279    if (appIcon != NULL) {
280        theIconPath = [NSString stringWithUTF8String:appIcon];
281        unsetenv(envVar);
282    }
283
284    if (theIconPath == nil) {
285        theIconPath = [PropertiesUtilities javaSystemPropertyForKey:@"apple.awt.application.icon" withEnv:env];
286    }
287
288    // Use the path specified to get the icon image
289    NSImage* iconImage = nil;
290    if (theIconPath != nil) {
291        iconImage = [[NSImage alloc] initWithContentsOfFile:theIconPath];
292    }
293
294    // If no icon file was specified or we failed to get the icon image
295    // and there is no bundle's icon, then use the default icon
296    if (iconImage == nil) {
297        NSString* bundleIcon = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIconFile"];
298        if (bundleIcon == nil) {
299            NSData* iconData;
300            iconData = [[NSData alloc] initWithBytesNoCopy: sAWTIconData length: sizeof(sAWTIconData) freeWhenDone: NO];
301            iconImage = [[NSImage alloc] initWithData: iconData];
302            [iconData release];
303        }
304    }
305
306    // Set up the dock icon if we have an icon image.
307    if (iconImage != nil) {
308        [NSApp setApplicationIconImage:iconImage];
309        [iconImage release];
310    }
311}
312
313+ (void) runAWTLoopWithApp:(NSApplication*)app {
314    NSAutoreleasePool *pool = [NSAutoreleasePool new];
315
316    // Make sure that when we run in javaRunLoopMode we don't exit randomly
317    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:[ThreadUtilities javaRunLoopMode]];
318
319    do {
320        @try {
321            [app run];
322        } @catch (NSException* e) {
323            NSLog(@"Apple AWT Startup Exception: %@", [e description]);
324            NSLog(@"Apple AWT Startup Exception callstack: %@", [e callStackSymbols]);
325            NSLog(@"Apple AWT Restarting Native Event Thread");
326
327            [app stop:app];
328        }
329    } while (YES);
330
331    [pool drain];
332}
333
334- (BOOL)usingDefaultNib {
335    return sUsingDefaultNIB;
336}
337
338- (void)orderFrontStandardAboutPanelWithOptions:(NSDictionary *)optionsDictionary {
339    if (!optionsDictionary) {
340        optionsDictionary = [NSMutableDictionary dictionaryWithCapacity:2];
341        [optionsDictionary setValue:[[[[[NSApp mainMenu] itemAtIndex:0] submenu] itemAtIndex:0] title] forKey:@"ApplicationName"];
342        if (![NSImage imageNamed:@"NSApplicationIcon"]) {
343            [optionsDictionary setValue:[NSApp applicationIconImage] forKey:@"ApplicationIcon"];
344        }
345    }
346
347    [super orderFrontStandardAboutPanelWithOptions:optionsDictionary];
348}
349
350#define DRAGMASK (NSMouseMovedMask | NSLeftMouseDraggedMask | NSRightMouseDownMask | NSRightMouseDraggedMask | NSLeftMouseUpMask | NSRightMouseUpMask | NSFlagsChangedMask | NSKeyDownMask)
351
352#if defined(MAC_OS_X_VERSION_10_12) && __LP64__
353   // 10.12 changed `mask` to NSEventMask (unsigned long long) for x86_64 builds.
354- (NSEvent *)nextEventMatchingMask:(NSEventMask)mask
355#else
356- (NSEvent *)nextEventMatchingMask:(NSUInteger)mask
357#endif
358untilDate:(NSDate *)expiration inMode:(NSString *)mode dequeue:(BOOL)deqFlag {
359    if (mask == DRAGMASK && [((NSString *)kCFRunLoopDefaultMode) isEqual:mode]) {
360        postEventDuringEventSynthesis = YES;
361    }
362
363    NSEvent *event = [super nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue: deqFlag];
364    postEventDuringEventSynthesis = NO;
365
366    return event;
367}
368
369// NSTimeInterval has microseconds precision
370#define TS_EQUAL(ts1, ts2) (fabs((ts1) - (ts2)) < 1e-6)
371
372- (void)sendEvent:(NSEvent *)event
373{
374    if ([event type] == NSApplicationDefined
375            && TS_EQUAL([event timestamp], dummyEventTimestamp)
376            && (short)[event subtype] == NativeSyncQueueEvent
377            && [event data1] == NativeSyncQueueEvent
378            && [event data2] == NativeSyncQueueEvent) {
379        [seenDummyEventLock lockWhenCondition:NO];
380        [seenDummyEventLock unlockWithCondition:YES];
381    } else if ([event type] == NSApplicationDefined
382               && (short)[event subtype] == ExecuteBlockEvent
383               && [event data1] != 0 && [event data2] == ExecuteBlockEvent) {
384        void (^block)() = (void (^)()) [event data1];
385        block();
386        [block release];
387    } else if ([event type] == NSKeyUp && ([event modifierFlags] & NSCommandKeyMask)) {
388        // Cocoa won't send us key up event when releasing a key while Cmd is down,
389        // so we have to do it ourselves.
390        [[self keyWindow] sendEvent:event];
391    } else {
392        [super sendEvent:event];
393    }
394}
395
396/*
397 * Posts the block to the AppKit event queue which will be executed
398 * on the main AppKit loop.
399 * While running nested loops this event will be ignored.
400 */
401- (void)postRunnableEvent:(void (^)())block
402{
403    void (^copy)() = [block copy];
404    NSInteger encode = (NSInteger) copy;
405    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
406    NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined
407                                        location: NSMakePoint(0,0)
408                                   modifierFlags: 0
409                                       timestamp: 0
410                                    windowNumber: 0
411                                         context: nil
412                                         subtype: ExecuteBlockEvent
413                                           data1: encode
414                                           data2: ExecuteBlockEvent];
415
416    [NSApp postEvent: event atStart: NO];
417    [pool drain];
418}
419
420- (void)postDummyEvent:(bool)useCocoa {
421    seenDummyEventLock = [[NSConditionLock alloc] initWithCondition:NO];
422    dummyEventTimestamp = [NSProcessInfo processInfo].systemUptime;
423
424    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
425    NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined
426                                        location: NSMakePoint(0,0)
427                                   modifierFlags: 0
428                                       timestamp: dummyEventTimestamp
429                                    windowNumber: 0
430                                         context: nil
431                                         subtype: NativeSyncQueueEvent
432                                           data1: NativeSyncQueueEvent
433                                           data2: NativeSyncQueueEvent];
434    if (useCocoa) {
435        [NSApp postEvent:event atStart:NO];
436    } else {
437        ProcessSerialNumber psn;
438        GetCurrentProcess(&psn);
439        CGEventPostToPSN(&psn, [event CGEvent]);
440    }
441    [pool drain];
442}
443
444- (void)waitForDummyEvent:(double)timeout {
445    bool unlock = true;
446    if (timeout >= 0) {
447        double sec = timeout / 1000;
448        unlock = [seenDummyEventLock lockWhenCondition:YES
449                               beforeDate:[NSDate dateWithTimeIntervalSinceNow:sec]];
450    } else {
451        [seenDummyEventLock lockWhenCondition:YES];
452    }
453    if (unlock) {
454        [seenDummyEventLock unlock];
455    }
456    [seenDummyEventLock release];
457
458    seenDummyEventLock = nil;
459}
460
461@end
462
463
464void OSXAPP_SetApplicationDelegate(id <NSApplicationDelegate> newdelegate)
465{
466AWT_ASSERT_APPKIT_THREAD;
467    applicationDelegate = newdelegate;
468
469    if (NSApp != nil) {
470        [NSApp setDelegate: applicationDelegate];
471
472        if (applicationDelegate && qad) {
473            [qad processQueuedEventsWithTargetDelegate: applicationDelegate];
474            qad = nil;
475        }
476    }
477}
478