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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cursor_tracker.mm +471 -226
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.20.1",
3
+ "version": "2.20.3",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- NSString *elementRole = (__bridge_transfer NSString*)role;
262
+ elementRole = CopyAndReleaseCFString(role);
263
+ }
264
+
265
+ if (elementRole) {
72
266
  NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
267
+ }
73
268
 
74
- // TEXT CURSORS
75
- if ([elementRole isEqualToString:@"AXTextField"] ||
76
- [elementRole isEqualToString:@"AXTextArea"] ||
77
- [elementRole isEqualToString:@"AXStaticText"] ||
78
- [elementRole isEqualToString:@"AXSearchField"]) {
79
- cursorType = @"text";
80
- }
81
- // POINTER CURSORS (clickable elements)
82
- else if ([elementRole isEqualToString:@"AXLink"] ||
83
- [elementRole isEqualToString:@"AXButton"] ||
84
- [elementRole isEqualToString:@"AXMenuItem"] ||
85
- [elementRole isEqualToString:@"AXRadioButton"] ||
86
- [elementRole isEqualToString:@"AXCheckBox"] ||
87
- [elementRole isEqualToString:@"AXPopUpButton"] ||
88
- [elementRole isEqualToString:@"AXTab"]) {
89
- cursorType = @"pointer";
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
- // GRAB CURSORS (draggable elements)
92
- else if ([elementRole isEqualToString:@"AXImage"] ||
93
- [elementRole isEqualToString:@"AXGroup"]) {
94
- // Check if element is draggable
95
- CFBooleanRef draggable = NULL;
96
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
97
- if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
98
- cursorType = @"grab";
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
- // PROGRESS CURSORS (loading/busy elements)
104
- else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
105
- [elementRole isEqualToString:@"AXBusyIndicator"]) {
106
- cursorType = @"progress";
107
- }
108
- // HELP CURSORS (help buttons/tooltips)
109
- else if ([elementRole isEqualToString:@"AXHelpTag"] ||
110
- [elementRole isEqualToString:@"AXTooltip"]) {
111
- cursorType = @"help";
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
- // RESIZE CURSORS - sadece AXSplitter için
114
- else if ([elementRole isEqualToString:@"AXSplitter"]) {
115
- // Get splitter orientation to determine resize direction
116
- CFStringRef orientation = NULL;
117
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
118
- if (error == kAXErrorSuccess && orientation) {
119
- NSString *orientationStr = (__bridge_transfer NSString*)orientation;
120
- if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
121
- cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
122
- } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
123
- cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
124
- } else {
125
- cursorType = @"default"; // Bilinmeyen orientation
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"; // Orientation alınamazsa default
424
+ cursorType = @"default";
129
425
  }
426
+ } else {
427
+ cursorType = @"default";
130
428
  }
131
- // SCROLL CURSORS - hep default olsun, all-scroll görünmesin
132
- else if ([elementRole isEqualToString:@"AXScrollBar"]) {
133
- cursorType = @"default"; // ScrollBar'lar için de default
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
- // AXScrollArea - hep default
136
- else if ([elementRole isEqualToString:@"AXScrollArea"]) {
137
- cursorType = @"default"; // ScrollArea her zaman default
455
+ // Alias/shortcut subroles
456
+ else if ([elementSubrole isEqualToString:@"AXAlias"] ||
457
+ [elementSubrole isEqualToString:@"AXShortcut"]) {
458
+ cursorType = @"alias";
138
459
  }
139
- // CROSSHAIR CURSORS (drawing/selection tools)
140
- else if ([elementRole isEqualToString:@"AXCanvas"] ||
141
- [elementRole isEqualToString:@"AXDrawingArea"]) {
142
- cursorType = @"crosshair";
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
- // ZOOM CURSORS (zoom controls)
145
- else if ([elementRole isEqualToString:@"AXZoomButton"]) {
465
+ // Zoom controls - sadece spesifik zoom butonları için
466
+ else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
146
467
  cursorType = @"zoom-in";
147
468
  }
148
- // NOT-ALLOWED CURSORS (disabled elements)
149
- else if ([elementRole isEqualToString:@"AXStaticText"] ||
150
- [elementRole isEqualToString:@"AXGroup"]) {
151
- // Check if element is disabled/readonly
152
- CFBooleanRef enabled = NULL;
153
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
154
- if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
155
- cursorType = @"not-allowed";
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
- // WINDOW BORDER RESIZE - sadece pencere kenarlarında
159
- else if ([elementRole isEqualToString:@"AXWindow"]) {
160
- // Check window attributes to see if it's resizable
161
- CFBooleanRef resizable = NULL;
162
- AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
163
-
164
- // Sadece resize edilebilir pencereler için cursor değişimi
165
- if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
166
- CFTypeRef position = NULL;
167
- CFTypeRef size = NULL;
168
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
169
- AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
170
-
171
- if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
172
- CGPoint windowPos;
173
- CGSize windowSize;
174
- AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
175
- AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
176
-
177
- CGFloat x = cursorPos.x - windowPos.x;
178
- CGFloat y = cursorPos.y - windowPos.y;
179
- CGFloat w = windowSize.width;
180
- CGFloat h = windowSize.height;
181
- CGFloat edge = 3.0; // Daha küçük edge detection (3px)
182
-
183
- // Sadece çok kenar köşelerde resize cursor'ı göster
184
- BOOL isOnBorder = NO;
185
-
186
- // Corner resize detection - çok dar alanda, doğru açılar
187
- if (x <= edge && y <= edge) {
188
- cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
189
- isOnBorder = YES;
190
- }
191
- else if (x >= w-edge && y <= edge) {
192
- cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
193
- isOnBorder = YES;
194
- }
195
- else if (x <= edge && y >= h-edge) {
196
- cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
197
- isOnBorder = YES;
198
- }
199
- else if (x >= w-edge && y >= h-edge) {
200
- cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
201
- isOnBorder = YES;
202
- }
203
- // Edge resize detection - sadece çok kenarlarda
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
- } else {
232
- cursorType = @"default";
539
+ CFRelease(deepElement);
233
540
  }
234
541
  }
235
- // HER DURUM İÇİN DEFAULT FALLBACK
236
- else {
237
- // Bilinmeyen elementler için her zaman default
238
- cursorType = @"default";
239
- }
542
+ }
240
543
 
241
- // Check subroles for additional context
242
- CFStringRef subrole = NULL;
243
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
244
- if (error == kAXErrorSuccess && subrole) {
245
- NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
246
- NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
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
- // Copy/alias subroles - sadece bu durumlar için override
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
- // Apply DPR scaling correction for Retina displays
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
- // Apply DPR scaling correction for Retina displays
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
- if (scalingInfo) {
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