1/*
2 * Copyright (c) 2012, 2019, 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 "LWCToolkit.h"
27#import "ThreadUtilities.h"
28#include "GeomUtilities.h"
29
30#import <JavaNativeFoundation/JavaNativeFoundation.h>
31
32/**
33 * Some default values for invalid CoreGraphics display ID.
34 */
35#define DEFAULT_DEVICE_WIDTH 1024
36#define DEFAULT_DEVICE_HEIGHT 768
37#define DEFAULT_DEVICE_DPI 72
38
39/*
40 * Convert the mode string to the more convinient bits per pixel value
41 */
42static int getBPPFromModeString(CFStringRef mode)
43{
44    if ((CFStringCompare(mode, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo)) {
45        // This is a strange mode, where we using 10 bits per RGB component and pack it into 32 bits
46        // Java is not ready to work with this mode but we have to specify it as supported
47        return 30;
48    }
49    else if (CFStringCompare(mode, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
50        return 32;
51    }
52    else if (CFStringCompare(mode, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
53        return 16;
54    }
55    else if (CFStringCompare(mode, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
56        return 8;
57    }
58
59    return 0;
60}
61
62static BOOL isValidDisplayMode(CGDisplayModeRef mode){
63    return (1 < CGDisplayModeGetWidth(mode) && 1 < CGDisplayModeGetHeight(mode));
64}
65
66static CFMutableArrayRef getAllValidDisplayModes(jint displayID){
67    // CGDisplayCopyAllDisplayModes can return NULL if displayID is invalid
68    CFArrayRef allModes = CGDisplayCopyAllDisplayModes(displayID, NULL);
69    CFMutableArrayRef validModes = nil;
70    if (allModes) {
71        CFIndex numModes = CFArrayGetCount(allModes);
72        validModes = CFArrayCreateMutable(kCFAllocatorDefault, numModes + 1, &kCFTypeArrayCallBacks);
73
74        CFIndex n;
75        for (n=0; n < numModes; n++) {
76            CGDisplayModeRef cRef = (CGDisplayModeRef) CFArrayGetValueAtIndex(allModes, n);
77            if (cRef != NULL && isValidDisplayMode(cRef)) {
78                CFArrayAppendValue(validModes, cRef);
79            }
80        }
81        CFRelease(allModes);
82
83        // CGDisplayCopyDisplayMode can return NULL if displayID is invalid
84        CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(displayID);
85        if (currentMode) {
86            BOOL containsCurrentMode = NO;
87            numModes = CFArrayGetCount(validModes);
88            for (n=0; n < numModes; n++) {
89                if(CFArrayGetValueAtIndex(validModes, n) == currentMode){
90                    containsCurrentMode = YES;
91                    break;
92                }
93            }
94            if (!containsCurrentMode) {
95                CFArrayAppendValue(validModes, currentMode);
96            }
97            CGDisplayModeRelease(currentMode);
98        }
99    }
100
101    return validModes;
102}
103
104/*
105 * Find the best possible match in the list of display modes that we can switch to based on
106 * the provided parameters.
107 */
108static CGDisplayModeRef getBestModeForParameters(CFArrayRef allModes, int w, int h, int bpp, int refrate) {
109    CGDisplayModeRef bestGuess = NULL;
110    CFIndex numModes = allModes ? CFArrayGetCount(allModes) : 0, n;
111
112    for(n = 0; n < numModes; n++ ) {
113        CGDisplayModeRef cRef = (CGDisplayModeRef) CFArrayGetValueAtIndex(allModes, n);
114        if(cRef == NULL) {
115            continue;
116        }
117        CFStringRef modeString = CGDisplayModeCopyPixelEncoding(cRef);
118        int thisBpp = getBPPFromModeString(modeString);
119        CFRelease(modeString);
120        int thisH = (int)CGDisplayModeGetHeight(cRef);
121        int thisW = (int)CGDisplayModeGetWidth(cRef);
122        if (thisBpp != bpp || thisH != h || thisW != w) {
123            // One of the key parameters does not match
124            continue;
125        }
126
127        if (refrate == 0) { // REFRESH_RATE_UNKNOWN
128            return cRef;
129        }
130
131        // Refresh rate might be 0 in display mode and we ask for specific display rate
132        // but if we do not find exact match then 0 refresh rate might be just Ok
133        int thisRefrate = (int)CGDisplayModeGetRefreshRate(cRef);
134        if (thisRefrate == refrate) {
135            // Exact match
136            return cRef;
137        }
138        if (thisRefrate == 0) {
139            // Not exactly what was asked for, but may fit our needs if we don't find an exact match
140            bestGuess = cRef;
141        }
142    }
143    return bestGuess;
144}
145
146/*
147 * Create a new java.awt.DisplayMode instance based on provided
148 * CGDisplayModeRef, if CGDisplayModeRef is NULL, then some stub is returned.
149 */
150static jobject createJavaDisplayMode(CGDisplayModeRef mode, JNIEnv *env) {
151    jobject ret = NULL;
152    jint h = DEFAULT_DEVICE_HEIGHT, w = DEFAULT_DEVICE_WIDTH, bpp = 0, refrate = 0;
153    JNF_COCOA_ENTER(env);
154    if (mode) {
155        CFStringRef currentBPP = CGDisplayModeCopyPixelEncoding(mode);
156        bpp = getBPPFromModeString(currentBPP);
157        refrate = CGDisplayModeGetRefreshRate(mode);
158        h = CGDisplayModeGetHeight(mode);
159        w = CGDisplayModeGetWidth(mode);
160        CFRelease(currentBPP);
161    }
162    static JNF_CLASS_CACHE(jc_DisplayMode, "java/awt/DisplayMode");
163    static JNF_CTOR_CACHE(jc_DisplayMode_ctor, jc_DisplayMode, "(IIII)V");
164    ret = JNFNewObject(env, jc_DisplayMode_ctor, w, h, bpp, refrate);
165    JNF_COCOA_EXIT(env);
166    return ret;
167}
168
169
170/*
171 * Class:     sun_awt_CGraphicsDevice
172 * Method:    nativeGetXResolution
173 * Signature: (I)D
174 */
175JNIEXPORT jdouble JNICALL
176Java_sun_awt_CGraphicsDevice_nativeGetXResolution
177  (JNIEnv *env, jclass class, jint displayID)
178{
179    // CGDisplayScreenSize can return 0 if displayID is invalid
180    CGSize size = CGDisplayScreenSize(displayID);
181    CGRect rect = CGDisplayBounds(displayID);
182    // 1 inch == 25.4 mm
183    jfloat inches = size.width / 25.4f;
184    return inches > 0 ? rect.size.width / inches : DEFAULT_DEVICE_DPI;
185}
186
187/*
188 * Class:     sun_awt_CGraphicsDevice
189 * Method:    nativeGetYResolution
190 * Signature: (I)D
191 */
192JNIEXPORT jdouble JNICALL
193Java_sun_awt_CGraphicsDevice_nativeGetYResolution
194  (JNIEnv *env, jclass class, jint displayID)
195{
196    // CGDisplayScreenSize can return 0 if displayID is invalid
197    CGSize size = CGDisplayScreenSize(displayID);
198    CGRect rect = CGDisplayBounds(displayID);
199    // 1 inch == 25.4 mm
200    jfloat inches = size.height / 25.4f;
201    return inches > 0 ? rect.size.height / inches : DEFAULT_DEVICE_DPI;
202}
203
204/*
205 * Class:     sun_awt_CGraphicsDevice
206 * Method:    nativeGetBounds
207 * Signature: (I)Ljava/awt/Rectangle;
208 */
209JNIEXPORT jobject JNICALL
210Java_sun_awt_CGraphicsDevice_nativeGetBounds
211(JNIEnv *env, jclass class, jint displayID)
212{
213    CGRect rect = CGDisplayBounds(displayID);
214    if (rect.size.width == 0) {
215        rect.size.width = DEFAULT_DEVICE_WIDTH;
216    }
217    if (rect.size.height == 0) {
218        rect.size.height = DEFAULT_DEVICE_HEIGHT;
219    }
220    return CGToJavaRect(env, rect);
221}
222
223/*
224 * Class:     sun_awt_CGraphicsDevice
225 * Method:    nativeGetScreenInsets
226 * Signature: (I)D
227 */
228JNIEXPORT jobject JNICALL
229Java_sun_awt_CGraphicsDevice_nativeGetScreenInsets
230  (JNIEnv *env, jclass class, jint displayID)
231{
232    jobject ret = NULL;
233    __block NSRect frame = NSZeroRect;
234    __block NSRect visibleFrame = NSZeroRect;
235JNF_COCOA_ENTER(env);
236
237    [ThreadUtilities performOnMainThreadWaiting:YES block:^(){
238        NSArray *screens = [NSScreen screens];
239        for (NSScreen *screen in screens) {
240            NSDictionary *screenInfo = [screen deviceDescription];
241            NSNumber *screenID = [screenInfo objectForKey:@"NSScreenNumber"];
242            if ([screenID unsignedIntValue] == displayID){
243                frame = [screen frame];
244                visibleFrame = [screen visibleFrame];
245                break;
246            }
247        }
248    }];
249    // Convert between Cocoa's coordinate system and Java.
250    jint bottom = visibleFrame.origin.y - frame.origin.y;
251    jint top = frame.size.height - visibleFrame.size.height - bottom;
252    jint left = visibleFrame.origin.x - frame.origin.x;
253    jint right = frame.size.width - visibleFrame.size.width - left;
254
255    static JNF_CLASS_CACHE(jc_Insets, "java/awt/Insets");
256    static JNF_CTOR_CACHE(jc_Insets_ctor, jc_Insets, "(IIII)V");
257    ret = JNFNewObject(env, jc_Insets_ctor, top, left, bottom, right);
258
259JNF_COCOA_EXIT(env);
260
261    return ret;
262}
263
264/*
265 * Class:     sun_awt_CGraphicsDevice
266 * Method:    nativeSetDisplayMode
267 * Signature: (IIIII)V
268 */
269JNIEXPORT void JNICALL
270Java_sun_awt_CGraphicsDevice_nativeSetDisplayMode
271(JNIEnv *env, jclass class, jint displayID, jint w, jint h, jint bpp, jint refrate)
272{
273    JNF_COCOA_ENTER(env);
274    CFArrayRef allModes = getAllValidDisplayModes(displayID);
275    CGDisplayModeRef closestMatch = getBestModeForParameters(allModes, (int)w, (int)h, (int)bpp, (int)refrate);
276
277    __block CGError retCode = kCGErrorSuccess;
278    if (closestMatch != NULL) {
279        CGDisplayModeRetain(closestMatch);
280        [ThreadUtilities performOnMainThreadWaiting:YES block:^(){
281            CGDisplayConfigRef config;
282            retCode = CGBeginDisplayConfiguration(&config);
283            if (retCode == kCGErrorSuccess) {
284                CGConfigureDisplayWithDisplayMode(config, displayID, closestMatch, NULL);
285                retCode = CGCompleteDisplayConfiguration(config, kCGConfigureForAppOnly);
286            }
287            CGDisplayModeRelease(closestMatch);
288        }];
289    } else {
290        [JNFException raise:env as:kIllegalArgumentException reason:"Invalid display mode"];
291    }
292
293    if (retCode != kCGErrorSuccess){
294        [JNFException raise:env as:kIllegalArgumentException reason:"Unable to set display mode!"];
295    }
296    CFRelease(allModes);
297    JNF_COCOA_EXIT(env);
298}
299/*
300 * Class:     sun_awt_CGraphicsDevice
301 * Method:    nativeGetDisplayMode
302 * Signature: (I)Ljava/awt/DisplayMode
303 */
304JNIEXPORT jobject JNICALL
305Java_sun_awt_CGraphicsDevice_nativeGetDisplayMode
306(JNIEnv *env, jclass class, jint displayID)
307{
308    jobject ret = NULL;
309    // CGDisplayCopyDisplayMode can return NULL if displayID is invalid
310    CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(displayID);
311    ret = createJavaDisplayMode(currentMode, env);
312    CGDisplayModeRelease(currentMode);
313    return ret;
314}
315
316/*
317 * Class:     sun_awt_CGraphicsDevice
318 * Method:    nativeGetDisplayMode
319 * Signature: (I)[Ljava/awt/DisplayModes
320 */
321JNIEXPORT jobjectArray JNICALL
322Java_sun_awt_CGraphicsDevice_nativeGetDisplayModes
323(JNIEnv *env, jclass class, jint displayID)
324{
325    jobjectArray jreturnArray = NULL;
326    JNF_COCOA_ENTER(env);
327    CFArrayRef allModes = getAllValidDisplayModes(displayID);
328
329    CFIndex numModes = allModes ? CFArrayGetCount(allModes): 0;
330    static JNF_CLASS_CACHE(jc_DisplayMode, "java/awt/DisplayMode");
331
332    jreturnArray = JNFNewObjectArray(env, &jc_DisplayMode, (jsize) numModes);
333    if (!jreturnArray) {
334        NSLog(@"CGraphicsDevice can't create java array of DisplayMode objects");
335        return nil;
336    }
337
338    CFIndex n;
339    for (n=0; n < numModes; n++) {
340        CGDisplayModeRef cRef = (CGDisplayModeRef) CFArrayGetValueAtIndex(allModes, n);
341        if (cRef != NULL) {
342            jobject oneMode = createJavaDisplayMode(cRef, env);
343            (*env)->SetObjectArrayElement(env, jreturnArray, n, oneMode);
344            if ((*env)->ExceptionOccurred(env)) {
345                (*env)->ExceptionDescribe(env);
346                (*env)->ExceptionClear(env);
347                continue;
348            }
349            (*env)->DeleteLocalRef(env, oneMode);
350        }
351    }
352    if (allModes) {
353        CFRelease(allModes);
354    }
355    JNF_COCOA_EXIT(env);
356
357    return jreturnArray;
358}
359
360/*
361 * Class:     sun_awt_CGraphicsDevice
362 * Method:    nativeGetScaleFactor
363 * Signature: (I)D
364 */
365JNIEXPORT jdouble JNICALL
366Java_sun_awt_CGraphicsDevice_nativeGetScaleFactor
367(JNIEnv *env, jclass class, jint displayID)
368{
369    __block jdouble ret = 1.0f;
370
371JNF_COCOA_ENTER(env);
372
373    [ThreadUtilities performOnMainThreadWaiting:YES block:^(){
374        NSArray *screens = [NSScreen screens];
375        for (NSScreen *screen in screens) {
376            NSDictionary *screenInfo = [screen deviceDescription];
377            NSNumber *screenID = [screenInfo objectForKey:@"NSScreenNumber"];
378            if ([screenID unsignedIntValue] == displayID){
379                if ([screen respondsToSelector:@selector(backingScaleFactor)]) {
380                    ret = [screen backingScaleFactor];
381                }
382                break;
383            }
384        }
385    }];
386
387JNF_COCOA_EXIT(env);
388    return ret;
389}
390