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