view dep/animia/src/win/quartz.cc @ 198:bc1ae1810855

dep/animia: switch from using classes to global functions the old idea was ok, but sort of hackish; this method doesn't use classes at all, and this way (especially important!) we can do wayland stuff AND x11 at the same time, which wasn't really possible without stupid workarounds in the other method
author Paper <mrpapersonic@gmail.com>
date Sun, 24 Dec 2023 02:59:42 -0500
parents c4ca035c565d
children a7d0d543b334
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 and linking
 * with AppKit in order to receive proper window titles.
*/
#include "animia/win/quartz.h"
#include "animia/util/osx.h"
#include "animia.h"

#include <objc/runtime.h>
#include <objc/message.h>

#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>

namespace animia::internal::quartz {

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)
		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)
		return false;

	// NSString* bundle_id = [app bundleIdentifier];
	const CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
	if (!bundle_id)
		return false;

	// return [bundle_id UTF8String];
	return osx::util::StringFromCFString(bundle_id, result);
}

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;
}

bool EnumerateWindows(window_proc_t window_proc) {
	if (!window_proc)
		return false;

	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, 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, win.text);
		}

		if (!window_proc(proc, win)) {
			CFRelease(windows);
			return false;
		}
	}

	CFRelease(windows);

	return true;
}

} // namespace animia::win::detail