node-mac-recorder 2.20.2 → 2.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cursor_tracker.mm +317 -320
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
#import <Accessibility/Accessibility.h>
|
|
8
8
|
#import <dispatch/dispatch.h>
|
|
9
9
|
|
|
10
|
+
#ifndef kAXHitTestParameterizedAttribute
|
|
11
|
+
#define kAXHitTestParameterizedAttribute CFSTR("AXHitTest")
|
|
12
|
+
#endif
|
|
13
|
+
|
|
10
14
|
// Global state for cursor tracking
|
|
11
15
|
static bool g_isCursorTracking = false;
|
|
12
16
|
static CFMachPortRef g_eventTap = NULL;
|
|
@@ -49,309 +53,330 @@ static NSString* CopyAndReleaseCFString(CFStringRef value) {
|
|
|
49
53
|
return result;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
static inline BOOL StringEqualsAny(NSString *value, NSArray<NSString *> *candidates) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return NO;
|
|
59
|
+
}
|
|
60
|
+
for (NSString *candidate in candidates) {
|
|
61
|
+
if ([value isEqualToString:candidate]) {
|
|
62
|
+
return YES;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return NO;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static NSString* CopyAttributeString(AXUIElementRef element, CFStringRef attribute) {
|
|
69
|
+
if (!element || !attribute) {
|
|
70
|
+
return nil;
|
|
71
|
+
}
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
CFStringRef value = NULL;
|
|
74
|
+
AXError error = AXUIElementCopyAttributeValue(element, attribute, (CFTypeRef *)&value);
|
|
75
|
+
if (error == kAXErrorSuccess && value) {
|
|
76
|
+
return CopyAndReleaseCFString(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value) {
|
|
80
|
+
CFRelease(value);
|
|
81
|
+
}
|
|
82
|
+
return nil;
|
|
60
83
|
}
|
|
61
84
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// ACCESSIBILITY API BASED CURSOR DETECTION
|
|
67
|
-
// Determine cursor type based on the UI element under the cursor
|
|
85
|
+
static BOOL CopyAttributeBoolean(AXUIElementRef element, CFStringRef attribute, BOOL *outValue) {
|
|
86
|
+
if (!element || !attribute || !outValue) {
|
|
87
|
+
return NO;
|
|
88
|
+
}
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
CFTypeRef rawValue = NULL;
|
|
91
|
+
AXError error = AXUIElementCopyAttributeValue(element, attribute, &rawValue);
|
|
92
|
+
if (error != kAXErrorSuccess || !rawValue) {
|
|
93
|
+
if (rawValue) {
|
|
94
|
+
CFRelease(rawValue);
|
|
95
|
+
}
|
|
96
|
+
return NO;
|
|
97
|
+
}
|
|
72
98
|
|
|
73
|
-
|
|
99
|
+
BOOL result = NO;
|
|
100
|
+
if (CFGetTypeID(rawValue) == CFBooleanGetTypeID()) {
|
|
101
|
+
result = CFBooleanGetValue((CFBooleanRef)rawValue);
|
|
102
|
+
}
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (error == kAXErrorSuccess && role) {
|
|
80
|
-
elementRole = CopyAndReleaseCFString(role);
|
|
81
|
-
}
|
|
104
|
+
CFRelease(rawValue);
|
|
105
|
+
*outValue = result;
|
|
106
|
+
return YES;
|
|
107
|
+
}
|
|
82
108
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
static BOOL ElementHasAction(AXUIElementRef element, CFStringRef action) {
|
|
110
|
+
if (!element || !action) {
|
|
111
|
+
return NO;
|
|
112
|
+
}
|
|
86
113
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
cursorType = @"text";
|
|
93
|
-
}
|
|
94
|
-
// POINTER CURSORS (clickable elements)
|
|
95
|
-
else if ([elementRole isEqualToString:@"AXLink"] ||
|
|
96
|
-
[elementRole isEqualToString:@"AXButton"] ||
|
|
97
|
-
[elementRole isEqualToString:@"AXMenuItem"] ||
|
|
98
|
-
[elementRole isEqualToString:@"AXRadioButton"] ||
|
|
99
|
-
[elementRole isEqualToString:@"AXCheckBox"] ||
|
|
100
|
-
[elementRole isEqualToString:@"AXPopUpButton"] ||
|
|
101
|
-
[elementRole isEqualToString:@"AXTab"]) {
|
|
102
|
-
cursorType = @"pointer";
|
|
103
|
-
}
|
|
104
|
-
// GRAB CURSORS (draggable elements)
|
|
105
|
-
else if ([elementRole isEqualToString:@"AXImage"] ||
|
|
106
|
-
[elementRole isEqualToString:@"AXGroup"]) {
|
|
107
|
-
// Check if element is draggable
|
|
108
|
-
CFBooleanRef draggable = NULL;
|
|
109
|
-
error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
|
|
110
|
-
if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
|
|
111
|
-
cursorType = @"grab";
|
|
112
|
-
} else {
|
|
113
|
-
cursorType = @"default";
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// PROGRESS CURSORS (loading/busy elements)
|
|
117
|
-
else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
|
|
118
|
-
[elementRole isEqualToString:@"AXBusyIndicator"]) {
|
|
119
|
-
cursorType = @"progress";
|
|
120
|
-
}
|
|
121
|
-
// HELP CURSORS (help buttons/tooltips)
|
|
122
|
-
else if ([elementRole isEqualToString:@"AXHelpTag"] ||
|
|
123
|
-
[elementRole isEqualToString:@"AXTooltip"]) {
|
|
124
|
-
cursorType = @"help";
|
|
125
|
-
}
|
|
126
|
-
// RESIZE CURSORS - sadece AXSplitter için
|
|
127
|
-
else if ([elementRole isEqualToString:@"AXSplitter"]) {
|
|
128
|
-
// Get splitter orientation to determine resize direction
|
|
129
|
-
CFStringRef orientation = NULL;
|
|
130
|
-
error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
|
|
131
|
-
if (error == kAXErrorSuccess && orientation) {
|
|
132
|
-
NSString *orientationStr = CopyAndReleaseCFString(orientation);
|
|
133
|
-
if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
|
|
134
|
-
cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
|
|
135
|
-
} else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
|
|
136
|
-
cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
|
|
137
|
-
} else {
|
|
138
|
-
cursorType = @"default"; // Bilinmeyen orientation
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
cursorType = @"default"; // Orientation alınamazsa default
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// SCROLL CURSORS - hep default olsun, all-scroll görünmesin
|
|
145
|
-
else if ([elementRole isEqualToString:@"AXScrollBar"]) {
|
|
146
|
-
cursorType = @"default"; // ScrollBar'lar için de default
|
|
147
|
-
}
|
|
148
|
-
// AXScrollArea - hep default
|
|
149
|
-
else if ([elementRole isEqualToString:@"AXScrollArea"]) {
|
|
150
|
-
cursorType = @"default"; // ScrollArea her zaman default
|
|
151
|
-
}
|
|
152
|
-
// CROSSHAIR CURSORS (drawing/selection tools)
|
|
153
|
-
else if ([elementRole isEqualToString:@"AXCanvas"] ||
|
|
154
|
-
[elementRole isEqualToString:@"AXDrawingArea"]) {
|
|
155
|
-
cursorType = @"crosshair";
|
|
156
|
-
}
|
|
157
|
-
// ZOOM CURSORS (zoom controls)
|
|
158
|
-
else if ([elementRole isEqualToString:@"AXZoomButton"]) {
|
|
159
|
-
cursorType = @"zoom-in";
|
|
160
|
-
}
|
|
161
|
-
// NOT-ALLOWED CURSORS (disabled elements)
|
|
162
|
-
else if ([elementRole isEqualToString:@"AXStaticText"] ||
|
|
163
|
-
[elementRole isEqualToString:@"AXGroup"]) {
|
|
164
|
-
// Check if element is disabled/readonly
|
|
165
|
-
CFBooleanRef enabled = NULL;
|
|
166
|
-
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
|
|
167
|
-
if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
|
|
168
|
-
cursorType = @"not-allowed";
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// WINDOW BORDER RESIZE - sadece pencere kenarlarında
|
|
172
|
-
else if ([elementRole isEqualToString:@"AXWindow"]) {
|
|
173
|
-
// Check window attributes to see if it's resizable
|
|
174
|
-
CFBooleanRef resizable = NULL;
|
|
175
|
-
AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
|
|
176
|
-
|
|
177
|
-
// Sadece resize edilebilir pencereler için cursor değişimi
|
|
178
|
-
if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
|
|
179
|
-
CFTypeRef position = NULL;
|
|
180
|
-
CFTypeRef size = NULL;
|
|
181
|
-
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
|
|
182
|
-
AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
|
|
183
|
-
|
|
184
|
-
if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
|
|
185
|
-
CGPoint windowPos;
|
|
186
|
-
CGSize windowSize;
|
|
187
|
-
AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
|
|
188
|
-
AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
|
|
189
|
-
|
|
190
|
-
CGFloat x = cursorPos.x - windowPos.x;
|
|
191
|
-
CGFloat y = cursorPos.y - windowPos.y;
|
|
192
|
-
CGFloat w = windowSize.width;
|
|
193
|
-
CGFloat h = windowSize.height;
|
|
194
|
-
CGFloat edge = 3.0; // Daha küçük edge detection (3px)
|
|
195
|
-
|
|
196
|
-
// Sadece çok kenar köşelerde resize cursor'ı göster
|
|
197
|
-
BOOL isOnBorder = NO;
|
|
198
|
-
|
|
199
|
-
// Corner resize detection - çok dar alanda, doğru açılar
|
|
200
|
-
if (x <= edge && y <= edge) {
|
|
201
|
-
cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
|
|
202
|
-
isOnBorder = YES;
|
|
203
|
-
}
|
|
204
|
-
else if (x >= w-edge && y <= edge) {
|
|
205
|
-
cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
|
|
206
|
-
isOnBorder = YES;
|
|
207
|
-
}
|
|
208
|
-
else if (x <= edge && y >= h-edge) {
|
|
209
|
-
cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
|
|
210
|
-
isOnBorder = YES;
|
|
211
|
-
}
|
|
212
|
-
else if (x >= w-edge && y >= h-edge) {
|
|
213
|
-
cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
|
|
214
|
-
isOnBorder = YES;
|
|
215
|
-
}
|
|
216
|
-
// Edge resize detection - sadece çok kenarlarda
|
|
217
|
-
else if (x <= edge && y > edge && y < h-edge) {
|
|
218
|
-
cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
|
|
219
|
-
isOnBorder = YES;
|
|
220
|
-
}
|
|
221
|
-
else if (x >= w-edge && y > edge && y < h-edge) {
|
|
222
|
-
cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
|
|
223
|
-
isOnBorder = YES;
|
|
224
|
-
}
|
|
225
|
-
else if (y <= edge && x > edge && x < w-edge) {
|
|
226
|
-
cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
|
|
227
|
-
isOnBorder = YES;
|
|
228
|
-
}
|
|
229
|
-
else if (y >= h-edge && x > edge && x < w-edge) {
|
|
230
|
-
cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
|
|
231
|
-
isOnBorder = YES;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Eğer border'da değilse default
|
|
235
|
-
if (!isOnBorder) {
|
|
236
|
-
cursorType = @"default";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (position) CFRelease(position);
|
|
240
|
-
if (size) CFRelease(size);
|
|
241
|
-
} else {
|
|
242
|
-
cursorType = @"default";
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
cursorType = @"default";
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// HER DURUM İÇİN DEFAULT FALLBACK
|
|
249
|
-
else {
|
|
250
|
-
// Bilinmeyen elementler için her zaman default
|
|
251
|
-
cursorType = @"default";
|
|
252
|
-
}
|
|
114
|
+
CFArrayRef actions = NULL;
|
|
115
|
+
AXError error = AXUIElementCopyActionNames(element, &actions);
|
|
116
|
+
if (error != kAXErrorSuccess || !actions) {
|
|
117
|
+
return NO;
|
|
118
|
+
}
|
|
253
119
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
cursorType = @"pointer";
|
|
267
|
-
}
|
|
268
|
-
// Copy/alias subroles - sadece bu durumlar için override
|
|
269
|
-
else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
|
|
270
|
-
[elementSubrole isEqualToString:@"AXDropTarget"]) {
|
|
271
|
-
cursorType = @"copy";
|
|
272
|
-
}
|
|
273
|
-
// Alias/shortcut subroles
|
|
274
|
-
else if ([elementSubrole isEqualToString:@"AXAlias"] ||
|
|
275
|
-
[elementSubrole isEqualToString:@"AXShortcut"]) {
|
|
276
|
-
cursorType = @"alias";
|
|
277
|
-
}
|
|
278
|
-
// Grabbing state (being dragged) - sadece gerçek drag sırasında
|
|
279
|
-
else if ([elementSubrole isEqualToString:@"AXDragging"] ||
|
|
280
|
-
[elementSubrole isEqualToString:@"AXMoving"]) {
|
|
281
|
-
cursorType = @"grabbing";
|
|
282
|
-
}
|
|
283
|
-
// Zoom controls - sadece spesifik zoom butonları için
|
|
284
|
-
else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
|
|
285
|
-
cursorType = @"zoom-in";
|
|
286
|
-
}
|
|
287
|
-
else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
|
|
288
|
-
cursorType = @"zoom-out";
|
|
289
|
-
}
|
|
290
|
-
// Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
|
|
291
|
-
}
|
|
120
|
+
BOOL hasAction = NO;
|
|
121
|
+
CFIndex count = CFArrayGetCount(actions);
|
|
122
|
+
for (CFIndex i = 0; i < count; i++) {
|
|
123
|
+
CFStringRef candidate = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
|
|
124
|
+
if (CFStringCompare(candidate, action, 0) == kCFCompareEqualTo) {
|
|
125
|
+
hasAction = YES;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
CFRelease(actions);
|
|
130
|
+
return hasAction;
|
|
131
|
+
}
|
|
292
132
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (roleDescription) {
|
|
298
|
-
NSString *roleDescriptionLower = [roleDescription lowercaseString];
|
|
299
|
-
if ([roleDescriptionLower containsString:@"text"] ||
|
|
300
|
-
[roleDescriptionLower containsString:@"editor"] ||
|
|
301
|
-
[roleDescriptionLower containsString:@"code"] ||
|
|
302
|
-
[roleDescriptionLower containsString:@"document"]) {
|
|
303
|
-
cursorType = @"text";
|
|
304
|
-
} else if ([roleDescriptionLower containsString:@"button"] ||
|
|
305
|
-
[roleDescriptionLower containsString:@"link"] ||
|
|
306
|
-
[roleDescriptionLower containsString:@"tab"]) {
|
|
307
|
-
cursorType = @"pointer";
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
133
|
+
static BOOL PointInsideElementFrame(AXUIElementRef element, CGPoint point) {
|
|
134
|
+
if (!element) {
|
|
135
|
+
return NO;
|
|
136
|
+
}
|
|
311
137
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
isEditable && CFBooleanGetValue(isEditable)) {
|
|
315
|
-
cursorType = @"text";
|
|
316
|
-
}
|
|
138
|
+
AXValueRef positionValue = NULL;
|
|
139
|
+
AXValueRef sizeValue = NULL;
|
|
317
140
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
supportsTextSelection && CFBooleanGetValue(supportsTextSelection)) {
|
|
321
|
-
cursorType = @"text";
|
|
322
|
-
}
|
|
141
|
+
AXError positionError = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, (CFTypeRef *)&positionValue);
|
|
142
|
+
AXError sizeError = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
|
|
323
143
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
cursorType = @"text";
|
|
330
|
-
}
|
|
331
|
-
CFRelease(valueAttribute);
|
|
332
|
-
}
|
|
144
|
+
if (positionError != kAXErrorSuccess || sizeError != kAXErrorSuccess || !positionValue || !sizeValue) {
|
|
145
|
+
if (positionValue) CFRelease(positionValue);
|
|
146
|
+
if (sizeValue) CFRelease(sizeValue);
|
|
147
|
+
return NO;
|
|
148
|
+
}
|
|
333
149
|
|
|
334
|
-
|
|
150
|
+
CGPoint origin = CGPointZero;
|
|
151
|
+
CGSize size = CGSizeZero;
|
|
152
|
+
AXValueGetValue(positionValue, kAXValueTypeCGPoint, &origin);
|
|
153
|
+
AXValueGetValue(sizeValue, kAXValueTypeCGSize, &size);
|
|
335
154
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
155
|
+
CFRelease(positionValue);
|
|
156
|
+
CFRelease(sizeValue);
|
|
157
|
+
|
|
158
|
+
CGRect frame = CGRectMake(origin.x, origin.y, size.width, size.height);
|
|
159
|
+
return CGRectContainsPoint(frame, point);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint cursorPos) {
|
|
163
|
+
AXValueRef positionValue = NULL;
|
|
164
|
+
AXValueRef sizeValue = NULL;
|
|
165
|
+
|
|
166
|
+
AXError positionError = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, (CFTypeRef *)&positionValue);
|
|
167
|
+
AXError sizeError = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
|
|
342
168
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
169
|
+
if (positionError != kAXErrorSuccess || sizeError != kAXErrorSuccess || !positionValue || !sizeValue) {
|
|
170
|
+
if (positionValue) CFRelease(positionValue);
|
|
171
|
+
if (sizeValue) CFRelease(sizeValue);
|
|
172
|
+
return nil;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
CGPoint windowOrigin = CGPointZero;
|
|
176
|
+
CGSize windowSize = CGSizeZero;
|
|
177
|
+
AXValueGetValue(positionValue, kAXValueTypeCGPoint, &windowOrigin);
|
|
178
|
+
AXValueGetValue(sizeValue, kAXValueTypeCGSize, &windowSize);
|
|
179
|
+
|
|
180
|
+
CFRelease(positionValue);
|
|
181
|
+
CFRelease(sizeValue);
|
|
182
|
+
|
|
183
|
+
CGFloat edge = 4.0;
|
|
184
|
+
CGFloat x = cursorPos.x - windowOrigin.x;
|
|
185
|
+
CGFloat y = cursorPos.y - windowOrigin.y;
|
|
186
|
+
CGFloat w = windowSize.width;
|
|
187
|
+
CGFloat h = windowSize.height;
|
|
188
|
+
|
|
189
|
+
if (x < 0 || y < 0 || x > w || y > h) {
|
|
190
|
+
return nil;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
BOOL nearLeft = (x >= 0 && x <= edge);
|
|
194
|
+
BOOL nearRight = (x >= w - edge && x <= w);
|
|
195
|
+
BOOL nearTop = (y >= 0 && y <= edge);
|
|
196
|
+
BOOL nearBottom = (y >= h - edge && y <= h);
|
|
197
|
+
|
|
198
|
+
if ((nearLeft && nearTop) || (nearRight && nearBottom)) {
|
|
199
|
+
return @"nwse-resize";
|
|
200
|
+
}
|
|
201
|
+
if ((nearRight && nearTop) || (nearLeft && nearBottom)) {
|
|
202
|
+
return @"nesw-resize";
|
|
203
|
+
}
|
|
204
|
+
if (nearLeft || nearRight) {
|
|
205
|
+
return @"col-resize";
|
|
206
|
+
}
|
|
207
|
+
if (nearTop || nearBottom) {
|
|
208
|
+
return @"ns-resize";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return nil;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static NSString* CursorTypeFromAccessibilityElement(AXUIElementRef element, CGPoint cursorPos) {
|
|
215
|
+
if (!element) {
|
|
216
|
+
return nil;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
NSString *role = CopyAttributeString(element, kAXRoleAttribute);
|
|
220
|
+
NSString *subrole = CopyAttributeString(element, kAXSubroleAttribute);
|
|
221
|
+
NSString *roleDescription = CopyAttributeString(element, kAXRoleDescriptionAttribute);
|
|
222
|
+
|
|
223
|
+
if (StringEqualsAny(role, @[@"AXTextField", @"AXTextArea", @"AXSearchField"])) {
|
|
224
|
+
return @"text";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (StringEqualsAny(subrole, @[@"AXSecureTextField", @"AXTextField"])) {
|
|
228
|
+
return @"text";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
BOOL isEditable = NO;
|
|
232
|
+
if (CopyAttributeBoolean(element, CFSTR("AXEditable"), &isEditable) && isEditable) {
|
|
233
|
+
return @"text";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
BOOL supportsSelection = NO;
|
|
237
|
+
if (CopyAttributeBoolean(element, CFSTR("AXSupportsTextSelection"), &supportsSelection) && supportsSelection) {
|
|
238
|
+
return @"text";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
CFTypeRef valueAttribute = NULL;
|
|
242
|
+
if (AXUIElementCopyAttributeValue(element, kAXValueAttribute, &valueAttribute) == kAXErrorSuccess && valueAttribute) {
|
|
243
|
+
CFTypeID typeId = CFGetTypeID(valueAttribute);
|
|
244
|
+
if (typeId == CFAttributedStringGetTypeID() || typeId == CFStringGetTypeID()) {
|
|
245
|
+
CFRelease(valueAttribute);
|
|
246
|
+
return @"text";
|
|
247
|
+
}
|
|
248
|
+
CFRelease(valueAttribute);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (StringEqualsAny(role, @[@"AXProgressIndicator", @"AXBusyIndicator"])) {
|
|
252
|
+
return @"progress";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (StringEqualsAny(role, @[@"AXHelpTag", @"AXTooltip"])) {
|
|
256
|
+
return @"help";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if ([role isEqualToString:@"AXSplitter"]) {
|
|
260
|
+
NSString *orientation = CopyAttributeString(element, CFSTR("AXOrientation"));
|
|
261
|
+
if ([orientation isEqualToString:@"AXHorizontalOrientation"]) {
|
|
262
|
+
return @"ns-resize";
|
|
263
|
+
}
|
|
264
|
+
if ([orientation isEqualToString:@"AXVerticalOrientation"]) {
|
|
265
|
+
return @"col-resize";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if ([role isEqualToString:@"AXWindow"]) {
|
|
270
|
+
NSString *windowCursor = CursorTypeForWindowBorder(element, cursorPos);
|
|
271
|
+
if (windowCursor) {
|
|
272
|
+
return windowCursor;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (StringEqualsAny(role, @[@"AXMenuItem", @"AXButton", @"AXLink", @"AXTab", @"AXRadioButton", @"AXCheckBox", @"AXPopUpButton"])) {
|
|
277
|
+
return @"pointer";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (StringEqualsAny(subrole, @[@"AXLink", @"AXToolbarButton", @"AXFileDrop", @"AXDropTarget"])) {
|
|
281
|
+
return @"pointer";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (roleDescription) {
|
|
285
|
+
NSString *lower = [roleDescription lowercaseString];
|
|
286
|
+
if ([lower containsString:@"button"] ||
|
|
287
|
+
[lower containsString:@"link"] ||
|
|
288
|
+
[lower containsString:@"tab"]) {
|
|
289
|
+
return @"pointer";
|
|
290
|
+
}
|
|
291
|
+
if ([lower containsString:@"text"] ||
|
|
292
|
+
[lower containsString:@"editor"] ||
|
|
293
|
+
[lower containsString:@"document"]) {
|
|
294
|
+
return @"text";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (ElementHasAction(element, kAXPressAction) ||
|
|
299
|
+
ElementHasAction(element, kAXShowMenuAction) ||
|
|
300
|
+
ElementHasAction(element, CFSTR("AXConfirm"))) {
|
|
301
|
+
return @"pointer";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
CFTypeRef urlValue = NULL;
|
|
305
|
+
if (AXUIElementCopyAttributeValue(element, kAXURLAttribute, &urlValue) == kAXErrorSuccess && urlValue) {
|
|
306
|
+
CFRelease(urlValue);
|
|
307
|
+
return @"pointer";
|
|
308
|
+
}
|
|
309
|
+
if (urlValue) {
|
|
310
|
+
CFRelease(urlValue);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
BOOL isDraggable = NO;
|
|
314
|
+
if (CopyAttributeBoolean(element, CFSTR("AXDraggable"), &isDraggable) && isDraggable) {
|
|
315
|
+
return @"grab";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
BOOL isMovable = NO;
|
|
319
|
+
if (CopyAttributeBoolean(element, CFSTR("AXMovable"), &isMovable) && isMovable) {
|
|
320
|
+
return @"grab";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (subrole && [subrole isEqualToString:@"AXZoomIn"]) {
|
|
324
|
+
return @"zoom-in";
|
|
325
|
+
}
|
|
326
|
+
if (subrole && [subrole isEqualToString:@"AXZoomOut"]) {
|
|
327
|
+
return @"zoom-out";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return nil;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Mouse button state tracking
|
|
334
|
+
static bool g_leftMouseDown = false;
|
|
335
|
+
static bool g_rightMouseDown = false;
|
|
336
|
+
static NSString *g_lastEventType = @"move";
|
|
337
|
+
|
|
338
|
+
// Accessibility tabanlı cursor tip tespiti
|
|
339
|
+
static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
340
|
+
@autoreleasepool {
|
|
341
|
+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
342
|
+
if (!systemWide) {
|
|
343
|
+
return nil;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
NSString *cursorType = nil;
|
|
347
|
+
|
|
348
|
+
AXUIElementRef elementAtPosition = NULL;
|
|
349
|
+
AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
|
|
350
|
+
if (error == kAXErrorSuccess && elementAtPosition) {
|
|
351
|
+
cursorType = CursorTypeFromAccessibilityElement(elementAtPosition, cursorPos);
|
|
352
|
+
CFRelease(elementAtPosition);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!cursorType) {
|
|
356
|
+
AXValueRef pointValue = AXValueCreate(kAXValueTypeCGPoint, &cursorPos);
|
|
357
|
+
if (pointValue) {
|
|
358
|
+
AXUIElementRef hitElement = NULL;
|
|
359
|
+
AXError hitError = AXUIElementCopyParameterizedAttributeValue(systemWide, kAXHitTestParameterizedAttribute, pointValue, (CFTypeRef *)&hitElement);
|
|
360
|
+
CFRelease(pointValue);
|
|
361
|
+
if (hitError == kAXErrorSuccess && hitElement) {
|
|
362
|
+
cursorType = CursorTypeFromAccessibilityElement(hitElement, cursorPos);
|
|
363
|
+
CFRelease(hitElement);
|
|
364
|
+
}
|
|
346
365
|
}
|
|
366
|
+
}
|
|
347
367
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
368
|
+
if (!cursorType) {
|
|
369
|
+
AXUIElementRef focusedElement = NULL;
|
|
370
|
+
if (AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement) == kAXErrorSuccess && focusedElement) {
|
|
371
|
+
if (PointInsideElementFrame(focusedElement, cursorPos)) {
|
|
372
|
+
cursorType = CursorTypeFromAccessibilityElement(focusedElement, cursorPos);
|
|
373
|
+
}
|
|
374
|
+
CFRelease(focusedElement);
|
|
375
|
+
}
|
|
354
376
|
}
|
|
377
|
+
|
|
378
|
+
CFRelease(systemWide);
|
|
379
|
+
return cursorType;
|
|
355
380
|
}
|
|
356
381
|
}
|
|
357
382
|
|
|
@@ -569,12 +594,10 @@ NSString* getCursorType() {
|
|
|
569
594
|
if (axCursorType && ![axCursorType isEqualToString:@"default"]) {
|
|
570
595
|
finalType = axCursorType;
|
|
571
596
|
} else if (systemCursorType && [systemCursorType length] > 0) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
finalType = systemCursorType;
|
|
577
|
-
}
|
|
597
|
+
// Prefer the system cursor when accessibility reports a generic value.
|
|
598
|
+
finalType = systemCursorType;
|
|
599
|
+
} else if (axCursorType && [axCursorType length] > 0) {
|
|
600
|
+
finalType = axCursorType;
|
|
578
601
|
} else {
|
|
579
602
|
finalType = @"default";
|
|
580
603
|
}
|
|
@@ -629,17 +652,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
629
652
|
|
|
630
653
|
CGPoint rawLocation = CGEventGetLocation(event);
|
|
631
654
|
|
|
632
|
-
//
|
|
633
|
-
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
655
|
+
// Coordinates are already in logical space; no additional scaling needed here.
|
|
634
656
|
CGPoint location = rawLocation;
|
|
635
|
-
|
|
636
|
-
if (scalingInfo) {
|
|
637
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
638
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
639
|
-
|
|
640
|
-
// Keep logical coordinates - no scaling needed here
|
|
641
|
-
location = rawLocation;
|
|
642
|
-
}
|
|
643
657
|
NSDate *currentDate = [NSDate date];
|
|
644
658
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
645
659
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
@@ -702,18 +716,9 @@ void cursorTimerCallback() {
|
|
|
702
716
|
CFRelease(event);
|
|
703
717
|
}
|
|
704
718
|
|
|
705
|
-
//
|
|
706
|
-
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
719
|
+
// Coordinates are already in logical space; no additional scaling needed here.
|
|
707
720
|
CGPoint location = rawLocation;
|
|
708
721
|
|
|
709
|
-
if (scalingInfo) {
|
|
710
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
711
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
712
|
-
|
|
713
|
-
// Keep logical coordinates - no scaling needed here
|
|
714
|
-
location = rawLocation;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
722
|
NSDate *currentDate = [NSDate date];
|
|
718
723
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
719
724
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
@@ -1042,16 +1047,8 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
|
|
|
1042
1047
|
// Get display scaling information
|
|
1043
1048
|
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
1044
1049
|
CGPoint logicalLocation = rawLocation;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
1048
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
1049
|
-
|
|
1050
|
-
// CGEventGetLocation returns LOGICAL coordinates (correct for JS layer)
|
|
1051
|
-
// Keep logical coordinates - transformation happens in JS layer
|
|
1052
|
-
logicalLocation = rawLocation;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1050
|
+
// CGEventGetLocation already returns logical coordinates; additional scaling happens in JS layer.
|
|
1051
|
+
|
|
1055
1052
|
NSString *cursorType = getCursorType();
|
|
1056
1053
|
|
|
1057
1054
|
// Mouse button state'ini kontrol et
|