Mercurial > libanimone
annotate src/win/quartz.cc @ 4:cce3a81b03bf
dep/animone: make osx code a bit less hacky
it would be nice if macos actually provided a real API for getting
window titles (outside of the accessibility api). the accessibility
API is a real mess to work with; the user has to give permission to
access it under newer versions.
| author | Paper <paper@paper.us.eu.org> |
|---|---|
| date | Fri, 12 Apr 2024 05:21:45 -0400 |
| parents | a76fa32bdc92 |
| children | f80b3c3ec7f0 |
| rev | line source |
|---|---|
| 0 | 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 | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
20 template<typename T> |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
21 using CFPtr = std::unique_ptr<T, CFDecontructor<T>>; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
22 |
| 0 | 23 #if __LP64__ |
| 24 typedef long NSInteger; | |
| 25 #else | |
| 26 typedef int NSInteger; | |
| 27 #endif | |
| 28 typedef int CGSConnection; | |
| 29 | |
| 30 typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void); | |
| 31 typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*); | |
| 32 | |
| 33 static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr; | |
| 34 static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr; | |
| 35 | |
| 36 static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics"); | |
| 37 | |
| 38 /* Objective-C */ | |
| 39 typedef id (*object_message_send)(id, SEL, ...); | |
| 40 typedef id (*class_message_send)(Class, SEL, ...); | |
| 41 | |
| 42 static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend); | |
| 43 static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend); | |
| 44 | |
| 45 static bool GetCoreGraphicsPrivateSymbols() { | |
| 46 CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID); | |
| 47 if (!core_graphics_bundle) | |
| 48 return false; | |
| 49 | |
| 50 CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName( | |
| 51 core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread")); | |
| 52 if (!CGSDefaultConnectionForThread) | |
| 53 return false; | |
| 54 | |
| 55 CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName( | |
| 56 core_graphics_bundle, CFSTR("CGSCopyWindowProperty")); | |
| 57 if (!CGSCopyWindowProperty) | |
| 58 return false; | |
| 59 | |
| 60 return true; | |
| 61 } | |
| 62 | |
| 63 template<typename T> | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
64 static bool GetCFNumber(CFNumberRef num, T& result) { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
65 if (!num) |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
66 return false; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
67 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
68 int64_t res; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
69 if (!CFNumberGetValue(num, static_cast<CFNumberType>(4), &res)) |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
70 return false; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
71 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
72 result = static_cast<T>(res); |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
73 return true; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
74 } |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
75 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
76 static bool StringFromCFString(CFStringRef string, std::string& result) { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
77 if (!string) |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
78 return false; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
79 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
80 result.resize(CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1); |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
81 if (!CFStringGetCString(string, &result.front(), result.length(), kCFStringEncodingUTF8)) |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
82 return false; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
83 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
84 return true; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
85 } |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
86 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
87 template<typename T> |
| 0 | 88 static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) { |
| 89 CFTypeRef data = nullptr; | |
| 90 if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data) | |
| 91 return false; | |
| 92 | |
| 93 if constexpr (std::is_arithmetic<T>::value) | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
94 GetCFNumber(reinterpret_cast<CFNumberRef>(data), out); |
| 0 | 95 else if constexpr (std::is_same<T, std::string>::value) |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
96 StringFromCFString(reinterpret_cast<CFStringRef>(data), out); |
| 0 | 97 else |
| 98 return false; | |
| 99 | |
| 100 return true; | |
| 101 } | |
| 102 | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
103 template<typename T> |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
104 struct CFDeconstructor { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
105 using pointer = T; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
106 void operator()(pointer t) const { ::CFRelease(t); }; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
107 }; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
108 |
| 0 | 109 static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) { |
| 110 CGRect bounds = {0}; | |
| 111 { | |
| 112 const CGWindowID wids[1] = {wid}; | |
| 113 CFPtr<CFArrayRef> arr(CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL)); | |
| 114 CFPtr<CFArrayRef> dicts(CGWindowListCreateDescriptionFromArray(arr)); | |
| 115 | |
| 116 if (!dicts.get() || CFArrayGetCount(dicts.get()) < 1) | |
| 117 return false; | |
| 118 | |
| 119 CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(dicts, 0)); | |
| 120 if (!dict) | |
| 121 return false; | |
| 122 | |
| 123 CFDictionaryRef bounds_dict = nullptr; | |
| 124 if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) || | |
| 125 !bounds_dict) | |
| 126 return false; | |
| 127 | |
| 128 if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) | |
| 129 return false; | |
| 130 } | |
| 131 | |
| 132 /* now we can actually do stuff */ | |
| 133 AXUIElementRef axapp = AXUIElementCreateApplication(pid); | |
| 134 CFPtr<CFArrayRef> windows; | |
| 135 { | |
| 136 CFArrayRef ref; | |
| 137 if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, reinterpret_cast<CFTypeRef*>(&ref)) != | |
| 138 kAXErrorSuccess) || | |
| 139 !windows) | |
| 140 return false; | |
| 141 | |
| 142 windows.reset(ref); | |
| 143 } | |
| 144 | |
| 145 const CFIndex count = CFArrayGetCount(windows.get()); | |
| 146 for (CFIndex i = 0; i < count; i++) { | |
| 147 const AXUIElementRef window = reinterpret_cast<AXUIElementRef>(CFArrayGetValueAtIndex(windows.get(), i)); | |
| 148 | |
| 149 /* does this leak memory? probably. */ | |
| 150 AXValueRef val; | |
| 151 if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, reinterpret_cast<CFTypeRef*>(&val)) == | |
| 152 kAXErrorSuccess) { | |
| 153 CGPoint point; | |
| 154 if (!AXValueGetValue(val, kAXValueTypeCGPoint, reinterpret_cast<CFTypeRef>(&point)) || | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
155 (point.x != bounds.origin.x || point.y != bounds.origin.y)) { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
156 CFRelease(val); |
| 0 | 157 continue; |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
158 } |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
159 } else { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
160 CFRelease(val); |
| 0 | 161 continue; |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
162 } |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
163 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
164 CFRelease(val); |
| 0 | 165 |
| 166 if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, reinterpret_cast<CFTypeRef*>(&val)) == | |
| 167 kAXErrorSuccess) { | |
| 168 CGSize size; | |
| 169 if (!AXValueGetValue(val, kAXValueTypeCGSize, reinterpret_cast<CFTypeRef>(&size)) || | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
170 (size.width != bounds.size.width || size.height != bounds.size.height)) { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
171 CFRelease(val); |
| 0 | 172 continue; |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
173 } |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
174 } else { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
175 CFRelease(val); |
| 0 | 176 continue; |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
177 } |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
178 |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
179 CFRelease(val); |
| 0 | 180 |
| 181 CFStringRef title; | |
| 182 if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, reinterpret_cast<CFTypeRef*>(&title)) == | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
183 kAXErrorSuccess) { |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
184 bool success = StringFromCFString(title, result); |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
185 CFRelease(title); |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
186 return success; |
|
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
187 } |
| 0 | 188 } |
| 189 | |
| 190 return false; | |
| 191 } | |
| 192 | |
| 193 static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) { | |
| 194 /* try using CoreGraphics (only usable on old versions of OS X) */ | |
| 195 if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) { | |
| 196 CFPtr<CFStringRef> title; | |
| 197 { | |
| 198 CFStringRef t = nullptr; | |
| 199 CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &t); | |
| 200 title.reset(t); | |
| 201 } | |
| 202 | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
203 if (title && CFStringGetLength(title.get()) && StringFromCFString(title.get(), result)) |
| 0 | 204 return true; |
| 205 } | |
| 206 | |
| 207 /* then try linking to a window using the accessibility API */ | |
| 208 return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false; | |
| 209 } | |
| 210 | |
| 211 static bool GetProcessBundleIdentifierNew(pid_t pid, std::string& result) { | |
| 212 /* 10.6 and higher */ | |
| 213 const id app = | |
| 214 cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid); | |
| 215 if (!app) | |
| 216 return false; | |
| 217 | |
| 218 CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier"))); | |
| 219 if (!bundle_id) | |
| 220 return false; | |
| 221 | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
222 return StringFromCFString(bundle_id, result); |
| 0 | 223 } |
| 224 | |
| 225 static bool GetProcessBundleIdentifierOld(pid_t pid, std::string& result) { | |
| 226 /* OS X 10.2; deprecated in 10.9 */ | |
| 227 ProcessSerialNumber psn; | |
| 228 if (GetProcessForPID(pid, &psn)) | |
| 229 return false; | |
| 230 | |
| 231 CFPtr<CFDictionaryRef> info = ProcessInformationCopyDictionary(psn, kProcessDictionaryIncludeAllInformationMask); | |
| 232 if (!info) | |
| 233 return false; | |
| 234 | |
| 235 CFStringRef value = reinterpret_cast<CFStringRef>(CFDictionaryGetValue(dict, CFSTR("CFBundleIdentifier"))); | |
| 236 if (!value) | |
| 237 return false; | |
| 238 | |
|
4
cce3a81b03bf
dep/animone: make osx code a bit less hacky
Paper <paper@paper.us.eu.org>
parents:
0
diff
changeset
|
239 return StringFromCFString(value, result); |
| 0 | 240 } |
| 241 | |
| 242 static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) { | |
| 243 /* The Bundle ID is essentially OS X's solution to Windows' | |
| 244 * "class name"; theoretically, it should be different for | |
| 245 * each program, although it requires an app bundle. | |
| 246 */ | |
| 247 if (GetProcessBundleIdentifierNew(pid, result)) | |
| 248 return true; | |
| 249 | |
| 250 return GetProcessBundleIdentifierOld(); | |
| 251 } | |
| 252 | |
| 253 bool EnumerateWindows(window_proc_t window_proc) { | |
| 254 if (!window_proc) | |
| 255 return false; | |
| 256 | |
| 257 const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); | |
| 258 if (!windows) | |
| 259 return false; | |
| 260 | |
| 261 const CFIndex count = CFArrayGetCount(windows); | |
| 262 for (CFIndex i = 0; i < count; i++) { | |
| 263 CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windows, i)); | |
| 264 if (!window) | |
| 265 continue; | |
| 266 | |
| 267 Process proc; | |
| 268 { | |
| 269 CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid); | |
| 270 if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.name)) | |
| 271 osx::util::GetProcessName(proc.pid, proc.name); | |
| 272 } | |
| 273 | |
| 274 Window win; | |
| 275 { | |
| 276 CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id); | |
| 277 | |
| 278 if (!GetProcessBundleIdentifier(proc.pid, win.class_name)) | |
| 279 // Fallback to the Quartz window name, which is unlikely to be filled, but it | |
| 280 // *could* be. | |
| 281 CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name); | |
| 282 | |
| 283 GetWindowTitle(win.id, proc.pid, win.text); | |
| 284 } | |
| 285 | |
| 286 if (!window_proc(proc, win)) { | |
| 287 CFRelease(windows); | |
| 288 return false; | |
| 289 } | |
| 290 } | |
| 291 | |
| 292 CFRelease(windows); | |
| 293 | |
| 294 return true; | |
| 295 } | |
| 296 | |
| 297 } // namespace animone::internal::quartz |
