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
|