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#include "JNIUtilities.h"
30
31/**
32 * Some default values for invalid CoreGraphics display ID.
33 */
34#define DEFAULT_DEVICE_WIDTH 1024
35#define DEFAULT_DEVICE_HEIGHT 768
36#define DEFAULT_DEVICE_DPI 72
37
38/*
39 * Convert the mode string to the more convinient bits per pixel value
40 */
41static int getBPPFromModeString(CFStringRef mode)
42{
43    if ((CFStringCompare(mode, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo)) {
44        // This is a strange mode, where we using 10 bits per RGB component and pack it into 32 bits
45        // Java is not ready to work with this mode but we have to specify it as supported
46        return 30;
47    }
48    else if (CFStringCompare(mode, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
49        return 32;
50    }
51    else if (CFStringCompare(mode, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
52        return 16;
53    }
54    else if (CFStringCompare(mode, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
55        return 8;
56    }
57
58    return 0;
59}
60
61static BOOL isValidDisplayMode(CGDisplayModeRef mode){
62    return (1 < CGDisplayModeGetWidth(mode) && 1 < CGDisplayModeGetHeight(mode));
63}
64
65static CFMutableArrayRef getAllValidDisplayModes(jint displayID){
66    // CGDisplayCopyAllDisplayModes can return NULL if displayID is invalid
67    CFArrayRef allModes = CGDisplayCopyAllDisplayModes(displayID, NULL);
68    CFMutableArrayRef validModes = nil;
69    if (allModes) {
70        CFIndex numModes = CFArrayGetCount(allModes);
71        validModes = CFArrayCreateMutable(kCFAllocatorDefault, numModes + 1, &kCFTypeArrayCallBacks);
72
73        CFIndex n;
74        for (n=0; n < numModes; n++) {
75            CGDisplayModeRef cRef = (CGDisplayModeRef) CFArrayGetValueAtIndex(allModes, n);
76            if (cRef != NULL && isValidDisplayMode(cRef)) {
77                CFArrayAppendValue(validModes, cRef);
78            }
79        }
80        CFRelease(allModes);
81
82        // CGDisplayCopyDisplayMode can return NULL if displayID is invalid
83        CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(displayID);
84        if (currentMode) {
85            BOOL containsCurrentMode = NO;
86            numModes = CFArrayGetCount(validModes);
87            for (n=0; n < numModes; n++) {
88                if(CFArrayGetValueAtIndex(validModes, n) == currentMode){
89                    containsCurrentMode = YES;
90                    break;
91                }
92            }
93            if (!containsCurrentMode) {
94                CFArrayAppendValue(validModes, currentMode);
95            }
96            CGDisplayModeRelease(currentMode);
97        }
98    }
99
100    return validModes;
101}
102
103/*
104 * Find the best possible match in the list of display modes that we can switch to based on
105 * the provided parameters.
106 */
107static CGDisplayModeRef getBestModeForParameters(CFArrayRef allModes, int w, int h, int bpp, int refrate) {
108    CGDisplayModeRef bestGuess = NULL;
109    CFIndex numModes = allModes ? CFArrayGetCount(allModes) : 0, n;
110
111    for(n = 0; n < numModes; n++ ) {
112        CGDisplayModeRef cRef = (CGDisplayModeRef) CFArrayGetValueAtIndex(allModes, n);
113        if(cRef == NULL) {
114            continue;
115        }
116        CFStringRef modeString = CGDisplayModeCopyPixelEncoding(cRef);
117        int thisBpp = getBPPFromModeString(modeString);
118        CFRelease(modeString);
119        int thisH = (int)CGDisplayModeGetHeight(cRef);
120        int thisW = (int)CGDisplayModeGetWidth(cRef);
121        if (thisBpp != bpp || thisH != h || thisW != w) {
122            // One of the key parameters does not match
123            continue;
124        }
125
126        if (refrate == 0) { // REFRESH_RATE_UNKNOWN
127            return cRef;
128        }
129
130        // Refresh rate might be 0 in display mode and we ask for specific display rate
131        // but if we do not find exact match then 0 refresh rate might be just Ok
132        int thisRefrate = (int)CGDisplayModeGetRefreshRate(cRef);
133        if (thisRefrate == refrate) {
134            // Exact match
135            return cRef;
136        }
137        if (thisRefrate == 0) {
138            // Not exactly what was asked for, but may fit our needs if we don't find an exact match
139            bestGuess = cRef;
140        }
141    }
142    return bestGuess;
143}
144
145/*
146 * Create a new java.awt.DisplayMode instance based on provided
147 * CGDisplayModeRef, if CGDisplayModeRef is NULL, then some stub is returned.
148 */
149static jobject createJavaDisplayMode(CGDisplayModeRef mode, JNIEnv *env) {
150    jobject ret = NULL;
151    jint h = DEFAULT_DEVICE_HEIGHT, w = DEFAULT_DEVICE_WIDTH, bpp = 0, refrate = 0;
152    JNI_COCOA_ENTER(env);
153    if (mode) {
154        CFStringRef currentBPP = CGDisplayModeCopyPixelEncoding(mode);
155        bpp = getBPPFromModeString(currentBPP);
156        refrate = CGDisplayModeGetRefreshRate(mode);
157        h = CGDisplayModeGetHeight(mode);
158        w = CGDisplayModeGetWidth(mode);
159        CFRelease(currentBPP);
160    }
161    DECLARE_CLASS_RETURN(jc_DisplayMode, "java/awt/DisplayMode", ret);
162    DECLARE_METHOD_RETURN(jc_DisplayMode_ctor, jc_DisplayMode, "<init>", "(IIII)V", ret);
163    ret = (*env)->NewObject(env, jc_DisplayMode, jc_DisplayMode_ctor, w, h, bpp, refrate);
164    CHECK_EXCEPTION();
165    JNI_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;
235JNI_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    DECLARE_CLASS_RETURN(jc_Insets, "java/awt/Insets", ret);
256    DECLARE_METHOD_RETURN(jc_Insets_ctor, jc_Insets, "<init>", "(IIII)V", ret);
257    ret = (*env)->NewObject(env, jc_Insets, jc_Insets_ctor, top, left, bottom, right);
258
259JNI_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    JNI_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        JNU_ThrowIllegalArgumentException(env, "Invalid display mode");
291    }
292
293    if (retCode != kCGErrorSuccess){
294        JNU_ThrowIllegalArgumentException(env, "Unable to set display mode!");
295    }
296    CFRelease(allModes);
297    JNI_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    JNI_COCOA_ENTER(env);
327    CFArrayRef allModes = getAllValidDisplayModes(displayID);
328
329    CFIndex numModes = allModes ? CFArrayGetCount(allModes): 0;
330    DECLARE_CLASS_RETURN(jc_DisplayMode, "java/awt/DisplayMode", NULL);
331
332    jreturnArray = (*env)->NewObjectArray(env, (jsize)numModes, jc_DisplayMode, NULL);
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    JNI_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
371JNI_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
387JNI_COCOA_EXIT(env);
388    return ret;
389}
390