| 258 | 1 /* | 
|  | 2  * win/quartz.cc: support for macOS (the Quartz Compositor) | 
|  | 3  * | 
|  | 4  * This file does not require an Objective-C++ compiler, | 
|  | 5  * but it *does* require an Objective-C runtime. | 
|  | 6  */ | 
|  | 7 #include "animone/win/quartz.h" | 
|  | 8 #include "animone.h" | 
|  | 9 #include "animone/util/osx.h" | 
|  | 10 | 
|  | 11 #include <objc/message.h> | 
|  | 12 #include <objc/runtime.h> | 
|  | 13 | 
|  | 14 #include <ApplicationServices/ApplicationServices.h> | 
|  | 15 #include <CoreFoundation/CoreFoundation.h> | 
|  | 16 #include <CoreGraphics/CoreGraphics.h> | 
|  | 17 | 
|  | 18 namespace animone::internal::quartz { | 
|  | 19 | 
|  | 20 #if __LP64__ | 
|  | 21 typedef long NSInteger; | 
|  | 22 #else | 
|  | 23 typedef int NSInteger; | 
|  | 24 #endif | 
|  | 25 typedef int CGSConnection; | 
|  | 26 | 
|  | 27 typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void); | 
|  | 28 typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*); | 
|  | 29 | 
|  | 30 static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr; | 
|  | 31 static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr; | 
|  | 32 | 
|  | 33 static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics"); | 
|  | 34 | 
|  | 35 /* Objective-C */ | 
|  | 36 typedef id (*object_message_send)(id, SEL, ...); | 
|  | 37 typedef id (*class_message_send)(Class, SEL, ...); | 
|  | 38 | 
|  | 39 static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend); | 
|  | 40 static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend); | 
|  | 41 | 
|  | 42 static bool GetCoreGraphicsPrivateSymbols() { | 
|  | 43 	CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID); | 
|  | 44 	if (!core_graphics_bundle) | 
|  | 45 		return false; | 
|  | 46 | 
|  | 47 	CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName( | 
|  | 48 	    core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread")); | 
|  | 49 	if (!CGSDefaultConnectionForThread) | 
|  | 50 		return false; | 
|  | 51 | 
|  | 52 	CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName( | 
|  | 53 	    core_graphics_bundle, CFSTR("CGSCopyWindowProperty")); | 
|  | 54 	if (!CGSCopyWindowProperty) | 
|  | 55 		return false; | 
|  | 56 | 
|  | 57 	return true; | 
|  | 58 } | 
|  | 59 | 
|  | 60 template<typename T> | 
|  | 61 static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) { | 
|  | 62 	CFTypeRef data = nullptr; | 
|  | 63 	if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data) | 
|  | 64 		return false; | 
|  | 65 | 
|  | 66 	if constexpr (std::is_arithmetic<T>::value) | 
|  | 67 		osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out); | 
|  | 68 	else if constexpr (std::is_same<T, std::string>::value) | 
|  | 69 		osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out); | 
|  | 70 	else | 
|  | 71 		return false; | 
|  | 72 | 
|  | 73 	return true; | 
|  | 74 } | 
|  | 75 | 
|  | 76 static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) { | 
|  | 77 	CGRect bounds = {0}; | 
|  | 78 	{ | 
|  | 79 		const CGWindowID wids[1] = {wid}; | 
|  | 80 		CFPtr<CFArrayRef> arr(CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL)); | 
|  | 81 		CFPtr<CFArrayRef> dicts(CGWindowListCreateDescriptionFromArray(arr)); | 
|  | 82 | 
|  | 83 		if (!dicts.get() || CFArrayGetCount(dicts.get()) < 1) | 
|  | 84 			return false; | 
|  | 85 | 
|  | 86 		CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(dicts, 0)); | 
|  | 87 		if (!dict) | 
|  | 88 			return false; | 
|  | 89 | 
|  | 90 		CFDictionaryRef bounds_dict = nullptr; | 
|  | 91 		if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) || | 
|  | 92 		    !bounds_dict) | 
|  | 93 			return false; | 
|  | 94 | 
|  | 95 		if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) | 
|  | 96 			return false; | 
|  | 97 	} | 
|  | 98 | 
|  | 99 	/* now we can actually do stuff */ | 
|  | 100 	AXUIElementRef axapp = AXUIElementCreateApplication(pid); | 
|  | 101 	CFPtr<CFArrayRef> windows; | 
|  | 102 	{ | 
|  | 103 		CFArrayRef ref; | 
|  | 104 		if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, reinterpret_cast<CFTypeRef*>(&ref)) != | 
|  | 105 		     kAXErrorSuccess) || | 
|  | 106 		    !windows) | 
|  | 107 			return false; | 
|  | 108 | 
|  | 109 		windows.reset(ref); | 
|  | 110 	} | 
|  | 111 | 
|  | 112 	const CFIndex count = CFArrayGetCount(windows.get()); | 
|  | 113 	for (CFIndex i = 0; i < count; i++) { | 
|  | 114 		const AXUIElementRef window = reinterpret_cast<AXUIElementRef>(CFArrayGetValueAtIndex(windows.get(), i)); | 
|  | 115 | 
|  | 116 		/* does this leak memory? probably. */ | 
|  | 117 		AXValueRef val; | 
|  | 118 		if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, reinterpret_cast<CFTypeRef*>(&val)) == | 
|  | 119 		    kAXErrorSuccess) { | 
|  | 120 			CGPoint point; | 
|  | 121 			if (!AXValueGetValue(val, kAXValueTypeCGPoint, reinterpret_cast<CFTypeRef>(&point)) || | 
|  | 122 			    (point.x != bounds.origin.x || point.y != bounds.origin.y)) | 
|  | 123 				continue; | 
|  | 124 		} else | 
|  | 125 			continue; | 
|  | 126 | 
|  | 127 		if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, reinterpret_cast<CFTypeRef*>(&val)) == | 
|  | 128 		    kAXErrorSuccess) { | 
|  | 129 			CGSize size; | 
|  | 130 			if (!AXValueGetValue(val, kAXValueTypeCGSize, reinterpret_cast<CFTypeRef>(&size)) || | 
|  | 131 			    (size.width != bounds.size.width || size.height != bounds.size.height)) | 
|  | 132 				continue; | 
|  | 133 		} else | 
|  | 134 			continue; | 
|  | 135 | 
|  | 136 		CFStringRef title; | 
|  | 137 		if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, reinterpret_cast<CFTypeRef*>(&title)) == | 
|  | 138 		    kAXErrorSuccess) | 
|  | 139 			return osx::util::StringFromCFString(title, result); | 
|  | 140 	} | 
|  | 141 | 
|  | 142 	return false; | 
|  | 143 } | 
|  | 144 | 
|  | 145 static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) { | 
|  | 146 	/* try using CoreGraphics (only usable on old versions of OS X) */ | 
|  | 147 	if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) { | 
|  | 148 		CFPtr<CFStringRef> title; | 
|  | 149 		{ | 
|  | 150 			CFStringRef t = nullptr; | 
|  | 151 			CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &t); | 
|  | 152 			title.reset(t); | 
|  | 153 		} | 
|  | 154 | 
|  | 155 		if (title && CFStringGetLength(title.get()) && osx::util::StringFromCFString(title.get(), result)) | 
|  | 156 			return true; | 
|  | 157 	} | 
|  | 158 | 
|  | 159 	/* then try linking to a window using the accessibility API */ | 
|  | 160 	return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false; | 
|  | 161 } | 
|  | 162 | 
|  | 163 static bool GetProcessBundleIdentifierNew(pid_t pid, std::string& result) { | 
|  | 164 	/* 10.6 and higher */ | 
|  | 165 	const id app = | 
|  | 166 	    cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid); | 
|  | 167 	if (!app) | 
|  | 168 		return false; | 
|  | 169 | 
|  | 170 	CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier"))); | 
|  | 171 	if (!bundle_id) | 
|  | 172 		return false; | 
|  | 173 | 
|  | 174 	result = osx::util::StringFromCFString(bundle_id, result); | 
|  | 175 	return true; | 
|  | 176 } | 
|  | 177 | 
|  | 178 static bool GetProcessBundleIdentifierOld(pid_t pid, std::string& result) { | 
|  | 179 	/* OS X 10.2; deprecated in 10.9 */ | 
|  | 180 	ProcessSerialNumber psn; | 
|  | 181 	if (GetProcessForPID(pid, &psn)) | 
|  | 182 		return false; | 
|  | 183 | 
|  | 184 	CFPtr<CFDictionaryRef> info = ProcessInformationCopyDictionary(psn, kProcessDictionaryIncludeAllInformationMask); | 
|  | 185 	if (!info) | 
|  | 186 		return false; | 
|  | 187 | 
|  | 188 	CFStringRef value = reinterpret_cast<CFStringRef>(CFDictionaryGetValue(dict, CFSTR("CFBundleIdentifier"))); | 
|  | 189 	if (!value) | 
|  | 190 		return false; | 
|  | 191 | 
|  | 192 	result = osx::util::StringFromCFString(value, result); | 
|  | 193 	return true; | 
|  | 194 } | 
|  | 195 | 
|  | 196 static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) { | 
|  | 197 	/* The Bundle ID is essentially OS X's solution to Windows' | 
|  | 198 	 * "class name"; theoretically, it should be different for | 
|  | 199 	 * each program, although it requires an app bundle. | 
|  | 200 	 */ | 
|  | 201 	if (GetProcessBundleIdentifierNew(pid, result)) | 
|  | 202 		return true; | 
|  | 203 | 
|  | 204 	return GetProcessBundleIdentifierOld(); | 
|  | 205 } | 
|  | 206 | 
|  | 207 bool EnumerateWindows(window_proc_t window_proc) { | 
|  | 208 	if (!window_proc) | 
|  | 209 		return false; | 
|  | 210 | 
|  | 211 	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); | 
|  | 212 	if (!windows) | 
|  | 213 		return false; | 
|  | 214 | 
|  | 215 	const CFIndex count = CFArrayGetCount(windows); | 
|  | 216 	for (CFIndex i = 0; i < count; i++) { | 
|  | 217 		CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windows, i)); | 
|  | 218 		if (!window) | 
|  | 219 			continue; | 
|  | 220 | 
|  | 221 		Process proc; | 
|  | 222 		{ | 
|  | 223 			CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid); | 
|  | 224 			if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.name)) | 
|  | 225 				osx::util::GetProcessName(proc.pid, proc.name); | 
|  | 226 		} | 
|  | 227 | 
|  | 228 		Window win; | 
|  | 229 		{ | 
|  | 230 			CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id); | 
|  | 231 | 
|  | 232 			if (!GetProcessBundleIdentifier(proc.pid, win.class_name)) | 
|  | 233 				// Fallback to the Quartz window name, which is unlikely to be filled, but it | 
|  | 234 				// *could* be. | 
|  | 235 				CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name); | 
|  | 236 | 
|  | 237 			GetWindowTitle(win.id, proc.pid, win.text); | 
|  | 238 		} | 
|  | 239 | 
|  | 240 		if (!window_proc(proc, win)) { | 
|  | 241 			CFRelease(windows); | 
|  | 242 			return false; | 
|  | 243 		} | 
|  | 244 	} | 
|  | 245 | 
|  | 246 	CFRelease(windows); | 
|  | 247 | 
|  | 248 	return true; | 
|  | 249 } | 
|  | 250 | 
|  | 251 } // namespace animone::internal::quartz |