view dep/animone/src/win/quartz.cc @ 342:adb79bdde329

dep/animone: fix tons of issues for example, the window ID stuff was just... completely wrong. since we're supporting multiple different window systems, it *has* to be a union rather than just a single integer type. HWND is also not a DWORD, it's a pointer(!), so now it's stored as a std::uintptr_t. (this probably breaks things)
author Paper <paper@paper.us.eu.org>
date Thu, 20 Jun 2024 03:03:05 -0400
parents a7d4e5107531
children
line wrap: on
line source

/*
 * 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/fd.h"
#include "animone.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 {

template<typename T>
struct CFDeconstructor {
	using pointer = T;
	void operator()(pointer t) const { ::CFRelease(t); };
};

template<typename T>
using CFPtr = std::unique_ptr<T, CFDeconstructor<T>>;

#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 GetCFNumber(CFNumberRef num, T& result) {
	if (!num)
		return false;

	int64_t res;
	if (!CFNumberGetValue(num, static_cast<CFNumberType>(4), &res))
		return false;

	result = static_cast<T>(res);
	return true;
}

static bool StringFromCFString(CFStringRef string, std::string& result) {
	if (!string)
		return false;

	result.resize(CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1);
	if (!CFStringGetCString(string, &result.front(), result.length(), kCFStringEncodingUTF8))
		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)
		GetCFNumber(reinterpret_cast<CFNumberRef>(data), out);
	else if constexpr (std::is_same<T, std::string>::value)
		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.get()));

		if (!dicts.get() || CFArrayGetCount(dicts.get()) < 1)
			return false;

		CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(dicts.get(), 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, static_cast<AXValueType>(kAXValueCGPointType), reinterpret_cast<void*>(&point)) ||
			    (point.x != bounds.origin.x || point.y != bounds.origin.y)) {
				CFRelease(val);
				continue;
			}
		} else {
			CFRelease(val);
			continue;
		}

		CFRelease(val);

		if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, reinterpret_cast<CFTypeRef*>(&val)) ==
		    kAXErrorSuccess) {
			CGSize size;
			if (!AXValueGetValue(val, static_cast<AXValueType>(kAXValueCGSizeType), reinterpret_cast<void*>(&size)) ||
			    (size.width != bounds.size.width || size.height != bounds.size.height)) {
				CFRelease(val);
				continue;
			}
		} else {
			CFRelease(val);
			continue;
		}

		CFRelease(val);

		CFStringRef title;
		if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, reinterpret_cast<CFTypeRef*>(&title)) ==
		    kAXErrorSuccess) {
			bool success = StringFromCFString(title, result);
			CFRelease(title);
			return success;
		}
	}

	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()) && 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((Class)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;

	return StringFromCFString(bundle_id, result);
}

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(info.get(), CFSTR("CFBundleIdentifier")));
	if (!value)
		return false;

	return StringFromCFString(value, 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.
	 */
	if (GetProcessBundleIdentifierNew(pid, result))
		return true;

	return GetProcessBundleIdentifierOld(pid, result);
}

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;
		proc.platform = ExecutablePlatform::Xnu;
		CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid);
		if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.comm))
			fd::GetProcessName(proc.pid, proc.comm);

		Window win;
		win.platform = WindowPlatform::Quartz;
		CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id.quartz);

		GetProcessBundleIdentifier(proc.pid, 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