|
19 | 19 | // platform includes |
20 | 20 | #include <arpa/inet.h> |
21 | 21 | #include <dlfcn.h> |
| 22 | +#include <ApplicationServices/ApplicationServices.h> |
| 23 | +#include <AppKit/AppKit.h> |
22 | 24 | #include <Foundation/Foundation.h> |
23 | 25 | #include <mach-o/dyld.h> |
24 | 26 | #include <net/if_dl.h> |
|
32 | 34 |
|
33 | 35 | // local includes |
34 | 36 | #include "misc.h" |
| 37 | +#include "src/utility.h" |
35 | 38 | #include "src/entry_handler.h" |
36 | 39 | #include "src/logging.h" |
37 | 40 | #include "src/platform/common.h" |
|
69 | 72 | /** |
70 | 73 | * @brief Check whether screen capture allowed. |
71 | 74 | */ |
| 75 | + namespace { |
| 76 | + /// Reads an Accessibility attribute, returning nothing rather than an error code. |
| 77 | + CFTypeRef copy_attribute(AXUIElementRef element, CFStringRef name) { |
| 78 | + CFTypeRef value = nullptr; |
| 79 | + if (AXUIElementCopyAttributeValue(element, name, &value) != kAXErrorSuccess) { |
| 80 | + return nullptr; |
| 81 | + } |
| 82 | + return value; |
| 83 | + } |
| 84 | + } // namespace |
| 85 | + |
| 86 | + std::optional<std::array<double, 4>> focused_caret() { |
| 87 | + if (!AXIsProcessTrusted()) { |
| 88 | + return std::nullopt; |
| 89 | + } |
| 90 | + |
| 91 | + NSRunningApplication *front = [[NSWorkspace sharedWorkspace] frontmostApplication]; |
| 92 | + if (front == nil) { |
| 93 | + return std::nullopt; |
| 94 | + } |
| 95 | + |
| 96 | + // Ask the application, not the system. AXUIElementCreateSystemWide() with |
| 97 | + // kAXFocusedUIElementAttribute comes back empty for applications that answer perfectly well |
| 98 | + // when asked directly — iTerm2 among them, which is the case this exists for. |
| 99 | + AXUIElementRef app = AXUIElementCreateApplication(front.processIdentifier); |
| 100 | + if (!app) { |
| 101 | + return std::nullopt; |
| 102 | + } |
| 103 | + // A busy or hung application must not stall the request that asked for this. |
| 104 | + AXUIElementSetMessagingTimeout(app, 0.1f); |
| 105 | + |
| 106 | + const auto release_app = util::fail_guard([app]() { |
| 107 | + CFRelease(app); |
| 108 | + }); |
| 109 | + |
| 110 | + CFTypeRef focused = copy_attribute(app, kAXFocusedUIElementAttribute); |
| 111 | + if (!focused) { |
| 112 | + return std::nullopt; |
| 113 | + } |
| 114 | + const auto release_focused = util::fail_guard([focused]() { |
| 115 | + CFRelease(focused); |
| 116 | + }); |
| 117 | + |
| 118 | + CFTypeRef range = copy_attribute(static_cast<AXUIElementRef>(focused), kAXSelectedTextRangeAttribute); |
| 119 | + if (!range) { |
| 120 | + return std::nullopt; |
| 121 | + } |
| 122 | + const auto release_range = util::fail_guard([range]() { |
| 123 | + CFRelease(range); |
| 124 | + }); |
| 125 | + |
| 126 | + CFTypeRef bounds = nullptr; |
| 127 | + if (AXUIElementCopyParameterizedAttributeValue( |
| 128 | + static_cast<AXUIElementRef>(focused), |
| 129 | + kAXBoundsForRangeParameterizedAttribute, |
| 130 | + range, |
| 131 | + &bounds |
| 132 | + ) != kAXErrorSuccess || |
| 133 | + !bounds) { |
| 134 | + return std::nullopt; |
| 135 | + } |
| 136 | + const auto release_bounds = util::fail_guard([bounds]() { |
| 137 | + CFRelease(bounds); |
| 138 | + }); |
| 139 | + |
| 140 | + CGRect caret = CGRectZero; |
| 141 | + if (!AXValueGetValue(static_cast<AXValueRef>(bounds), kAXValueTypeCGRect, &caret)) { |
| 142 | + return std::nullopt; |
| 143 | + } |
| 144 | + // An element that does not really have an insertion point answers with an empty rect at the |
| 145 | + // origin rather than with an error. |
| 146 | + if (caret.size.width == 0 && caret.size.height == 0) { |
| 147 | + return std::nullopt; |
| 148 | + } |
| 149 | + |
| 150 | + // Accessibility works in the coordinates of the whole desktop arrangement, which on a second |
| 151 | + // display can be negative or larger than the streamed display. Only a caret on the display |
| 152 | + // being streamed means anything to the client. |
| 153 | + const CGRect display = CGDisplayBounds(CGMainDisplayID()); |
| 154 | + const CGPoint anchor = CGPointMake(CGRectGetMidX(caret), CGRectGetMidY(caret)); |
| 155 | + if (!CGRectContainsPoint(display, anchor)) { |
| 156 | + return std::nullopt; |
| 157 | + } |
| 158 | + |
| 159 | + return std::array<double, 4> { |
| 160 | + (caret.origin.x - display.origin.x) / display.size.width, |
| 161 | + (caret.origin.y - display.origin.y) / display.size.height, |
| 162 | + caret.size.width / display.size.width, |
| 163 | + caret.size.height / display.size.height |
| 164 | + }; |
| 165 | + } |
| 166 | + |
| 167 | + std::optional<std::array<double, 2>> pointer_location() { |
| 168 | + // A fresh event every time rather than a reused one, as get_mouse_loc() does: the location |
| 169 | + // on a reused event is whatever it was when the event was made. |
| 170 | + CGEventRef snapshot = CGEventCreate(nullptr); |
| 171 | + if (!snapshot) { |
| 172 | + return std::nullopt; |
| 173 | + } |
| 174 | + const CGPoint location = CGEventGetLocation(snapshot); |
| 175 | + CFRelease(snapshot); |
| 176 | + |
| 177 | + const CGRect display = CGDisplayBounds(CGMainDisplayID()); |
| 178 | + if (!CGRectContainsPoint(display, location)) { |
| 179 | + return std::nullopt; |
| 180 | + } |
| 181 | + |
| 182 | + return std::array<double, 2> { |
| 183 | + (location.x - display.origin.x) / display.size.width, |
| 184 | + (location.y - display.origin.y) / display.size.height |
| 185 | + }; |
| 186 | + } |
| 187 | + |
72 | 188 | bool is_screen_capture_allowed() { |
73 | 189 | return screen_capture_allowed; |
74 | 190 | } |
|
0 commit comments