comparison dep/animia/src/win/quartz.cc @ 237:a7d0d543b334

*: make OS X builds succeed new script: deploy_build.sh, creates the app bundle
author Paper <paper@paper.us.eu.org>
date Fri, 19 Jan 2024 11:14:44 -0500
parents bc1ae1810855
children
comparison
equal deleted inserted replaced
236:4d461ef7d424 237:a7d0d543b334
1 /* 1 /*
2 * win/quartz.cc: support for macOS (the Quartz Compositor) 2 * win/quartz.cc: support for macOS (the Quartz Compositor)
3 * 3 *
4 * This file does not require an Objective-C++ compiler, 4 * This file does not require an Objective-C++ compiler,
5 * but it *does* require an Objective-C runtime and linking 5 * but it *does* require an Objective-C runtime.
6 * with AppKit in order to receive proper window titles.
7 */ 6 */
8 #include "animia/win/quartz.h" 7 #include "animia/win/quartz.h"
9 #include "animia/util/osx.h" 8 #include "animia/util/osx.h"
10 #include "animia.h" 9 #include "animia.h"
11 10
12 #include <objc/runtime.h> 11 #include <objc/runtime.h>
13 #include <objc/message.h> 12 #include <objc/message.h>
14 13
15 #include <CoreFoundation/CoreFoundation.h> 14 #include <CoreFoundation/CoreFoundation.h>
16 #include <CoreGraphics/CoreGraphics.h> 15 #include <CoreGraphics/CoreGraphics.h>
16 #include <ApplicationServices/ApplicationServices.h>
17 17
18 namespace animia::internal::quartz { 18 namespace animia::internal::quartz {
19 19
20 /* all of these LaunchServices things use *internal functions* that are subject
21 * to change. Granted, it's not very likely that these will change very much
22 * because I'm fairly sure Apple uses them lots in their own internal code.
23 */
24 #if __LP64__
25 typedef long NSInteger;
26 #else
27 typedef int NSInteger;
28 #endif
29 typedef int CGSConnection;
30
31 typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void);
32 typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*);
33
34 static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr;
35 static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr;
36
37 static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics");
38
39 /* Objective-C */
20 typedef id (*object_message_send)(id, SEL, ...); 40 typedef id (*object_message_send)(id, SEL, ...);
21 typedef id (*class_message_send)(Class, SEL, ...); 41 typedef id (*class_message_send)(Class, SEL, ...);
22 42
23 static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend); 43 static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend);
24 static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend); 44 static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend);
25 45
26 static bool GetWindowTitle(unsigned int wid, std::string& result) { 46 static bool GetCoreGraphicsPrivateSymbols() {
27 // NSApplication* app = [NSApplication sharedApplication]; 47 CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID);
28 const id app = cls_send(objc_getClass("NSApplication"), sel_getUid("sharedApplication")); 48 if (!core_graphics_bundle)
29 49 return false;
30 // NSWindow* window = [app windowWithWindowNumber: wid]; 50
31 const id window = obj_send(app, sel_getUid("windowWithWindowNumber:"), wid); 51 CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName(core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread"));
32 if (!window) 52 if (!CGSDefaultConnectionForThread)
33 return false; 53 return false;
34 54
35 // NSString* title = [window title]; 55 CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName(core_graphics_bundle, CFSTR("CGSCopyWindowProperty"));
36 const CFStringRef title = reinterpret_cast<CFStringRef>(obj_send(window, sel_getUid("title"))); 56 if (!CGSCopyWindowProperty)
37 if (!title) 57 return false;
38 return false; 58
39 59 return true;
40 // return [title UTF8String]; 60 }
41 return osx::util::StringFromCFString(title, result); 61
62 template<typename T>
63 static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) {
64 CFTypeRef data = nullptr;
65 if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data)
66 return false;
67
68 if constexpr (std::is_arithmetic<T>::value)
69 osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out);
70 else if constexpr (std::is_same<T, std::string>::value)
71 osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out);
72 else
73 return false;
74
75 return true;
76 }
77
78 static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) {
79 CGRect bounds = {0};
80 {
81 const CGWindowID wids[1] = {wid};
82 CFArrayRef arr = CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL);
83
84 CFArrayRef dicts = CGWindowListCreateDescriptionFromArray(arr);
85
86 CFRelease(arr);
87
88 if (!dicts || CFArrayGetCount(dicts) < 1)
89 return false;
90
91 CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(dicts, 0);
92 if (!dict) {
93 CFRelease(dicts);
94 return false;
95 }
96
97 CFDictionaryRef bounds_dict = nullptr;
98 if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) || !bounds_dict) {
99 CFRelease(dicts);
100 return false;
101 }
102
103 if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) {
104 CFRelease(dicts);
105 return false;
106 }
107
108 CFRelease(dicts);
109 }
110
111 /* now we can actually do stuff */
112 AXUIElementRef axapp = AXUIElementCreateApplication(pid);
113 CFArrayRef windows;
114 if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, (CFTypeRef*)&windows) != kAXErrorSuccess) || !windows)
115 return false;
116
117 const CFIndex count = CFArrayGetCount(windows);
118 for (CFIndex i = 0; i < count; i++) {
119 const AXUIElementRef window = (AXUIElementRef)CFArrayGetValueAtIndex(windows, i);
120
121 AXValueRef val;
122 if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, (CFTypeRef*)&val) == kAXErrorSuccess) {
123 CGPoint point;
124 if (!AXValueGetValue(val, kAXValueTypeCGPoint, (void*)&point) || (point.x != bounds.origin.x || point.y != bounds.origin.y))
125 continue;
126 } else continue;
127
128 if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, (CFTypeRef*)&val) == kAXErrorSuccess) {
129 CGSize size;
130 if (!AXValueGetValue(val, kAXValueTypeCGSize, (void*)&size) || (size.width != bounds.size.width || size.height != bounds.size.height))
131 continue;
132 } else continue;
133
134 CFStringRef title;
135 if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, (CFTypeRef*)&title) == kAXErrorSuccess) {
136 CFRelease(windows);
137 return osx::util::StringFromCFString(title, result);
138 }
139 }
140
141 CFRelease(windows);
142
143 return false;
144 }
145
146 static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) {
147 /* private internal OS X functions */
148 if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) {
149 CFStringRef title = nullptr;
150
151 CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &title);
152 if (title && CFStringGetLength(title) && osx::util::StringFromCFString(title, result))
153 return true;
154 }
155
156 /* don't attempt to use accessibility if we aren't trusted */
157 return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false;
42 } 158 }
43 159
44 static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) { 160 static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) {
45 /* The Bundle ID is essentially OS X's solution to Windows' 161 /* The Bundle ID is essentially OS X's solution to Windows'
46 * "class name"; theoretically, it should be different for 162 * "class name"; theoretically, it should be different for
47 * each program, although it requires an app bundle. 163 * each program, although it requires an app bundle.
48 */ 164 */
49
50 // NSRunningApplication* app = [NSRunningApplication runningApplicationWithProcessIdentifier: pid];
51 const id app = cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid); 165 const id app = cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid);
52 if (!app) 166 if (!app)
53 return false; 167 return false;
54 168
55 // NSString* bundle_id = [app bundleIdentifier]; 169 CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
56 const CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
57 if (!bundle_id) 170 if (!bundle_id)
58 return false; 171 return false;
59 172
60 // return [bundle_id UTF8String]; 173 result = osx::util::StringFromCFString(bundle_id, result);
61 return osx::util::StringFromCFString(bundle_id, result);
62 }
63
64 template<typename T>
65 static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) {
66 CFTypeRef data = nullptr;
67 if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data)
68 return false;
69
70 if constexpr (std::is_arithmetic<T>::value)
71 osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out);
72 else if constexpr (std::is_same<T, std::string>::value)
73 osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out);
74 else
75 return false;
76
77 return true; 174 return true;
78 } 175 }
79 176
80 bool EnumerateWindows(window_proc_t window_proc) { 177 bool EnumerateWindows(window_proc_t window_proc) {
81 if (!window_proc) 178 if (!window_proc)
82 return false; 179 return false;
83 180
84 const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID); 181 const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
85 if (!windows) 182 if (!windows)
86 return false; 183 return false;
87 184
88 const CFIndex count = CFArrayGetCount(windows); 185 const CFIndex count = CFArrayGetCount(windows);
89 for (CFIndex i = 0; i < count; i++) { 186 for (CFIndex i = 0; i < count; i++) {
100 197
101 Window win; 198 Window win;
102 { 199 {
103 CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id); 200 CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id);
104 201
105 if (!GetProcessBundleIdentifier(proc.pid, win.class_name)) { 202 if (!GetProcessBundleIdentifier(proc.pid, win.class_name))
106 // Fallback to the Quartz window name, which is unlikely to be filled, but it 203 // Fallback to the Quartz window name, which is unlikely to be filled, but it
107 // *could* be. 204 // *could* be.
108 CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name); 205 CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name);
109 } 206
110 207 GetWindowTitle(win.id, proc.pid, win.text);
111 GetWindowTitle(win.id, win.text);
112 } 208 }
113 209
114 if (!window_proc(proc, win)) { 210 if (!window_proc(proc, win)) {
115 CFRelease(windows); 211 CFRelease(windows);
116 return false; 212 return false;