comparison dep/animone/src/win/quartz.cc @ 258:862d0d8619f6

*: HUUUGE changes animia has been renamed to animone, so instead of thinking of a health condition, you think of a beautiful flower :) I've also edited some of the code for animone, but I have no idea if it even works or not because I don't have a mac or windows machine lying around. whoops! ... anyway, all of the changes divergent from Anisthesia are now licensed under BSD. it's possible that I could even rewrite most of the code to where I don't even have to keep the MIT license, but that's thinking too far into the future I've been slacking off on implementing the anime seasons page, mostly out of laziness. I think I'd have to create another db file specifically for the seasons anyway, this code is being pushed *primarily* because the hard drive it's on is failing! yay :)
author Paper <paper@paper.us.eu.org>
date Mon, 01 Apr 2024 02:43:44 -0400
parents
children 382b50754fe4
comparison
equal deleted inserted replaced
257:699a20c57dc8 258:862d0d8619f6
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