node-mac-recorder 2.20.1 → 2.20.3
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 +471 -226
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;
|
|
@@ -40,16 +44,203 @@ static CursorTimerTarget *g_timerTarget = nil;
|
|
|
40
44
|
static NSString *g_lastDetectedCursorType = nil;
|
|
41
45
|
static int g_cursorTypeCounter = 0;
|
|
42
46
|
|
|
47
|
+
static NSString* CopyAndReleaseCFString(CFStringRef value) {
|
|
48
|
+
if (!value) {
|
|
49
|
+
return nil;
|
|
50
|
+
}
|
|
51
|
+
NSString *result = [NSString stringWithString:(NSString *)value];
|
|
52
|
+
CFRelease(value);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static BOOL pointInsideElementFrame(AXUIElementRef element, CGPoint point) {
|
|
57
|
+
if (!element) {
|
|
58
|
+
return NO;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
AXValueRef positionValue = NULL;
|
|
62
|
+
AXValueRef sizeValue = NULL;
|
|
63
|
+
|
|
64
|
+
AXError positionError = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, (CFTypeRef *)&positionValue);
|
|
65
|
+
AXError sizeError = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
|
|
66
|
+
|
|
67
|
+
if (positionError != kAXErrorSuccess || sizeError != kAXErrorSuccess || !positionValue || !sizeValue) {
|
|
68
|
+
if (positionValue) CFRelease(positionValue);
|
|
69
|
+
if (sizeValue) CFRelease(sizeValue);
|
|
70
|
+
return NO;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
CGPoint elementOrigin = CGPointZero;
|
|
74
|
+
CGSize elementSize = CGSizeZero;
|
|
75
|
+
AXValueGetValue(positionValue, kAXValueTypeCGPoint, &elementOrigin);
|
|
76
|
+
AXValueGetValue(sizeValue, kAXValueTypeCGSize, &elementSize);
|
|
77
|
+
|
|
78
|
+
CFRelease(positionValue);
|
|
79
|
+
CFRelease(sizeValue);
|
|
80
|
+
|
|
81
|
+
CGRect frame = CGRectMake(elementOrigin.x, elementOrigin.y, elementSize.width, elementSize.height);
|
|
82
|
+
return CGRectContainsPoint(frame, point);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static BOOL elementHasAction(AXUIElementRef element, CFStringRef actionName) {
|
|
86
|
+
if (!element || !actionName) {
|
|
87
|
+
return NO;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
CFArrayRef actions = NULL;
|
|
91
|
+
AXError error = AXUIElementCopyActionNames(element, &actions);
|
|
92
|
+
if (error != kAXErrorSuccess || !actions) {
|
|
93
|
+
return NO;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
BOOL hasAction = NO;
|
|
97
|
+
CFIndex count = CFArrayGetCount(actions);
|
|
98
|
+
for (CFIndex i = 0; i < count; i++) {
|
|
99
|
+
CFStringRef action = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
|
|
100
|
+
if (CFStringCompare(action, actionName, 0) == kCFCompareEqualTo) {
|
|
101
|
+
hasAction = YES;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
CFRelease(actions);
|
|
107
|
+
return hasAction;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static BOOL elementIsClickable(AXUIElementRef element) {
|
|
111
|
+
if (!element) {
|
|
112
|
+
return NO;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
CFStringRef roleRef = NULL;
|
|
116
|
+
if (AXUIElementCopyAttributeValue(element, kAXRoleAttribute, (CFTypeRef *)&roleRef) == kAXErrorSuccess && roleRef) {
|
|
117
|
+
NSString *role = CopyAndReleaseCFString(roleRef);
|
|
118
|
+
if ([role isEqualToString:@"AXButton"] ||
|
|
119
|
+
[role isEqualToString:@"AXLink"] ||
|
|
120
|
+
[role isEqualToString:@"AXMenuItem"] ||
|
|
121
|
+
[role isEqualToString:@"AXTab"] ||
|
|
122
|
+
[role isEqualToString:@"AXRadioButton"] ||
|
|
123
|
+
[role isEqualToString:@"AXCheckBox"] ||
|
|
124
|
+
[role isEqualToString:@"AXPopUpButton"]) {
|
|
125
|
+
return YES;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
CFStringRef subroleRef = NULL;
|
|
130
|
+
if (AXUIElementCopyAttributeValue(element, kAXSubroleAttribute, (CFTypeRef *)&subroleRef) == kAXErrorSuccess && subroleRef) {
|
|
131
|
+
NSString *subrole = CopyAndReleaseCFString(subroleRef);
|
|
132
|
+
if ([subrole isEqualToString:@"AXLink"] ||
|
|
133
|
+
[subrole isEqualToString:@"AXFileDrop"] ||
|
|
134
|
+
[subrole isEqualToString:@"AXDropTarget"] ||
|
|
135
|
+
[subrole isEqualToString:@"AXToolbarButton"]) {
|
|
136
|
+
return YES;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (elementHasAction(element, kAXPressAction) ||
|
|
141
|
+
elementHasAction(element, kAXShowMenuAction) ||
|
|
142
|
+
elementHasAction(element, CFSTR("AXConfirm"))) {
|
|
143
|
+
return YES;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
CFTypeRef urlValue = NULL;
|
|
147
|
+
if (AXUIElementCopyAttributeValue(element, kAXURLAttribute, &urlValue) == kAXErrorSuccess && urlValue) {
|
|
148
|
+
CFRelease(urlValue);
|
|
149
|
+
return YES;
|
|
150
|
+
}
|
|
151
|
+
if (urlValue) {
|
|
152
|
+
CFRelease(urlValue);
|
|
153
|
+
urlValue = NULL;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
CFBooleanRef isEnabled = NULL;
|
|
157
|
+
if (AXUIElementCopyAttributeValue(element, kAXEnabledAttribute, (CFTypeRef *)&isEnabled) == kAXErrorSuccess &&
|
|
158
|
+
isEnabled && CFBooleanGetValue(isEnabled)) {
|
|
159
|
+
CFRelease(isEnabled);
|
|
160
|
+
isEnabled = NULL;
|
|
161
|
+
if (elementHasAction(element, CFSTR("AXShowMenu")) || elementHasAction(element, CFSTR("AXDecrement")) || elementHasAction(element, CFSTR("AXIncrement"))) {
|
|
162
|
+
return YES;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (isEnabled) {
|
|
166
|
+
CFRelease(isEnabled);
|
|
167
|
+
isEnabled = NULL;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return NO;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static BOOL elementSupportsText(AXUIElementRef element) {
|
|
174
|
+
if (!element) {
|
|
175
|
+
return NO;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
BOOL supportsText = NO;
|
|
179
|
+
|
|
180
|
+
CFStringRef roleRef = NULL;
|
|
181
|
+
if (AXUIElementCopyAttributeValue(element, kAXRoleAttribute, (CFTypeRef *)&roleRef) == kAXErrorSuccess && roleRef) {
|
|
182
|
+
NSString *role = CopyAndReleaseCFString(roleRef);
|
|
183
|
+
if ([role isEqualToString:@"AXTextField"] ||
|
|
184
|
+
[role isEqualToString:@"AXTextArea"] ||
|
|
185
|
+
[role isEqualToString:@"AXSearchField"] ||
|
|
186
|
+
[role isEqualToString:@"AXStaticText"] ||
|
|
187
|
+
[role isEqualToString:@"AXDocument"] ||
|
|
188
|
+
[role isEqualToString:@"AXWebArea"]) {
|
|
189
|
+
supportsText = YES;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
CFStringRef subroleRef = NULL;
|
|
194
|
+
if (!supportsText && AXUIElementCopyAttributeValue(element, kAXSubroleAttribute, (CFTypeRef *)&subroleRef) == kAXErrorSuccess && subroleRef) {
|
|
195
|
+
NSString *subrole = CopyAndReleaseCFString(subroleRef);
|
|
196
|
+
if ([subrole isEqualToString:@"AXSecureTextField"] ||
|
|
197
|
+
[subrole isEqualToString:@"AXTextAttachment"] ||
|
|
198
|
+
[subrole isEqualToString:@"AXTextField"]) {
|
|
199
|
+
supportsText = YES;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
CFStringRef roleDescriptionRef = NULL;
|
|
204
|
+
if (!supportsText && AXUIElementCopyAttributeValue(element, kAXRoleDescriptionAttribute, (CFTypeRef *)&roleDescriptionRef) == kAXErrorSuccess && roleDescriptionRef) {
|
|
205
|
+
NSString *roleDescription = CopyAndReleaseCFString(roleDescriptionRef);
|
|
206
|
+
NSString *lower = [roleDescription lowercaseString];
|
|
207
|
+
if ([lower containsString:@"text"] ||
|
|
208
|
+
[lower containsString:@"editor"] ||
|
|
209
|
+
[lower containsString:@"code"] ||
|
|
210
|
+
[lower containsString:@"document"]) {
|
|
211
|
+
supportsText = YES;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
CFBooleanRef editable = NULL;
|
|
216
|
+
if (!supportsText && AXUIElementCopyAttributeValue(element, CFSTR("AXEditable"), (CFTypeRef *)&editable) == kAXErrorSuccess && editable) {
|
|
217
|
+
supportsText = CFBooleanGetValue(editable);
|
|
218
|
+
CFRelease(editable);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
CFBooleanRef supportsSelection = NULL;
|
|
222
|
+
if (!supportsText && AXUIElementCopyAttributeValue(element, CFSTR("AXSupportsTextSelection"), (CFTypeRef *)&supportsSelection) == kAXErrorSuccess && supportsSelection) {
|
|
223
|
+
supportsText = CFBooleanGetValue(supportsSelection);
|
|
224
|
+
CFRelease(supportsSelection);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
CFTypeRef valueAttribute = NULL;
|
|
228
|
+
if (!supportsText && AXUIElementCopyAttributeValue(element, kAXValueAttribute, &valueAttribute) == kAXErrorSuccess && valueAttribute) {
|
|
229
|
+
CFTypeID typeId = CFGetTypeID(valueAttribute);
|
|
230
|
+
if (typeId == CFAttributedStringGetTypeID() || typeId == CFStringGetTypeID()) {
|
|
231
|
+
supportsText = YES;
|
|
232
|
+
}
|
|
233
|
+
CFRelease(valueAttribute);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return supportsText;
|
|
237
|
+
}
|
|
238
|
+
|
|
43
239
|
// Mouse button state tracking
|
|
44
240
|
static bool g_leftMouseDown = false;
|
|
45
241
|
static bool g_rightMouseDown = false;
|
|
46
242
|
static NSString *g_lastEventType = @"move";
|
|
47
243
|
|
|
48
|
-
// Event tap callback
|
|
49
|
-
static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) {
|
|
50
|
-
return event;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
244
|
// Accessibility tabanlı cursor tip tespiti
|
|
54
245
|
static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
55
246
|
@autoreleasepool {
|
|
@@ -64,223 +255,304 @@ static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
|
64
255
|
NSString *cursorType = @"default"; // Default fallback
|
|
65
256
|
|
|
66
257
|
if (error == kAXErrorSuccess && elementAtPosition) {
|
|
258
|
+
NSString *elementRole = nil;
|
|
67
259
|
CFStringRef role = NULL;
|
|
68
260
|
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
|
|
69
|
-
|
|
70
261
|
if (error == kAXErrorSuccess && role) {
|
|
71
|
-
|
|
262
|
+
elementRole = CopyAndReleaseCFString(role);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (elementRole) {
|
|
72
266
|
NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
|
|
267
|
+
}
|
|
73
268
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
269
|
+
// TEXT CURSORS
|
|
270
|
+
if ([elementRole isEqualToString:@"AXTextField"] ||
|
|
271
|
+
[elementRole isEqualToString:@"AXTextArea"] ||
|
|
272
|
+
[elementRole isEqualToString:@"AXStaticText"] ||
|
|
273
|
+
[elementRole isEqualToString:@"AXSearchField"]) {
|
|
274
|
+
cursorType = @"text";
|
|
275
|
+
}
|
|
276
|
+
// POINTER CURSORS (clickable elements)
|
|
277
|
+
else if ([elementRole isEqualToString:@"AXLink"] ||
|
|
278
|
+
[elementRole isEqualToString:@"AXButton"] ||
|
|
279
|
+
[elementRole isEqualToString:@"AXMenuItem"] ||
|
|
280
|
+
[elementRole isEqualToString:@"AXRadioButton"] ||
|
|
281
|
+
[elementRole isEqualToString:@"AXCheckBox"] ||
|
|
282
|
+
[elementRole isEqualToString:@"AXPopUpButton"] ||
|
|
283
|
+
[elementRole isEqualToString:@"AXTab"]) {
|
|
284
|
+
cursorType = @"pointer";
|
|
285
|
+
}
|
|
286
|
+
// GRAB CURSORS (draggable elements)
|
|
287
|
+
else if ([elementRole isEqualToString:@"AXImage"] ||
|
|
288
|
+
[elementRole isEqualToString:@"AXGroup"]) {
|
|
289
|
+
// Check if element is draggable
|
|
290
|
+
CFBooleanRef draggable = NULL;
|
|
291
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
|
|
292
|
+
if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
|
|
293
|
+
cursorType = @"grab";
|
|
294
|
+
} else {
|
|
295
|
+
cursorType = @"default";
|
|
90
296
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
297
|
+
}
|
|
298
|
+
// PROGRESS CURSORS (loading/busy elements)
|
|
299
|
+
else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
|
|
300
|
+
[elementRole isEqualToString:@"AXBusyIndicator"]) {
|
|
301
|
+
cursorType = @"progress";
|
|
302
|
+
}
|
|
303
|
+
// HELP CURSORS (help buttons/tooltips)
|
|
304
|
+
else if ([elementRole isEqualToString:@"AXHelpTag"] ||
|
|
305
|
+
[elementRole isEqualToString:@"AXTooltip"]) {
|
|
306
|
+
cursorType = @"help";
|
|
307
|
+
}
|
|
308
|
+
// RESIZE CURSORS - sadece AXSplitter için
|
|
309
|
+
else if ([elementRole isEqualToString:@"AXSplitter"]) {
|
|
310
|
+
// Get splitter orientation to determine resize direction
|
|
311
|
+
CFStringRef orientation = NULL;
|
|
312
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
|
|
313
|
+
if (error == kAXErrorSuccess && orientation) {
|
|
314
|
+
NSString *orientationStr = CopyAndReleaseCFString(orientation);
|
|
315
|
+
if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
|
|
316
|
+
cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
|
|
317
|
+
} else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
|
|
318
|
+
cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
|
|
99
319
|
} else {
|
|
100
|
-
cursorType = @"default";
|
|
320
|
+
cursorType = @"default"; // Bilinmeyen orientation
|
|
101
321
|
}
|
|
322
|
+
} else {
|
|
323
|
+
cursorType = @"default"; // Orientation alınamazsa default
|
|
102
324
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
325
|
+
}
|
|
326
|
+
// SCROLL CURSORS - hep default olsun, all-scroll görünmesin
|
|
327
|
+
else if ([elementRole isEqualToString:@"AXScrollBar"]) {
|
|
328
|
+
cursorType = @"default"; // ScrollBar'lar için de default
|
|
329
|
+
}
|
|
330
|
+
// AXScrollArea - hep default
|
|
331
|
+
else if ([elementRole isEqualToString:@"AXScrollArea"]) {
|
|
332
|
+
cursorType = @"default"; // ScrollArea her zaman default
|
|
333
|
+
}
|
|
334
|
+
// CROSSHAIR CURSORS (drawing/selection tools)
|
|
335
|
+
else if ([elementRole isEqualToString:@"AXCanvas"] ||
|
|
336
|
+
[elementRole isEqualToString:@"AXDrawingArea"]) {
|
|
337
|
+
cursorType = @"crosshair";
|
|
338
|
+
}
|
|
339
|
+
// ZOOM CURSORS (zoom controls)
|
|
340
|
+
else if ([elementRole isEqualToString:@"AXZoomButton"]) {
|
|
341
|
+
cursorType = @"zoom-in";
|
|
342
|
+
}
|
|
343
|
+
// NOT-ALLOWED CURSORS (disabled elements)
|
|
344
|
+
else if ([elementRole isEqualToString:@"AXStaticText"] ||
|
|
345
|
+
[elementRole isEqualToString:@"AXGroup"]) {
|
|
346
|
+
// Check if element is disabled/readonly
|
|
347
|
+
CFBooleanRef enabled = NULL;
|
|
348
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
|
|
349
|
+
if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
|
|
350
|
+
cursorType = @"not-allowed";
|
|
112
351
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
352
|
+
}
|
|
353
|
+
// WINDOW BORDER RESIZE - sadece pencere kenarlarında
|
|
354
|
+
else if ([elementRole isEqualToString:@"AXWindow"]) {
|
|
355
|
+
// Check window attributes to see if it's resizable
|
|
356
|
+
CFBooleanRef resizable = NULL;
|
|
357
|
+
AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
|
|
358
|
+
|
|
359
|
+
// Sadece resize edilebilir pencereler için cursor değişimi
|
|
360
|
+
if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
|
|
361
|
+
CFTypeRef position = NULL;
|
|
362
|
+
CFTypeRef size = NULL;
|
|
363
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
|
|
364
|
+
AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
|
|
365
|
+
|
|
366
|
+
if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
|
|
367
|
+
CGPoint windowPos;
|
|
368
|
+
CGSize windowSize;
|
|
369
|
+
AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
|
|
370
|
+
AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
|
|
371
|
+
|
|
372
|
+
CGFloat x = cursorPos.x - windowPos.x;
|
|
373
|
+
CGFloat y = cursorPos.y - windowPos.y;
|
|
374
|
+
CGFloat w = windowSize.width;
|
|
375
|
+
CGFloat h = windowSize.height;
|
|
376
|
+
CGFloat edge = 3.0; // Daha küçük edge detection (3px)
|
|
377
|
+
|
|
378
|
+
// Sadece çok kenar köşelerde resize cursor'ı göster
|
|
379
|
+
BOOL isOnBorder = NO;
|
|
380
|
+
|
|
381
|
+
// Corner resize detection - çok dar alanda, doğru açılar
|
|
382
|
+
if (x <= edge && y <= edge) {
|
|
383
|
+
cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
|
|
384
|
+
isOnBorder = YES;
|
|
385
|
+
}
|
|
386
|
+
else if (x >= w-edge && y <= edge) {
|
|
387
|
+
cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
|
|
388
|
+
isOnBorder = YES;
|
|
389
|
+
}
|
|
390
|
+
else if (x <= edge && y >= h-edge) {
|
|
391
|
+
cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
|
|
392
|
+
isOnBorder = YES;
|
|
393
|
+
}
|
|
394
|
+
else if (x >= w-edge && y >= h-edge) {
|
|
395
|
+
cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
|
|
396
|
+
isOnBorder = YES;
|
|
126
397
|
}
|
|
398
|
+
// Edge resize detection - sadece çok kenarlarda
|
|
399
|
+
else if (x <= edge && y > edge && y < h-edge) {
|
|
400
|
+
cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
|
|
401
|
+
isOnBorder = YES;
|
|
402
|
+
}
|
|
403
|
+
else if (x >= w-edge && y > edge && y < h-edge) {
|
|
404
|
+
cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
|
|
405
|
+
isOnBorder = YES;
|
|
406
|
+
}
|
|
407
|
+
else if (y <= edge && x > edge && x < w-edge) {
|
|
408
|
+
cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
|
|
409
|
+
isOnBorder = YES;
|
|
410
|
+
}
|
|
411
|
+
else if (y >= h-edge && x > edge && x < w-edge) {
|
|
412
|
+
cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
|
|
413
|
+
isOnBorder = YES;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Eğer border'da değilse default
|
|
417
|
+
if (!isOnBorder) {
|
|
418
|
+
cursorType = @"default";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (position) CFRelease(position);
|
|
422
|
+
if (size) CFRelease(size);
|
|
127
423
|
} else {
|
|
128
|
-
cursorType = @"default";
|
|
424
|
+
cursorType = @"default";
|
|
129
425
|
}
|
|
426
|
+
} else {
|
|
427
|
+
cursorType = @"default";
|
|
130
428
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
429
|
+
}
|
|
430
|
+
// HER DURUM İÇİN DEFAULT FALLBACK
|
|
431
|
+
else {
|
|
432
|
+
// Bilinmeyen elementler için her zaman default
|
|
433
|
+
cursorType = @"default";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check subroles for additional context
|
|
437
|
+
CFStringRef subrole = NULL;
|
|
438
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
|
|
439
|
+
if (error == kAXErrorSuccess && subrole) {
|
|
440
|
+
NSString *elementSubrole = CopyAndReleaseCFString(subrole);
|
|
441
|
+
NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
|
|
442
|
+
|
|
443
|
+
// Subrole override'ları - sadece çok spesifik durumlar için
|
|
444
|
+
if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
|
|
445
|
+
[elementSubrole isEqualToString:@"AXMinimizeButton"] ||
|
|
446
|
+
[elementSubrole isEqualToString:@"AXZoomButton"] ||
|
|
447
|
+
[elementSubrole isEqualToString:@"AXToolbarButton"]) {
|
|
448
|
+
cursorType = @"pointer";
|
|
449
|
+
}
|
|
450
|
+
// Copy/alias subroles - sadece bu durumlar için override
|
|
451
|
+
else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
|
|
452
|
+
[elementSubrole isEqualToString:@"AXDropTarget"]) {
|
|
453
|
+
cursorType = @"copy";
|
|
134
454
|
}
|
|
135
|
-
//
|
|
136
|
-
else if ([
|
|
137
|
-
|
|
455
|
+
// Alias/shortcut subroles
|
|
456
|
+
else if ([elementSubrole isEqualToString:@"AXAlias"] ||
|
|
457
|
+
[elementSubrole isEqualToString:@"AXShortcut"]) {
|
|
458
|
+
cursorType = @"alias";
|
|
138
459
|
}
|
|
139
|
-
//
|
|
140
|
-
else if ([
|
|
141
|
-
[
|
|
142
|
-
cursorType = @"
|
|
460
|
+
// Grabbing state (being dragged) - sadece gerçek drag sırasında
|
|
461
|
+
else if ([elementSubrole isEqualToString:@"AXDragging"] ||
|
|
462
|
+
[elementSubrole isEqualToString:@"AXMoving"]) {
|
|
463
|
+
cursorType = @"grabbing";
|
|
143
464
|
}
|
|
144
|
-
//
|
|
145
|
-
else if ([
|
|
465
|
+
// Zoom controls - sadece spesifik zoom butonları için
|
|
466
|
+
else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
|
|
146
467
|
cursorType = @"zoom-in";
|
|
147
468
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
469
|
+
else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
|
|
470
|
+
cursorType = @"zoom-out";
|
|
471
|
+
}
|
|
472
|
+
// Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
CFStringRef roleDescriptionRef = NULL;
|
|
476
|
+
AXError descriptionError = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleDescriptionAttribute, (CFTypeRef*)&roleDescriptionRef);
|
|
477
|
+
if (descriptionError == kAXErrorSuccess && roleDescriptionRef) {
|
|
478
|
+
NSString *roleDescription = CopyAndReleaseCFString(roleDescriptionRef);
|
|
479
|
+
if (roleDescription) {
|
|
480
|
+
NSString *roleDescriptionLower = [roleDescription lowercaseString];
|
|
481
|
+
if ([roleDescriptionLower containsString:@"text"] ||
|
|
482
|
+
[roleDescriptionLower containsString:@"editor"] ||
|
|
483
|
+
[roleDescriptionLower containsString:@"code"] ||
|
|
484
|
+
[roleDescriptionLower containsString:@"document"]) {
|
|
485
|
+
cursorType = @"text";
|
|
486
|
+
} else if ([roleDescriptionLower containsString:@"button"] ||
|
|
487
|
+
[roleDescriptionLower containsString:@"link"] ||
|
|
488
|
+
[roleDescriptionLower containsString:@"tab"]) {
|
|
489
|
+
cursorType = @"pointer";
|
|
156
490
|
}
|
|
157
491
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
else if (x <= edge && y > edge && y < h-edge) {
|
|
205
|
-
cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
|
|
206
|
-
isOnBorder = YES;
|
|
207
|
-
}
|
|
208
|
-
else if (x >= w-edge && y > edge && y < h-edge) {
|
|
209
|
-
cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
|
|
210
|
-
isOnBorder = YES;
|
|
211
|
-
}
|
|
212
|
-
else if (y <= edge && x > edge && x < w-edge) {
|
|
213
|
-
cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
|
|
214
|
-
isOnBorder = YES;
|
|
215
|
-
}
|
|
216
|
-
else if (y >= h-edge && x > edge && x < w-edge) {
|
|
217
|
-
cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
|
|
218
|
-
isOnBorder = YES;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Eğer border'da değilse default
|
|
222
|
-
if (!isOnBorder) {
|
|
223
|
-
cursorType = @"default";
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (position) CFRelease(position);
|
|
227
|
-
if (size) CFRelease(size);
|
|
228
|
-
} else {
|
|
229
|
-
cursorType = @"default";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
CFBooleanRef isEditable = NULL;
|
|
495
|
+
if (AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXEditable"), (CFTypeRef*)&isEditable) == kAXErrorSuccess &&
|
|
496
|
+
isEditable && CFBooleanGetValue(isEditable)) {
|
|
497
|
+
cursorType = @"text";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
CFBooleanRef supportsTextSelection = NULL;
|
|
501
|
+
if (AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXSupportsTextSelection"), (CFTypeRef*)&supportsTextSelection) == kAXErrorSuccess &&
|
|
502
|
+
supportsTextSelection && CFBooleanGetValue(supportsTextSelection)) {
|
|
503
|
+
cursorType = @"text";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
CFTypeRef valueAttribute = NULL;
|
|
507
|
+
if (AXUIElementCopyAttributeValue(elementAtPosition, kAXValueAttribute, &valueAttribute) == kAXErrorSuccess && valueAttribute) {
|
|
508
|
+
CFTypeID typeId = CFGetTypeID(valueAttribute);
|
|
509
|
+
if (typeId == CFAttributedStringGetTypeID() ||
|
|
510
|
+
typeId == CFStringGetTypeID()) {
|
|
511
|
+
cursorType = @"text";
|
|
512
|
+
}
|
|
513
|
+
CFRelease(valueAttribute);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (elementAtPosition) {
|
|
519
|
+
if ([cursorType isEqualToString:@"default"] && elementSupportsText(elementAtPosition)) {
|
|
520
|
+
cursorType = @"text";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if ([cursorType isEqualToString:@"default"] && elementIsClickable(elementAtPosition)) {
|
|
524
|
+
cursorType = @"pointer";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if ([cursorType isEqualToString:@"default"]) {
|
|
528
|
+
AXValueRef pointValue = AXValueCreate(kAXValueTypeCGPoint, &cursorPos);
|
|
529
|
+
if (pointValue) {
|
|
530
|
+
AXUIElementRef deepElement = NULL;
|
|
531
|
+
AXError hitError = AXUIElementCopyParameterizedAttributeValue(systemWide, kAXHitTestParameterizedAttribute, pointValue, (CFTypeRef *)&deepElement);
|
|
532
|
+
CFRelease(pointValue);
|
|
533
|
+
if (hitError == kAXErrorSuccess && deepElement) {
|
|
534
|
+
if (elementSupportsText(deepElement)) {
|
|
535
|
+
cursorType = @"text";
|
|
536
|
+
} else if (elementIsClickable(deepElement)) {
|
|
537
|
+
cursorType = @"pointer";
|
|
230
538
|
}
|
|
231
|
-
|
|
232
|
-
cursorType = @"default";
|
|
539
|
+
CFRelease(deepElement);
|
|
233
540
|
}
|
|
234
541
|
}
|
|
235
|
-
|
|
236
|
-
else {
|
|
237
|
-
// Bilinmeyen elementler için her zaman default
|
|
238
|
-
cursorType = @"default";
|
|
239
|
-
}
|
|
542
|
+
}
|
|
240
543
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// Subrole override'ları - sadece çok spesifik durumlar için
|
|
249
|
-
if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
|
|
250
|
-
[elementSubrole isEqualToString:@"AXMinimizeButton"] ||
|
|
251
|
-
[elementSubrole isEqualToString:@"AXZoomButton"] ||
|
|
252
|
-
[elementSubrole isEqualToString:@"AXToolbarButton"]) {
|
|
544
|
+
if ([cursorType isEqualToString:@"default"]) {
|
|
545
|
+
AXUIElementRef focusedElement = NULL;
|
|
546
|
+
if (AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement) == kAXErrorSuccess && focusedElement) {
|
|
547
|
+
if (elementSupportsText(focusedElement) && pointInsideElementFrame(focusedElement, cursorPos)) {
|
|
548
|
+
cursorType = @"text";
|
|
549
|
+
} else if (elementIsClickable(focusedElement) && pointInsideElementFrame(focusedElement, cursorPos)) {
|
|
253
550
|
cursorType = @"pointer";
|
|
254
551
|
}
|
|
255
|
-
|
|
256
|
-
else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
|
|
257
|
-
[elementSubrole isEqualToString:@"AXDropTarget"]) {
|
|
258
|
-
cursorType = @"copy";
|
|
259
|
-
}
|
|
260
|
-
// Alias/shortcut subroles
|
|
261
|
-
else if ([elementSubrole isEqualToString:@"AXAlias"] ||
|
|
262
|
-
[elementSubrole isEqualToString:@"AXShortcut"]) {
|
|
263
|
-
cursorType = @"alias";
|
|
264
|
-
}
|
|
265
|
-
// Grabbing state (being dragged) - sadece gerçek drag sırasında
|
|
266
|
-
else if ([elementSubrole isEqualToString:@"AXDragging"] ||
|
|
267
|
-
[elementSubrole isEqualToString:@"AXMoving"]) {
|
|
268
|
-
cursorType = @"grabbing";
|
|
269
|
-
}
|
|
270
|
-
// Zoom controls - sadece spesifik zoom butonları için
|
|
271
|
-
else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
|
|
272
|
-
cursorType = @"zoom-in";
|
|
273
|
-
}
|
|
274
|
-
else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
|
|
275
|
-
cursorType = @"zoom-out";
|
|
276
|
-
}
|
|
277
|
-
// Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
|
|
552
|
+
CFRelease(focusedElement);
|
|
278
553
|
}
|
|
279
554
|
}
|
|
280
555
|
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (elementAtPosition) {
|
|
284
556
|
CFRelease(elementAtPosition);
|
|
285
557
|
}
|
|
286
558
|
if (systemWide) {
|
|
@@ -482,10 +754,6 @@ NSString* getCursorType() {
|
|
|
482
754
|
g_cursorTypeCounter++;
|
|
483
755
|
|
|
484
756
|
NSString *systemCursorType = detectSystemCursorType();
|
|
485
|
-
if (systemCursorType && ![systemCursorType isEqualToString:@"default"]) {
|
|
486
|
-
NSLog(@"🎯 FINAL CURSOR TYPE: %@", systemCursorType);
|
|
487
|
-
return systemCursorType;
|
|
488
|
-
}
|
|
489
757
|
|
|
490
758
|
NSString *axCursorType = nil;
|
|
491
759
|
BOOL hasCursorPosition = NO;
|
|
@@ -520,7 +788,10 @@ NSString* getCursorType() {
|
|
|
520
788
|
if (axCursorType && ![axCursorType isEqualToString:@"default"]) {
|
|
521
789
|
finalType = axCursorType;
|
|
522
790
|
} else if (systemCursorType && [systemCursorType length] > 0) {
|
|
791
|
+
// Prefer the system cursor when accessibility reports a generic value.
|
|
523
792
|
finalType = systemCursorType;
|
|
793
|
+
} else if (axCursorType && [axCursorType length] > 0) {
|
|
794
|
+
finalType = axCursorType;
|
|
524
795
|
} else {
|
|
525
796
|
finalType = @"default";
|
|
526
797
|
}
|
|
@@ -575,17 +846,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
575
846
|
|
|
576
847
|
CGPoint rawLocation = CGEventGetLocation(event);
|
|
577
848
|
|
|
578
|
-
//
|
|
579
|
-
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
849
|
+
// Coordinates are already in logical space; no additional scaling needed here.
|
|
580
850
|
CGPoint location = rawLocation;
|
|
581
|
-
|
|
582
|
-
if (scalingInfo) {
|
|
583
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
584
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
585
|
-
|
|
586
|
-
// Keep logical coordinates - no scaling needed here
|
|
587
|
-
location = rawLocation;
|
|
588
|
-
}
|
|
589
851
|
NSDate *currentDate = [NSDate date];
|
|
590
852
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
591
853
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
@@ -648,18 +910,9 @@ void cursorTimerCallback() {
|
|
|
648
910
|
CFRelease(event);
|
|
649
911
|
}
|
|
650
912
|
|
|
651
|
-
//
|
|
652
|
-
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
913
|
+
// Coordinates are already in logical space; no additional scaling needed here.
|
|
653
914
|
CGPoint location = rawLocation;
|
|
654
915
|
|
|
655
|
-
if (scalingInfo) {
|
|
656
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
657
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
658
|
-
|
|
659
|
-
// Keep logical coordinates - no scaling needed here
|
|
660
|
-
location = rawLocation;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
916
|
NSDate *currentDate = [NSDate date];
|
|
664
917
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
665
918
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
@@ -988,16 +1241,8 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
|
|
|
988
1241
|
// Get display scaling information
|
|
989
1242
|
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
990
1243
|
CGPoint logicalLocation = rawLocation;
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
994
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
995
|
-
|
|
996
|
-
// CGEventGetLocation returns LOGICAL coordinates (correct for JS layer)
|
|
997
|
-
// Keep logical coordinates - transformation happens in JS layer
|
|
998
|
-
logicalLocation = rawLocation;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1244
|
+
// CGEventGetLocation already returns logical coordinates; additional scaling happens in JS layer.
|
|
1245
|
+
|
|
1001
1246
|
NSString *cursorType = getCursorType();
|
|
1002
1247
|
|
|
1003
1248
|
// Mouse button state'ini kontrol et
|