view dep/animia/src/win/quartz.cc @ 191:0fc126d52de4

animia: multiple stylistic choices win.class_name is now used to store bundle IDs on OS X, add some little explanations here and there for dumb stuff
author Paper <mrpapersonic@gmail.com>
date Thu, 07 Dec 2023 01:48:04 -0500
parents 2d5823df870f
children 50108040d792
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 GetWindowBundleIdentifier(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);
}

bool QuartzWinTools::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;
		{
			{
				CFNumberRef num = nullptr;
				if (CFDictionaryGetValueIfPresent(window, CFSTR("kCGWindowOwnerPID"), reinterpret_cast<const void**>(&num)) && num)
					osx::util::GetCFNumber(num, proc.pid);
			}
			{
				CFStringRef str = nullptr;
				if (CFDictionaryGetValueIfPresent(window, CFSTR("kCGWindowOwnerName"), reinterpret_cast<const void**>(&str)) && str)
					osx::util::StringFromCFString(str, proc.name);
			}
			if (proc.name.empty())
				osx::util::GetProcessName(proc.pid, proc.name);
		}

		Window win;
		{
			{
				CFNumberRef num = nullptr;
				if (CFDictionaryGetValueIfPresent(window, CFSTR("kCGWindowNumber"), reinterpret_cast<const void**>(&num)) && num)
					osx::util::GetCFNumber(num, win.id);
			}
			if (!GetWindowBundleIdentifier(proc.pid, win.class_name)) {
				// Fallback to the Quartz window name, which is unlikely to be filled, but it
				// *could* be.
				CFStringRef str = nullptr;
				if (CFDictionaryGetValueIfPresent(window, CFSTR("kCGWindowName"), reinterpret_cast<const void**>(&str)) && str)
					osx::util::StringFromCFString(str, 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