diff --git a/CHANGES.md b/CHANGES.md index e334a079e..8f5eeeef3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Features * [#1696](https://github.com/java-native-access/jna/pull/1696): Add `LARGE_INTEGER.ByValue` to `LARGE_INTEGER` in `WinNT.java` - [@baier233](https://github.com/baier233). * [#1697](https://github.com/java-native-access/jna/pull/1697): Add WlanApi module - [@eranl](https://github.com/eranl). * [#1718](https://github.com/java-native-access/jna/pull/1718): Add `Cups` to `c.s.j.p.unix` providing CUPS printing system bindings for destinations, jobs, options, and server configuration - [@dbwiddis](https://github.com/dbwiddis). +* [#1719](https://github.com/java-native-access/jna/pull/1719): Add `CoreGraphics` to `c.s.j.p.mac` with Quartz Window Services and Display Services bindings; implement `getAllWindows()` in `MacWindowUtils` - [@dbwiddis](https://github.com/dbwiddis). Bug Fixes --------- diff --git a/contrib/platform/src/com/sun/jna/platform/WindowUtils.java b/contrib/platform/src/com/sun/jna/platform/WindowUtils.java index 78ea1fa2d..2180e0ec7 100644 --- a/contrib/platform/src/com/sun/jna/platform/WindowUtils.java +++ b/contrib/platform/src/com/sun/jna/platform/WindowUtils.java @@ -97,6 +97,11 @@ import com.sun.jna.platform.win32.WinDef.HICON; import com.sun.jna.platform.win32.WinDef.HRGN; import com.sun.jna.platform.win32.WinDef.HWND; +import com.sun.jna.platform.mac.CoreFoundation; +import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef; +import com.sun.jna.platform.mac.CoreFoundation.CFDictionaryRef; +import com.sun.jna.platform.mac.CoreFoundation.CFStringRef; +import com.sun.jna.platform.mac.CoreGraphics; import com.sun.jna.platform.win32.WinDef.LPARAM; import com.sun.jna.platform.win32.WinDef.LRESULT; import com.sun.jna.platform.win32.WinDef.POINT; @@ -1560,6 +1565,91 @@ private void setBackgroundTransparent(Window w, boolean transparent, String cont } fixWindowDragging(w, context); } + + @Override + protected List getAllWindows(final boolean onlyVisibleWindows) { + final List result = new LinkedList<>(); + int options = onlyVisibleWindows + ? CoreGraphics.kCGWindowListOptionOnScreenOnly | CoreGraphics.kCGWindowListExcludeDesktopElements + : CoreGraphics.kCGWindowListOptionAll; + CFArrayRef windowList = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo( + options, CoreGraphics.kCGNullWindowID); + if (windowList == null) { + return result; + } + try { + CFStringRef kName = CFStringRef.createCFString(CoreGraphics.kCGWindowName); + CFStringRef kOwnerName = CFStringRef.createCFString(CoreGraphics.kCGWindowOwnerName); + CFStringRef kBounds = CFStringRef.createCFString(CoreGraphics.kCGWindowBounds); + CFStringRef kNumber = CFStringRef.createCFString(CoreGraphics.kCGWindowNumber); + CFStringRef kLayer = CFStringRef.createCFString(CoreGraphics.kCGWindowLayer); + try { + int count = windowList.getCount(); + for (int i = 0; i < count; i++) { + Pointer p = windowList.getValueAtIndex(i); + if (p == null) { + continue; + } + CFDictionaryRef dict = new CFDictionaryRef(p); + + // Skip non-normal windows (layer != 0) + Pointer layerPtr = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, kLayer); + if (layerPtr != null) { + CoreFoundation.CFNumberRef layerNum = new CoreFoundation.CFNumberRef(layerPtr); + if (layerNum.intValue() != 0) { + continue; + } + } + + // Window title (may be null) + String title = ""; + Pointer namePtr = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, kName); + if (namePtr != null) { + title = new CFStringRef(namePtr).stringValue(); + } + + // Owner name as filePath equivalent + String ownerName = ""; + Pointer ownerPtr = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, kOwnerName); + if (ownerPtr != null) { + ownerName = new CFStringRef(ownerPtr).stringValue(); + } + + // Window ID as HWND + HWND hwnd = null; + Pointer numPtr = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, kNumber); + if (numPtr != null) { + int windowId = new CoreFoundation.CFNumberRef(numPtr).intValue(); + hwnd = new HWND(Pointer.createConstant(windowId)); + } + + // Bounds + Rectangle locAndSize = new Rectangle(); + Pointer boundsPtr = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, kBounds); + if (boundsPtr != null) { + CoreGraphics.CGRect rect = new CoreGraphics.CGRect(); + if (CoreGraphics.INSTANCE.CGRectMakeWithDictionaryRepresentation( + new CFDictionaryRef(boundsPtr), rect)) { + locAndSize = new Rectangle( + (int) rect.origin.x, (int) rect.origin.y, + (int) rect.size.width, (int) rect.size.height); + } + } + + result.add(new DesktopWindow(hwnd, title, ownerName, locAndSize)); + } + } finally { + kName.release(); + kOwnerName.release(); + kBounds.release(); + kNumber.release(); + kLayer.release(); + } + } finally { + windowList.release(); + } + return result; + } } private static class X11WindowUtils extends NativeWindowUtils { private static Pixmap createBitmap(final Display dpy, diff --git a/contrib/platform/src/com/sun/jna/platform/mac/CoreGraphics.java b/contrib/platform/src/com/sun/jna/platform/mac/CoreGraphics.java new file mode 100644 index 000000000..6343e9096 --- /dev/null +++ b/contrib/platform/src/com/sun/jna/platform/mac/CoreGraphics.java @@ -0,0 +1,369 @@ +/* Copyright (c) 2026 Daniel Widdis, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna.platform.mac; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.Structure.FieldOrder; +import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef; +import com.sun.jna.platform.mac.CoreFoundation.CFDictionaryRef; +import com.sun.jna.platform.mac.CoreFoundation.CFStringRef; +import com.sun.jna.ptr.IntByReference; + +/** + * Bindings for the macOS CoreGraphics framework, specifically the Quartz Window + * Services and Quartz Display Services APIs. + *

+ * CoreGraphics provides low-level 2D rendering and, on macOS, services for + * working with display hardware, low-level user input events, and the windowing + * system. + * + * @see Quartz + * Window Services + * @see Quartz + * Display Services + */ +public interface CoreGraphics extends Library { + + CoreGraphics INSTANCE = Native.load("CoreGraphics", CoreGraphics.class); + + // CGWindowListOption constants + + /** List all windows, including off-screen windows. */ + int kCGWindowListOptionAll = 0; + /** List only on-screen windows. */ + int kCGWindowListOptionOnScreenOnly = 1 << 0; + /** List on-screen windows above the specified window. */ + int kCGWindowListOptionOnScreenAboveWindow = 1 << 1; + /** List on-screen windows below the specified window. */ + int kCGWindowListOptionOnScreenBelowWindow = 1 << 2; + /** Include the specified window. */ + int kCGWindowListOptionIncludingWindow = 1 << 3; + /** Exclude desktop elements (wallpaper, icons). */ + int kCGWindowListExcludeDesktopElements = 1 << 4; + + /** A null window ID, used as the relativeToWindow parameter. */ + int kCGNullWindowID = 0; + + // CGWindowImageOption constants + + /** Default window image options. */ + int kCGWindowImageDefault = 0; + /** Include window frame decorations in the image. */ + int kCGWindowImageBoundsIgnoreFraming = 1 << 0; + /** Only include the specified window (not windows below it). */ + int kCGWindowImageShouldBeOpaque = 1 << 1; + /** Only capture the shadow of the window. */ + int kCGWindowImageOnlyShadows = 1 << 2; + /** Use best resolution regardless of display. @since macOS 10.9 */ + int kCGWindowImageBestResolution = 1 << 3; + /** Use nominal resolution. @since macOS 10.9 */ + int kCGWindowImageNominalResolution = 1 << 4; + + // Window info dictionary keys (CFString constants) + + /** + * Key for the window ID ({@code CGWindowID}, a {@code CFNumber}). + */ + String kCGWindowNumber = "kCGWindowNumber"; + /** + * Key for the window's Core Graphics backing store type + * ({@code CFNumber}). + */ + String kCGWindowStoreType = "kCGWindowStoreType"; + /** + * Key for the window layer ({@code CFNumber}). Windows with layer 0 are + * normal windows. + */ + String kCGWindowLayer = "kCGWindowLayer"; + /** + * Key for the window bounds ({@code CFDictionary} with X, Y, Width, + * Height). Use {@link #CGRectMakeWithDictionaryRepresentation} to parse. + */ + String kCGWindowBounds = "kCGWindowBounds"; + /** + * Key for the sharing state ({@code CFNumber}). + */ + String kCGWindowSharingState = "kCGWindowSharingState"; + /** + * Key for the window alpha/opacity ({@code CFNumber}, 0.0-1.0). + */ + String kCGWindowAlpha = "kCGWindowAlpha"; + /** + * Key for the owning process ID ({@code CFNumber}, a {@code pid_t}). + */ + String kCGWindowOwnerPID = "kCGWindowOwnerPID"; + /** + * Key for the memory usage of the window in bytes ({@code CFNumber}). + */ + String kCGWindowMemoryUsage = "kCGWindowMemoryUsage"; + /** + * Key for the window name/title ({@code CFString}). May be absent if the + * window has no title or the caller lacks permissions. + */ + String kCGWindowName = "kCGWindowName"; + /** + * Key for the name of the process that owns the window + * ({@code CFString}). + */ + String kCGWindowOwnerName = "kCGWindowOwnerName"; + /** + * Key for whether the window is on screen ({@code CFBoolean}). + */ + String kCGWindowIsOnscreen = "kCGWindowIsOnscreen"; + /** + * Key for the backing location type ({@code CFNumber}). + */ + String kCGWindowBackingLocationVideoMemory = "kCGWindowBackingLocationVideoMemory"; + + // CGWindowSharingType constants + + /** Window contents cannot be read. */ + int kCGWindowSharingNone = 0; + /** Window contents can be read only by the owning process. */ + int kCGWindowSharingReadOnly = 1; + /** Window contents can be read by any process. */ + int kCGWindowSharingReadWrite = 2; + + // CGWindowBackingType constants + + /** Retained backing store (deprecated). */ + int kCGBackingStoreRetained = 0; + /** Non-retained backing store (deprecated). */ + int kCGBackingStoreNonretained = 1; + /** Buffered backing store. */ + int kCGBackingStoreBuffered = 2; + + /** + * A point in the Core Graphics coordinate system. + * + * @see CGPoint + */ + @FieldOrder({ "x", "y" }) + class CGPoint extends Structure { + /** The x-coordinate of the point. */ + public double x; + /** The y-coordinate of the point. */ + public double y; + } + + /** + * A size in the Core Graphics coordinate system. + * + * @see CGSize + */ + @FieldOrder({ "width", "height" }) + class CGSize extends Structure { + /** The width value. */ + public double width; + /** The height value. */ + public double height; + } + + /** + * A rectangle in the Core Graphics coordinate system. + * + * @see CGRect + */ + @FieldOrder({ "origin", "size" }) + class CGRect extends Structure { + /** The origin (top-left corner) of the rectangle. */ + public CGPoint origin; + /** The size (width and height) of the rectangle. */ + public CGSize size; + + public static class ByValue extends CGRect implements Structure.ByValue { + } + } + + // Quartz Window Services functions + + /** + * Returns information about the windows in the current user session. + *

+ * Each element in the returned array is a {@code CFDictionary} containing + * window properties keyed by the {@code kCGWindow*} constants. + * + * @param option a combination of {@code kCGWindowListOption*} + * constants specifying which windows to include + * @param relativeToWindow the window ID to use as a reference point for + * above/below options, or + * {@link #kCGNullWindowID} for all windows + * @return a {@code CFArray} of {@code CFDictionary} objects describing each + * window, or {@code null} on failure. The caller is responsible for + * releasing the array. + * @see CGWindowListCopyWindowInfo + */ + CFArrayRef CGWindowListCopyWindowInfo(int option, int relativeToWindow); + + /** + * Creates a {@link CGRect} from a dictionary representation (as returned in + * the {@link #kCGWindowBounds} key of window info dictionaries). + * + * @param dict the dictionary containing X, Y, Width, and Height keys + * @param rect a {@link CGRect} structure to populate + * @return {@code true} if the conversion was successful + * @see CGRectMakeWithDictionaryRepresentation + * + */ + boolean CGRectMakeWithDictionaryRepresentation(CFDictionaryRef dict, CGRect rect); + + // Quartz Display Services functions + + /** + * Returns the display ID of the main display. + *

+ * The main display is the display with its screen location at (0,0) in the + * global display coordinate space. In a system without display mirroring, + * the display with the menu bar is typically the main display. + * + * @return the main display ID + * @see CGMainDisplayID + */ + int CGMainDisplayID(); + + /** + * Returns the bounds of a display in the global display coordinate space. + * + * @param display the display ID + * @return a {@link CGRect} describing the display bounds + * @see CGDisplayBounds + */ + CGRect.ByValue CGDisplayBounds(int display); + + /** + * Returns the width in pixels of a display. + * + * @param display the display ID + * @return the width in pixels + * @see CGRectMakeWithDictionaryRepresentation + * + */ + long CGDisplayPixelsWide(int display); + + /** + * Returns the height in pixels of a display. + * + * @param display the display ID + * @return the height in pixels + * @see CGDisplayPixelsHigh + */ + long CGDisplayPixelsHigh(int display); + + /** + * Provides a list of displays that are active (or drawable). + * + * @param maxDisplays maximum number of displays to return + * @param activeDisplays an array to fill with display IDs + * @param displayCount receives the actual number of displays + * @return 0 ({@code kCGErrorSuccess}) on success + * @see CGGetActiveDisplayList + */ + int CGGetActiveDisplayList(int maxDisplays, int[] activeDisplays, IntByReference displayCount); + + /** + * Provides a list of online displays (active or mirrored). + * + * @param maxDisplays maximum number of displays to return + * @param onlineDisplays an array to fill with display IDs + * @param displayCount receives the actual number of displays + * @return 0 ({@code kCGErrorSuccess}) on success + * @see CGGetOnlineDisplayList + */ + int CGGetOnlineDisplayList(int maxDisplays, int[] onlineDisplays, IntByReference displayCount); + + /** + * Returns the rotation angle of a display in degrees. + * + * @param display the display ID + * @return the rotation angle (0, 90, 180, or 270) + * @see CGDisplayRotation + */ + double CGDisplayRotation(int display); + + /** + * Returns whether a display is active. + * + * @param display the display ID + * @return non-zero if the display is active + */ + int CGDisplayIsActive(int display); + + /** + * Returns whether a display is the main display. + * + * @param display the display ID + * @return non-zero if the display is the main display + */ + int CGDisplayIsMain(int display); + + /** + * Returns whether a display is built-in (e.g., a laptop screen). + * + * @param display the display ID + * @return non-zero if the display is built-in + */ + int CGDisplayIsBuiltin(int display); + + /** + * Returns the vendor number for a display. + * + * @param display the display ID + * @return the vendor number + */ + int CGDisplayVendorNumber(int display); + + /** + * Returns the model number for a display. + * + * @param display the display ID + * @return the model number + */ + int CGDisplayModelNumber(int display); + + /** + * Returns the serial number for a display. + * + * @param display the display ID + * @return the serial number + */ + int CGDisplaySerialNumber(int display); +} diff --git a/contrib/platform/test/com/sun/jna/platform/mac/CoreGraphicsTest.java b/contrib/platform/test/com/sun/jna/platform/mac/CoreGraphicsTest.java new file mode 100644 index 000000000..461946926 --- /dev/null +++ b/contrib/platform/test/com/sun/jna/platform/mac/CoreGraphicsTest.java @@ -0,0 +1,194 @@ +/* Copyright (c) 2026 Daniel Widdis, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna.platform.mac; + +import com.sun.jna.Platform; +import com.sun.jna.Pointer; +import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef; +import com.sun.jna.platform.mac.CoreFoundation.CFDictionaryRef; +import com.sun.jna.platform.mac.CoreFoundation.CFStringRef; +import com.sun.jna.ptr.IntByReference; + +import junit.framework.TestCase; + +/** + * Exercise the {@link CoreGraphics} class. + */ +public class CoreGraphicsTest extends TestCase { + + public void testWindowList() { + if (!Platform.isMac()) { + return; + } + CFArrayRef windowList = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo( + CoreGraphics.kCGWindowListOptionAll, CoreGraphics.kCGNullWindowID); + try { + assertNotNull("Window list should not be null", windowList); + int count = windowList.getCount(); + assertTrue("Should have at least one window", count > 0); + System.out.println("Total windows: " + count); + + // Read first window's info + Pointer dictPtr = windowList.getValueAtIndex(0); + assertNotNull("First window dict should not be null", dictPtr); + CFDictionaryRef dict = new CFDictionaryRef(dictPtr); + + // Get window owner name + CFStringRef key = CFStringRef.createCFString(CoreGraphics.kCGWindowOwnerName); + try { + Pointer val = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, key); + if (val != null) { + CFStringRef nameRef = new CFStringRef(val); + System.out.println("First window owner: " + nameRef.stringValue()); + } + } finally { + key.release(); + } + + // Get window PID + key = CFStringRef.createCFString(CoreGraphics.kCGWindowOwnerPID); + try { + Pointer val = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, key); + if (val != null) { + CoreFoundation.CFNumberRef pidNum = new CoreFoundation.CFNumberRef(val); + System.out.println("First window PID: " + pidNum.intValue()); + } + } finally { + key.release(); + } + } finally { + windowList.release(); + } + } + + public void testOnScreenWindows() { + if (!Platform.isMac()) { + return; + } + CFArrayRef windowList = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo( + CoreGraphics.kCGWindowListOptionOnScreenOnly | CoreGraphics.kCGWindowListExcludeDesktopElements, + CoreGraphics.kCGNullWindowID); + try { + assertNotNull("On-screen window list should not be null", windowList); + int count = windowList.getCount(); + assertTrue("Should have at least one on-screen window", count > 0); + System.out.println("On-screen windows (excluding desktop): " + count); + } finally { + windowList.release(); + } + } + + public void testCGRectMakeWithDictionaryRepresentation() { + if (!Platform.isMac()) { + return; + } + // Get a window and parse its bounds + CFArrayRef windowList = CoreGraphics.INSTANCE.CGWindowListCopyWindowInfo( + CoreGraphics.kCGWindowListOptionOnScreenOnly, CoreGraphics.kCGNullWindowID); + try { + int count = windowList.getCount(); + if (count == 0) { + return; + } + Pointer dictPtr = windowList.getValueAtIndex(0); + CFDictionaryRef dict = new CFDictionaryRef(dictPtr); + + CFStringRef boundsKey = CFStringRef.createCFString(CoreGraphics.kCGWindowBounds); + try { + Pointer boundsPtr = CoreFoundation.INSTANCE.CFDictionaryGetValue(dict, boundsKey); + if (boundsPtr != null) { + CFDictionaryRef boundsDict = new CFDictionaryRef(boundsPtr); + CoreGraphics.CGRect rect = new CoreGraphics.CGRect(); + boolean success = CoreGraphics.INSTANCE.CGRectMakeWithDictionaryRepresentation( + boundsDict, rect); + assertTrue("CGRectMakeWithDictionaryRepresentation should succeed", success); + System.out.println("First window bounds: x=" + rect.origin.x + + " y=" + rect.origin.y + + " w=" + rect.size.width + + " h=" + rect.size.height); + } + } finally { + boundsKey.release(); + } + } finally { + windowList.release(); + } + } + + public void testMainDisplayID() { + if (!Platform.isMac()) { + return; + } + int mainDisplay = CoreGraphics.INSTANCE.CGMainDisplayID(); + assertTrue("Main display ID should be positive", mainDisplay > 0); + System.out.println("Main display ID: " + mainDisplay); + } + + public void testGetActiveDisplayList() { + if (!Platform.isMac()) { + return; + } + IntByReference countRef = new IntByReference(); + int err = CoreGraphics.INSTANCE.CGGetActiveDisplayList(0, null, countRef); + assertEquals("CGGetActiveDisplayList should succeed", 0, err); + int count = countRef.getValue(); + assertTrue("Should have at least one active display", count > 0); + + int[] displays = new int[count]; + err = CoreGraphics.INSTANCE.CGGetActiveDisplayList(count, displays, countRef); + assertEquals("CGGetActiveDisplayList should succeed", 0, err); + System.out.println("Active displays: " + count); + + for (int i = 0; i < count; i++) { + long width = CoreGraphics.INSTANCE.CGDisplayPixelsWide(displays[i]); + long height = CoreGraphics.INSTANCE.CGDisplayPixelsHigh(displays[i]); + assertTrue("Display width should be positive", width > 0); + assertTrue("Display height should be positive", height > 0); + System.out.println(" Display " + displays[i] + ": " + width + "x" + height); + } + } + + public void testDisplayProperties() { + if (!Platform.isMac()) { + return; + } + int mainDisplay = CoreGraphics.INSTANCE.CGMainDisplayID(); + + int isActive = CoreGraphics.INSTANCE.CGDisplayIsActive(mainDisplay); + assertTrue("Main display should be active", isActive != 0); + + int isMain = CoreGraphics.INSTANCE.CGDisplayIsMain(mainDisplay); + assertTrue("Main display should report as main", isMain != 0); + + double rotation = CoreGraphics.INSTANCE.CGDisplayRotation(mainDisplay); + assertTrue("Rotation should be 0, 90, 180, or 270", + rotation == 0 || rotation == 90 || rotation == 180 || rotation == 270); + System.out.println("Main display rotation: " + rotation); + + CoreGraphics.CGRect.ByValue bounds = CoreGraphics.INSTANCE.CGDisplayBounds(mainDisplay); + assertTrue("Display width should be positive", bounds.size.width > 0); + assertTrue("Display height should be positive", bounds.size.height > 0); + System.out.println("Main display bounds: " + bounds.size.width + "x" + bounds.size.height); + } +}