diff dep/animia/src/win/quartz.cc @ 237:a7d0d543b334

*: make OS X builds succeed new script: deploy_build.sh, creates the app bundle
author Paper <paper@paper.us.eu.org>
date Fri, 19 Jan 2024 11:14:44 -0500
parents bc1ae1810855
children
line wrap: on
line diff
--- a/dep/animia/src/win/quartz.cc	Fri Jan 19 00:24:02 2024 -0500
+++ b/dep/animia/src/win/quartz.cc	Fri Jan 19 11:14:44 2024 -0500
@@ -2,8 +2,7 @@
  * win/quartz.cc: support for macOS (the Quartz Compositor)
  *
  * This file does not require an Objective-C++ compiler,
- * but it *does* require an Objective-C runtime and linking
- * with AppKit in order to receive proper window titles.
+ * but it *does* require an Objective-C runtime.
 */
 #include "animia/win/quartz.h"
 #include "animia/util/osx.h"
@@ -14,51 +13,50 @@
 
 #include <CoreFoundation/CoreFoundation.h>
 #include <CoreGraphics/CoreGraphics.h>
+#include <ApplicationServices/ApplicationServices.h>
 
 namespace animia::internal::quartz {
 
+/* all of these LaunchServices things use *internal functions* that are subject
+ * to change. Granted, it's not very likely that these will change very much
+ * because I'm fairly sure Apple uses them lots in their own internal code.
+*/
+#if __LP64__
+typedef long NSInteger;
+#else
+typedef int NSInteger;
+#endif
+typedef int CGSConnection;
+
+typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void);
+typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*);
+
+static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr;
+static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr;
+
+static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics");
+
+/* Objective-C */
 typedef id (*object_message_send)(id, SEL, ...);
 typedef id (*class_message_send)(Class, SEL, ...);
 
 static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend);
 static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend);
 
-static bool GetWindowTitle(unsigned int wid, std::string& result) {
-	// NSApplication* app = [NSApplication sharedApplication];
-	const id app = cls_send(objc_getClass("NSApplication"), sel_getUid("sharedApplication"));
-
-	// NSWindow* window = [app windowWithWindowNumber: wid];
-	const id window = obj_send(app, sel_getUid("windowWithWindowNumber:"), wid);
-	if (!window)
-		return false;
-
-	// NSString* title = [window title];
-	const CFStringRef title = reinterpret_cast<CFStringRef>(obj_send(window, sel_getUid("title")));
-	if (!title)
+static bool GetCoreGraphicsPrivateSymbols() {
+	CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID);
+	if (!core_graphics_bundle)
 		return false;
 
-	// return [title UTF8String];
-	return osx::util::StringFromCFString(title, result);
-}
-
-static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) {
-	/* The Bundle ID is essentially OS X's solution to Windows'
-	 * "class name"; theoretically, it should be different for
-	 * each program, although it requires an app bundle.
-	*/
-
-	// NSRunningApplication* app = [NSRunningApplication runningApplicationWithProcessIdentifier: pid];
-	const id app = cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid);
-	if (!app)
+	CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName(core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread"));
+	if (!CGSDefaultConnectionForThread)
 		return false;
 
-	// NSString* bundle_id = [app bundleIdentifier];
-	const CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
-	if (!bundle_id)
+	CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName(core_graphics_bundle, CFSTR("CGSCopyWindowProperty"));
+	if (!CGSCopyWindowProperty)
 		return false;
 
-	// return [bundle_id UTF8String];
-	return osx::util::StringFromCFString(bundle_id, result);
+	return true;
 }
 
 template<typename T>
@@ -77,11 +75,110 @@
 	return true;
 }
 
+static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) {
+	CGRect bounds = {0};
+	{
+		const CGWindowID wids[1] = {wid};
+		CFArrayRef arr = CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL);
+
+		CFArrayRef dicts = CGWindowListCreateDescriptionFromArray(arr);
+
+		CFRelease(arr);
+
+		if (!dicts || CFArrayGetCount(dicts) < 1)
+			return false;
+
+		CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(dicts, 0);
+		if (!dict) {
+			CFRelease(dicts);
+			return false;
+		}
+
+		CFDictionaryRef bounds_dict = nullptr;
+		if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) || !bounds_dict) {
+			CFRelease(dicts);
+			return false;
+		}
+
+		if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) {
+			CFRelease(dicts);
+			return false;
+		}
+
+		CFRelease(dicts);
+	}
+
+	/* now we can actually do stuff */
+	AXUIElementRef axapp = AXUIElementCreateApplication(pid);
+	CFArrayRef windows;
+	if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, (CFTypeRef*)&windows) != kAXErrorSuccess) || !windows)
+		return false;
+
+	const CFIndex count = CFArrayGetCount(windows);
+	for (CFIndex i = 0; i < count; i++) {
+		const AXUIElementRef window = (AXUIElementRef)CFArrayGetValueAtIndex(windows, i);
+
+		AXValueRef val;
+		if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, (CFTypeRef*)&val) == kAXErrorSuccess) {
+			CGPoint point;
+			if (!AXValueGetValue(val, kAXValueTypeCGPoint, (void*)&point) || (point.x != bounds.origin.x || point.y != bounds.origin.y))
+				continue;
+		} else continue;
+
+		if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, (CFTypeRef*)&val) == kAXErrorSuccess) {
+			CGSize size;
+			if (!AXValueGetValue(val, kAXValueTypeCGSize, (void*)&size) || (size.width != bounds.size.width || size.height != bounds.size.height))
+				continue;
+		} else continue;
+
+		CFStringRef title;
+		if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, (CFTypeRef*)&title) == kAXErrorSuccess) {
+			CFRelease(windows);
+			return osx::util::StringFromCFString(title, result);
+		}
+	}
+
+	CFRelease(windows);
+
+	return false;
+}
+
+static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) {
+	/* private internal OS X functions */
+	if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) {
+		CFStringRef title = nullptr;
+
+		CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &title);
+		if (title && CFStringGetLength(title) && osx::util::StringFromCFString(title, result))
+			return true;
+	}
+
+	/* don't attempt to use accessibility if we aren't trusted */
+	return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false;
+}
+
+static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) {
+	/* The Bundle ID is essentially OS X's solution to Windows'
+	 * "class name"; theoretically, it should be different for
+	 * each program, although it requires an app bundle.
+	*/
+	const id app = cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid);
+	if (!app)
+		return false;
+
+	CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
+	if (!bundle_id)
+		return false;
+
+	result = osx::util::StringFromCFString(bundle_id, result);
+	return true;
+}
+
 bool EnumerateWindows(window_proc_t window_proc) {
 	if (!window_proc)
 		return false;
 
-	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
+	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
 	if (!windows)
 		return false;
 
@@ -102,13 +199,12 @@
 		{
 			CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id);
 
-			if (!GetProcessBundleIdentifier(proc.pid, win.class_name)) {
+			if (!GetProcessBundleIdentifier(proc.pid, win.class_name))
 				// Fallback to the Quartz window name, which is unlikely to be filled, but it
 				// *could* be.
 				CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name);
-			}
 
-			GetWindowTitle(win.id, win.text);
+			GetWindowTitle(win.id, proc.pid, win.text);
 		}
 
 		if (!window_proc(proc, win)) {