diff dep/animone/src/win/quartz.cc @ 258:862d0d8619f6

*: HUUUGE changes animia has been renamed to animone, so instead of thinking of a health condition, you think of a beautiful flower :) I've also edited some of the code for animone, but I have no idea if it even works or not because I don't have a mac or windows machine lying around. whoops! ... anyway, all of the changes divergent from Anisthesia are now licensed under BSD. it's possible that I could even rewrite most of the code to where I don't even have to keep the MIT license, but that's thinking too far into the future I've been slacking off on implementing the anime seasons page, mostly out of laziness. I think I'd have to create another db file specifically for the seasons anyway, this code is being pushed *primarily* because the hard drive it's on is failing! yay :)
author Paper <paper@paper.us.eu.org>
date Mon, 01 Apr 2024 02:43:44 -0400
parents
children 382b50754fe4
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dep/animone/src/win/quartz.cc	Mon Apr 01 02:43:44 2024 -0400
@@ -0,0 +1,251 @@
+/*
+ * 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.
+ */
+#include "animone/win/quartz.h"
+#include "animone.h"
+#include "animone/util/osx.h"
+
+#include <objc/message.h>
+#include <objc/runtime.h>
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreGraphics/CoreGraphics.h>
+
+namespace animone::internal::quartz {
+
+#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 GetCoreGraphicsPrivateSymbols() {
+	CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID);
+	if (!core_graphics_bundle)
+		return false;
+
+	CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName(
+	    core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread"));
+	if (!CGSDefaultConnectionForThread)
+		return false;
+
+	CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName(
+	    core_graphics_bundle, CFSTR("CGSCopyWindowProperty"));
+	if (!CGSCopyWindowProperty)
+		return false;
+
+	return true;
+}
+
+template<typename T>
+static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) {
+	CFTypeRef data = nullptr;
+	if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data)
+		return false;
+
+	if constexpr (std::is_arithmetic<T>::value)
+		osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out);
+	else if constexpr (std::is_same<T, std::string>::value)
+		osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out);
+	else
+		return false;
+
+	return true;
+}
+
+static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) {
+	CGRect bounds = {0};
+	{
+		const CGWindowID wids[1] = {wid};
+		CFPtr<CFArrayRef> arr(CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL));
+		CFPtr<CFArrayRef> dicts(CGWindowListCreateDescriptionFromArray(arr));
+
+		if (!dicts.get() || CFArrayGetCount(dicts.get()) < 1)
+			return false;
+
+		CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(dicts, 0));
+		if (!dict)
+			return false;
+
+		CFDictionaryRef bounds_dict = nullptr;
+		if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) ||
+		    !bounds_dict)
+			return false;
+
+		if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds))
+			return false;
+	}
+
+	/* now we can actually do stuff */
+	AXUIElementRef axapp = AXUIElementCreateApplication(pid);
+	CFPtr<CFArrayRef> windows;
+	{
+		CFArrayRef ref;
+		if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, reinterpret_cast<CFTypeRef*>(&ref)) !=
+		     kAXErrorSuccess) ||
+		    !windows)
+			return false;
+
+		windows.reset(ref);
+	}
+
+	const CFIndex count = CFArrayGetCount(windows.get());
+	for (CFIndex i = 0; i < count; i++) {
+		const AXUIElementRef window = reinterpret_cast<AXUIElementRef>(CFArrayGetValueAtIndex(windows.get(), i));
+
+		/* does this leak memory? probably. */
+		AXValueRef val;
+		if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, reinterpret_cast<CFTypeRef*>(&val)) ==
+		    kAXErrorSuccess) {
+			CGPoint point;
+			if (!AXValueGetValue(val, kAXValueTypeCGPoint, reinterpret_cast<CFTypeRef>(&point)) ||
+			    (point.x != bounds.origin.x || point.y != bounds.origin.y))
+				continue;
+		} else
+			continue;
+
+		if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, reinterpret_cast<CFTypeRef*>(&val)) ==
+		    kAXErrorSuccess) {
+			CGSize size;
+			if (!AXValueGetValue(val, kAXValueTypeCGSize, reinterpret_cast<CFTypeRef>(&size)) ||
+			    (size.width != bounds.size.width || size.height != bounds.size.height))
+				continue;
+		} else
+			continue;
+
+		CFStringRef title;
+		if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, reinterpret_cast<CFTypeRef*>(&title)) ==
+		    kAXErrorSuccess)
+			return osx::util::StringFromCFString(title, result);
+	}
+
+	return false;
+}
+
+static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) {
+	/* try using CoreGraphics (only usable on old versions of OS X) */
+	if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) {
+		CFPtr<CFStringRef> title;
+		{
+			CFStringRef t = nullptr;
+			CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &t);
+			title.reset(t);
+		}
+
+		if (title && CFStringGetLength(title.get()) && osx::util::StringFromCFString(title.get(), result))
+			return true;
+	}
+
+	/* then try linking to a window using the accessibility API */
+	return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false;
+}
+
+static bool GetProcessBundleIdentifierNew(pid_t pid, std::string& result) {
+	/* 10.6 and higher */
+	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;
+}
+
+static bool GetProcessBundleIdentifierOld(pid_t pid, std::string& result) {
+	/* OS X 10.2; deprecated in 10.9 */
+	ProcessSerialNumber psn;
+	if (GetProcessForPID(pid, &psn))
+		return false;
+
+	CFPtr<CFDictionaryRef> info = ProcessInformationCopyDictionary(psn, kProcessDictionaryIncludeAllInformationMask);
+	if (!info)
+		return false;
+
+	CFStringRef value = reinterpret_cast<CFStringRef>(CFDictionaryGetValue(dict, CFSTR("CFBundleIdentifier")));
+	if (!value)
+		return false;
+
+	result = osx::util::StringFromCFString(value, result);
+	return true;
+}
+
+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.
+	 */
+	if (GetProcessBundleIdentifierNew(pid, result))
+		return true;
+
+	return GetProcessBundleIdentifierOld();
+}
+
+bool EnumerateWindows(window_proc_t window_proc) {
+	if (!window_proc)
+		return false;
+
+	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
+	if (!windows)
+		return false;
+
+	const CFIndex count = CFArrayGetCount(windows);
+	for (CFIndex i = 0; i < count; i++) {
+		CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windows, i));
+		if (!window)
+			continue;
+
+		Process proc;
+		{
+			CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid);
+			if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.name))
+				osx::util::GetProcessName(proc.pid, proc.name);
+		}
+
+		Window win;
+		{
+			CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id);
+
+			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, proc.pid, win.text);
+		}
+
+		if (!window_proc(proc, win)) {
+			CFRelease(windows);
+			return false;
+		}
+	}
+
+	CFRelease(windows);
+
+	return true;
+}
+
+} // namespace animone::internal::quartz