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