node-mac-recorder 2.17.21 → 2.18.0

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.
@@ -5,7 +5,6 @@
5
5
  #import <ApplicationServices/ApplicationServices.h>
6
6
  #import <Carbon/Carbon.h>
7
7
  #import <Accessibility/Accessibility.h>
8
- #import <CoreServices/CoreServices.h>
9
8
 
10
9
  // Global state for cursor tracking
11
10
  static bool g_isCursorTracking = false;
@@ -13,7 +12,7 @@ static CFMachPortRef g_eventTap = NULL;
13
12
  static CFRunLoopSourceRef g_runLoopSource = NULL;
14
13
  static NSDate *g_trackingStartTime = nil;
15
14
  static NSString *g_outputPath = nil;
16
- static dispatch_source_t g_cursorTimer = nil;
15
+ static NSTimer *g_cursorTimer = nil;
17
16
  static int g_debugCallbackCount = 0;
18
17
  static NSFileHandle *g_fileHandle = nil;
19
18
  static bool g_isFirstWrite = true;
@@ -23,78 +22,6 @@ void cursorTimerCallback();
23
22
  void writeToFile(NSDictionary *cursorData);
24
23
  NSDictionary* getDisplayScalingInfo(CGPoint globalPoint);
25
24
 
26
- // Core Graphics cursor detection as ultimate fallback
27
- NSString* detectCursorTypeFromCoreGraphics() {
28
- @try {
29
- // Use Core Graphics to get cursor information
30
- // This is more reliable than NSCursor in some cases
31
-
32
- // Get the current cursor seed (changes when cursor changes)
33
- static uint32_t lastCursorSeed = 0;
34
- // Note: CGEventSourceGetUserData requires actual source, simplified for now
35
- static uint32_t currentSeed = 0;
36
- currentSeed++; // Simple increment for basic tracking
37
-
38
- // If cursor hasn't changed, use cached result for performance
39
- static NSString *cachedCGCursorType = nil;
40
- if (currentSeed == lastCursorSeed && cachedCGCursorType) {
41
- return cachedCGCursorType;
42
- }
43
-
44
- lastCursorSeed = currentSeed;
45
-
46
- // Try to detect cursor based on system state
47
- // Check if we're in a text editing context by looking at input methods
48
- TISInputSourceRef currentInputSource = TISCopyCurrentKeyboardInputSource();
49
- if (currentInputSource) {
50
- // If input source is active, likely in text field
51
- CFStringRef inputSourceID = (CFStringRef)TISGetInputSourceProperty(currentInputSource, kTISPropertyInputSourceID);
52
- if (inputSourceID) {
53
- // Text input is active, likely text cursor
54
- cachedCGCursorType = @"text";
55
- CFRelease(currentInputSource);
56
- return cachedCGCursorType;
57
- }
58
- CFRelease(currentInputSource);
59
- }
60
-
61
- // Check current application and window state for context
62
- NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
63
- if (frontApp) {
64
- NSString *bundleId = frontApp.bundleIdentifier;
65
-
66
- // Web browsers often have custom cursor handling
67
- if ([bundleId containsString:@"chrome"] ||
68
- [bundleId containsString:@"firefox"] ||
69
- [bundleId containsString:@"safari"] ||
70
- [bundleId containsString:@"webkit"]) {
71
-
72
- // In browsers, use heuristics based on recent patterns
73
- // This is speculative but can improve accuracy
74
- return nil; // Let other methods handle browser cursors
75
- }
76
-
77
- // Text editors typically show text cursors
78
- if ([bundleId containsString:@"textedit"] ||
79
- [bundleId containsString:@"xcode"] ||
80
- [bundleId containsString:@"vscode"] ||
81
- [bundleId containsString:@"sublime"] ||
82
- [bundleId containsString:@"atom"]) {
83
-
84
- cachedCGCursorType = @"text";
85
- return cachedCGCursorType;
86
- }
87
- }
88
-
89
- // No specific cursor detected
90
- cachedCGCursorType = nil;
91
- return nil;
92
-
93
- } @catch (NSException *exception) {
94
- return nil;
95
- }
96
- }
97
-
98
25
  // Timer helper class
99
26
  @interface CursorTimerTarget : NSObject
100
27
  - (void)timerCallback:(NSTimer *)timer;
@@ -108,17 +35,9 @@ NSString* detectCursorTypeFromCoreGraphics() {
108
35
 
109
36
  static CursorTimerTarget *g_timerTarget = nil;
110
37
 
111
- // Enhanced cursor state tracking with stability
38
+ // Global cursor state tracking
112
39
  static NSString *g_lastDetectedCursorType = nil;
113
- static NSString *g_stableCursorType = @"default";
114
40
  static int g_cursorTypeCounter = 0;
115
- static NSTimeInterval g_lastCursorCheckTime = 0;
116
- static int g_sameCursorDetectionCount = 0;
117
- static NSString *g_pendingCursorType = nil;
118
-
119
- // Enhanced cursor stability constants - ultra-responsive for real-time cursor tracking
120
- static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.005; // 5ms (ultra fast response)
121
- static const int CURSOR_CONFIRMATION_COUNT = 0; // No confirmation needed - immediate response
122
41
 
123
42
  // Mouse button state tracking
124
43
  static bool g_leftMouseDown = false;
@@ -130,204 +49,38 @@ static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEv
130
49
  return event;
131
50
  }
132
51
 
133
- // Enhanced cursor type detection with multiple fallbacks and better image analysis
134
- NSString* detectCursorTypeFromNSCursor() {
135
- @try {
136
- // Try multiple methods to get current cursor since currentSystemCursor often fails
137
- NSCursor *currentCursor = nil;
138
-
139
- // Method 1: currentSystemCursor (often returns nil but try anyway)
140
- currentCursor = [NSCursor currentSystemCursor];
141
-
142
- // Method 2: If nil, try getting cursor from current window
143
- if (!currentCursor) {
144
- NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
145
- if (keyWindow) {
146
- // Try to get cursor from window's current event
147
- NSEvent *currentEvent = [[NSApplication sharedApplication] currentEvent];
148
- if (currentEvent) {
149
- // This is a fallback - may not always work but worth trying
150
- @try {
151
- // Get window at mouse position
152
- NSPoint mouseLocation = [NSEvent mouseLocation];
153
- // This approach has limited success, fallback to image analysis
154
- } @catch (NSException *e) {
155
- // Ignore and continue
156
- }
157
- }
158
- }
159
- }
160
-
161
- if (!currentCursor) {
162
- return nil; // No cursor available, use contextual detection
163
- }
164
-
165
- // Compare with known system cursors using identity comparison first
166
- if (currentCursor == [NSCursor arrowCursor]) {
167
- return @"default";
168
- } else if (currentCursor == [NSCursor IBeamCursor]) {
169
- return @"text";
170
- } else if (currentCursor == [NSCursor pointingHandCursor]) {
171
- return @"pointer";
172
- } else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
173
- return @"col-resize";
174
- } else if (currentCursor == [NSCursor resizeUpDownCursor]) {
175
- return @"ns-resize";
176
- } else if (currentCursor == [NSCursor crosshairCursor]) {
177
- return @"crosshair";
178
- } else if (currentCursor == [NSCursor openHandCursor]) {
179
- return @"grab";
180
- } else if (currentCursor == [NSCursor closedHandCursor]) {
181
- return @"grabbing";
182
- } else if (currentCursor == [NSCursor operationNotAllowedCursor]) {
183
- return @"not-allowed";
184
- }
185
-
186
- // Enhanced image analysis for better cursor detection
187
- NSImage *cursorImage = [currentCursor image];
188
- if (cursorImage) {
189
- NSSize imageSize = [cursorImage size];
190
- NSPoint hotSpot = [currentCursor hotSpot];
191
-
192
- // Get image data for more detailed analysis
193
- CGImageRef cgImage = [cursorImage CGImageForProposedRect:NULL context:nil hints:nil];
194
- if (cgImage) {
195
- size_t width = CGImageGetWidth(cgImage);
196
- size_t height = CGImageGetHeight(cgImage);
197
-
198
- // More precise size-based detection
199
- if (width > 0 && height > 0) {
200
- float aspectRatio = (float)width / (float)height;
201
-
202
- // Text cursor: Very narrow (I-beam)
203
- if (width <= 3 && height >= 15) {
204
- return @"text";
205
- }
206
-
207
- // Hand cursors: Usually square-ish with specific sizes
208
- if (width >= 12 && width <= 20 && height >= 12 && height <= 20 && aspectRatio > 0.8 && aspectRatio < 1.2) {
209
- // Try to distinguish hand types by hotspot
210
- if (hotSpot.x > width * 0.6) { // hotspot towards right
211
- return @"pointer";
212
- } else if (hotSpot.x < width * 0.4) { // hotspot towards left
213
- return @"grab";
214
- }
215
- return @"pointer"; // default to pointer for hand-like cursors
216
- }
217
-
218
- // Resize cursors: Usually larger and directional
219
- if (width >= 10 && height >= 10) {
220
- // Very wide: horizontal resize
221
- if (aspectRatio > 2.0) {
222
- return @"col-resize";
223
- }
224
- // Very tall: vertical resize
225
- else if (aspectRatio < 0.5) {
226
- return @"ns-resize";
227
- }
228
- // Square-ish but large: diagonal resize
229
- else if (width >= 15 && height >= 15 && aspectRatio > 0.7 && aspectRatio < 1.4) {
230
- // Use hotspot to determine diagonal direction
231
- float hotSpotX = hotSpot.x / width;
232
- float hotSpotY = hotSpot.y / height;
233
-
234
- if ((hotSpotX < 0.5 && hotSpotY < 0.5) || (hotSpotX > 0.5 && hotSpotY > 0.5)) {
235
- return @"nwse-resize"; // northwest-southeast
236
- } else {
237
- return @"nesw-resize"; // northeast-southwest
238
- }
239
- }
240
- }
241
-
242
- // Crosshair: Usually symmetric and medium sized
243
- if (width >= 12 && width <= 25 && height >= 12 && height <= 25 &&
244
- aspectRatio > 0.8 && aspectRatio < 1.2 &&
245
- hotSpot.x > width * 0.4 && hotSpot.x < width * 0.6 &&
246
- hotSpot.y > height * 0.4 && hotSpot.y < height * 0.6) {
247
- return @"crosshair";
248
- }
249
- }
250
- }
251
-
252
- // Fallback to original size-based analysis
253
- if (imageSize.width > 10 && imageSize.height > 10) {
254
- // Check for diagonal resize cursors (corner resize)
255
- if (imageSize.width >= 15 && imageSize.height >= 15) {
256
- // These are likely diagonal resize cursors
257
- // hotSpot can help distinguish between nwse and nesw
258
- if (hotSpot.x < imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
259
- return @"nwse-resize"; // northwest-southeast
260
- } else if (hotSpot.x > imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
261
- return @"nesw-resize"; // northeast-southwest
262
- }
263
- }
264
-
265
- // Check for horizontal/vertical resize cursors
266
- if (imageSize.width > imageSize.height + 5) {
267
- return @"col-resize"; // horizontal resize
268
- } else if (imageSize.height > imageSize.width + 5) {
269
- return @"ns-resize"; // vertical resize
270
- }
271
- }
272
-
273
- // Text cursors typically have I-beam shape (narrow width)
274
- if (imageSize.width < 8 && imageSize.height > 15) {
275
- return @"text";
276
- }
277
- }
278
-
279
- // Return nil to indicate we should use contextual detection
280
- return nil;
281
- } @catch (NSException *exception) {
282
- return nil;
283
- }
284
- }
285
-
286
- // Improved cursor type detection with stability and multi-layer approach
52
+ // Cursor type detection helper - sistem genelindeki cursor type'ı al
287
53
  NSString* getCursorType() {
288
54
  @autoreleasepool {
289
55
  g_cursorTypeCounter++;
290
- NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
291
-
56
+
292
57
  @try {
293
- // Layer 1: Fast NSCursor detection (high confidence cases)
294
- NSString *nsCursorType = detectCursorTypeFromNSCursor();
295
-
296
- // Layer 1.5: Core Graphics detection as primary fallback
297
- NSString *cgCursorType = detectCursorTypeFromCoreGraphics();
298
-
299
- // Layer 2: Optimized Accessibility API detection with caching
300
- NSString *contextualCursorType = nil;
301
-
302
- // Get cursor position once and reuse
303
- CGEventRef event = CGEventCreate(NULL);
304
- CGPoint cursorPos = CGEventGetLocation(event);
305
- CFRelease(event);
306
-
307
- // Use cached system-wide reference for better performance
308
- static AXUIElementRef cachedSystemWide = NULL;
309
- if (!cachedSystemWide) {
310
- cachedSystemWide = AXUIElementCreateSystemWide();
311
- }
58
+ // ACCESSIBILITY API BASED CURSOR DETECTION
59
+ // Determine cursor type based on the UI element under the cursor
312
60
 
61
+ CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
62
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
313
63
  AXUIElementRef elementAtPosition = NULL;
314
- AXError error = AXUIElementCopyElementAtPosition(cachedSystemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
64
+ AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
65
+
66
+ NSString *cursorType = @"default"; // Default fallback
315
67
 
316
68
  if (error == kAXErrorSuccess && elementAtPosition) {
317
- // Fast role detection with immediate processing
318
69
  CFStringRef role = NULL;
319
70
  error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
320
71
 
321
72
  if (error == kAXErrorSuccess && role) {
322
73
  NSString *elementRole = (__bridge_transfer NSString*)role;
74
+ NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
323
75
 
324
- // TEXT CURSORS - high priority
76
+ // TEXT CURSORS
325
77
  if ([elementRole isEqualToString:@"AXTextField"] ||
326
78
  [elementRole isEqualToString:@"AXTextArea"] ||
79
+ [elementRole isEqualToString:@"AXStaticText"] ||
327
80
  [elementRole isEqualToString:@"AXSearchField"]) {
328
- contextualCursorType = @"text";
81
+ cursorType = @"text";
329
82
  }
330
- // POINTER CURSORS - interactive elements with broader detection
83
+ // POINTER CURSORS (clickable elements)
331
84
  else if ([elementRole isEqualToString:@"AXLink"] ||
332
85
  [elementRole isEqualToString:@"AXButton"] ||
333
86
  [elementRole isEqualToString:@"AXMenuItem"] ||
@@ -335,182 +88,216 @@ NSString* getCursorType() {
335
88
  [elementRole isEqualToString:@"AXCheckBox"] ||
336
89
  [elementRole isEqualToString:@"AXPopUpButton"] ||
337
90
  [elementRole isEqualToString:@"AXTab"]) {
338
- contextualCursorType = @"pointer";
339
-
340
- // Also check subroles for links and buttons
341
- CFStringRef subrole = NULL;
342
- AXError subroleError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
343
- if (subroleError == kAXErrorSuccess && subrole) {
344
- NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
345
- if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
346
- [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
347
- [elementSubrole isEqualToString:@"AXZoomButton"] ||
348
- [elementSubrole isEqualToString:@"AXToolbarButton"]) {
349
- contextualCursorType = @"pointer";
350
- }
351
- }
91
+ cursorType = @"pointer";
352
92
  }
353
- // WEB ELEMENTS - for web links that might not show as AXLink
354
- else if ([elementRole isEqualToString:@"AXGroup"] ||
355
- [elementRole isEqualToString:@"AXStaticText"]) {
356
- // Check if it's clickable/has action
357
- CFArrayRef actions = NULL;
358
- AXError actionsError = AXUIElementCopyActionNames(elementAtPosition, &actions);
359
- if (actionsError == kAXErrorSuccess && actions) {
360
- CFIndex actionCount = CFArrayGetCount(actions);
361
- for (CFIndex i = 0; i < actionCount; i++) {
362
- CFStringRef action = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
363
- NSString *actionStr = (__bridge NSString*)action;
364
- if ([actionStr isEqualToString:@"AXPress"] ||
365
- [actionStr isEqualToString:@"AXShowMenu"]) {
366
- contextualCursorType = @"pointer";
367
- break;
368
- }
369
- }
370
- CFRelease(actions);
93
+ // GRAB CURSORS (draggable elements)
94
+ else if ([elementRole isEqualToString:@"AXImage"] ||
95
+ [elementRole isEqualToString:@"AXGroup"]) {
96
+ // Check if element is draggable
97
+ CFBooleanRef draggable = NULL;
98
+ error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
99
+ if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
100
+ cursorType = @"grab";
101
+ } else {
102
+ cursorType = @"default";
371
103
  }
372
104
  }
373
- // WINDOW BORDER RESIZE - critical for resize detection
374
- else if ([elementRole isEqualToString:@"AXWindow"]) {
375
- CFTypeRef position = NULL;
376
- CFTypeRef size = NULL;
377
- AXError posErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
378
- AXError sizeErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
379
-
380
- if (posErr == kAXErrorSuccess && sizeErr == kAXErrorSuccess && position && size) {
381
- CGPoint windowPos;
382
- CGSize windowSize;
383
- AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
384
- AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
385
-
386
- CGFloat x = cursorPos.x - windowPos.x;
387
- CGFloat y = cursorPos.y - windowPos.y;
388
- CGFloat w = windowSize.width;
389
- CGFloat h = windowSize.height;
390
- CGFloat edge = 5.0; // 5px edge detection
391
-
392
- // Corner resize detection
393
- if (x <= edge && y <= edge) {
394
- contextualCursorType = @"nwse-resize";
395
- }
396
- else if (x >= w-edge && y <= edge) {
397
- contextualCursorType = @"nesw-resize";
398
- }
399
- else if (x <= edge && y >= h-edge) {
400
- contextualCursorType = @"nesw-resize";
401
- }
402
- else if (x >= w-edge && y >= h-edge) {
403
- contextualCursorType = @"nwse-resize";
404
- }
405
- // Edge resize detection
406
- else if (x <= edge || x >= w-edge) {
407
- contextualCursorType = @"col-resize";
408
- }
409
- else if (y <= edge || y >= h-edge) {
410
- contextualCursorType = @"ns-resize";
411
- }
412
-
413
- if (position) CFRelease(position);
414
- if (size) CFRelease(size);
415
- }
105
+ // PROGRESS CURSORS (loading/busy elements)
106
+ else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
107
+ [elementRole isEqualToString:@"AXBusyIndicator"]) {
108
+ cursorType = @"progress";
416
109
  }
417
- // SPLITTER RESIZE
110
+ // HELP CURSORS (help buttons/tooltips)
111
+ else if ([elementRole isEqualToString:@"AXHelpTag"] ||
112
+ [elementRole isEqualToString:@"AXTooltip"]) {
113
+ cursorType = @"help";
114
+ }
115
+ // RESIZE CURSORS - sadece AXSplitter için
418
116
  else if ([elementRole isEqualToString:@"AXSplitter"]) {
117
+ // Get splitter orientation to determine resize direction
419
118
  CFStringRef orientation = NULL;
420
119
  error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
421
120
  if (error == kAXErrorSuccess && orientation) {
422
121
  NSString *orientationStr = (__bridge_transfer NSString*)orientation;
423
122
  if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
424
- contextualCursorType = @"ns-resize";
123
+ cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
425
124
  } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
426
- contextualCursorType = @"col-resize";
125
+ cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
126
+ } else {
127
+ cursorType = @"default"; // Bilinmeyen orientation
427
128
  }
129
+ } else {
130
+ cursorType = @"default"; // Orientation alınamazsa default
428
131
  }
429
132
  }
430
- // PROGRESS INDICATORS
431
- else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
432
- contextualCursorType = @"progress";
133
+ // SCROLL CURSORS - hep default olsun, all-scroll görünmesin
134
+ else if ([elementRole isEqualToString:@"AXScrollBar"]) {
135
+ cursorType = @"default"; // ScrollBar'lar için de default
136
+ }
137
+ // AXScrollArea - hep default
138
+ else if ([elementRole isEqualToString:@"AXScrollArea"]) {
139
+ cursorType = @"default"; // ScrollArea her zaman default
140
+ }
141
+ // CROSSHAIR CURSORS (drawing/selection tools)
142
+ else if ([elementRole isEqualToString:@"AXCanvas"] ||
143
+ [elementRole isEqualToString:@"AXDrawingArea"]) {
144
+ cursorType = @"crosshair";
145
+ }
146
+ // ZOOM CURSORS (zoom controls)
147
+ else if ([elementRole isEqualToString:@"AXZoomButton"]) {
148
+ cursorType = @"zoom-in";
433
149
  }
434
- // DEFAULT AREAS - explicitly set default for known non-interactive elements
435
- else if ([elementRole isEqualToString:@"AXApplication"] ||
436
- [elementRole isEqualToString:@"AXWindow"] ||
437
- [elementRole isEqualToString:@"AXScrollArea"] ||
438
- [elementRole isEqualToString:@"AXScrollBar"] ||
439
- [elementRole isEqualToString:@"AXStaticText"] ||
150
+ // NOT-ALLOWED CURSORS (disabled elements)
151
+ else if ([elementRole isEqualToString:@"AXStaticText"] ||
440
152
  [elementRole isEqualToString:@"AXGroup"]) {
441
- // These are typically non-interactive - should be default cursor
442
- contextualCursorType = @"default";
153
+ // Check if element is disabled/readonly
154
+ CFBooleanRef enabled = NULL;
155
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
156
+ if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
157
+ cursorType = @"not-allowed";
158
+ }
159
+ }
160
+ // WINDOW BORDER RESIZE - sadece pencere kenarlarında
161
+ else if ([elementRole isEqualToString:@"AXWindow"]) {
162
+ // Check window attributes to see if it's resizable
163
+ CFBooleanRef resizable = NULL;
164
+ AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
165
+
166
+ // Sadece resize edilebilir pencereler için cursor değişimi
167
+ if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
168
+ CFTypeRef position = NULL;
169
+ CFTypeRef size = NULL;
170
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
171
+ AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
172
+
173
+ if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
174
+ CGPoint windowPos;
175
+ CGSize windowSize;
176
+ AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
177
+ AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
178
+
179
+ CGFloat x = cursorPos.x - windowPos.x;
180
+ CGFloat y = cursorPos.y - windowPos.y;
181
+ CGFloat w = windowSize.width;
182
+ CGFloat h = windowSize.height;
183
+ CGFloat edge = 3.0; // Daha küçük edge detection (3px)
184
+
185
+ // Sadece çok kenar köşelerde resize cursor'ı göster
186
+ BOOL isOnBorder = NO;
187
+
188
+ // Corner resize detection - çok dar alanda, doğru açılar
189
+ if (x <= edge && y <= edge) {
190
+ cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
191
+ isOnBorder = YES;
192
+ }
193
+ else if (x >= w-edge && y <= edge) {
194
+ cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
195
+ isOnBorder = YES;
196
+ }
197
+ else if (x <= edge && y >= h-edge) {
198
+ cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
199
+ isOnBorder = YES;
200
+ }
201
+ else if (x >= w-edge && y >= h-edge) {
202
+ cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
203
+ isOnBorder = YES;
204
+ }
205
+ // Edge resize detection - sadece çok kenarlarda
206
+ else if (x <= edge && y > edge && y < h-edge) {
207
+ cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
208
+ isOnBorder = YES;
209
+ }
210
+ else if (x >= w-edge && y > edge && y < h-edge) {
211
+ cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
212
+ isOnBorder = YES;
213
+ }
214
+ else if (y <= edge && x > edge && x < w-edge) {
215
+ cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
216
+ isOnBorder = YES;
217
+ }
218
+ else if (y >= h-edge && x > edge && x < w-edge) {
219
+ cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
220
+ isOnBorder = YES;
221
+ }
222
+
223
+ // Eğer border'da değilse default
224
+ if (!isOnBorder) {
225
+ cursorType = @"default";
226
+ }
227
+
228
+ if (position) CFRelease(position);
229
+ if (size) CFRelease(size);
230
+ } else {
231
+ cursorType = @"default";
232
+ }
233
+ } else {
234
+ cursorType = @"default";
235
+ }
443
236
  }
444
- // OTHER ELEMENTS - let NSCursor handle or default
237
+ // HER DURUM İÇİN DEFAULT FALLBACK
445
238
  else {
446
- contextualCursorType = nil;
239
+ // Bilinmeyen elementler için her zaman default
240
+ cursorType = @"default";
447
241
  }
448
- }
449
- CFRelease(elementAtPosition);
450
- }
451
242
 
452
- // Layer 3: Enhanced multi-layer priority detection
453
- NSString *detectedCursorType = @"default";
243
+ // Check subroles for additional context
244
+ CFStringRef subrole = NULL;
245
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
246
+ if (error == kAXErrorSuccess && subrole) {
247
+ NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
248
+ NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
249
+
250
+ // Subrole override'ları - sadece çok spesifik durumlar için
251
+ if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
252
+ [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
253
+ [elementSubrole isEqualToString:@"AXZoomButton"] ||
254
+ [elementSubrole isEqualToString:@"AXToolbarButton"]) {
255
+ cursorType = @"pointer";
256
+ }
257
+ // Copy/alias subroles - sadece bu durumlar için override
258
+ else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
259
+ [elementSubrole isEqualToString:@"AXDropTarget"]) {
260
+ cursorType = @"copy";
261
+ }
262
+ // Alias/shortcut subroles
263
+ else if ([elementSubrole isEqualToString:@"AXAlias"] ||
264
+ [elementSubrole isEqualToString:@"AXShortcut"]) {
265
+ cursorType = @"alias";
266
+ }
267
+ // Grabbing state (being dragged) - sadece gerçek drag sırasında
268
+ else if ([elementSubrole isEqualToString:@"AXDragging"] ||
269
+ [elementSubrole isEqualToString:@"AXMoving"]) {
270
+ cursorType = @"grabbing";
271
+ }
272
+ // Zoom controls - sadece spesifik zoom butonları için
273
+ else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
274
+ cursorType = @"zoom-in";
275
+ }
276
+ else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
277
+ cursorType = @"zoom-out";
278
+ }
279
+ // Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
280
+ }
281
+ }
454
282
 
455
- // Priority logic - multiple fallbacks for maximum accuracy:
456
- // 1. If contextual (Accessibility) gives ANY specific cursor, use it (highest priority)
457
- if (contextualCursorType != nil) {
458
- detectedCursorType = contextualCursorType;
459
- }
460
- // 2. If NSCursor gives reliable results, use them
461
- else if (nsCursorType != nil &&
462
- ([nsCursorType isEqualToString:@"text"] ||
463
- [nsCursorType isEqualToString:@"pointer"] ||
464
- [nsCursorType hasSuffix:@"resize"])) {
465
- // Trust NSCursor for these specific types
466
- detectedCursorType = nsCursorType;
467
- }
468
- // 3. Core Graphics context-based detection
469
- else if (cgCursorType != nil) {
470
- detectedCursorType = cgCursorType;
471
- }
472
- // 4. If NSCursor gives any result, use it as last resort
473
- else if (nsCursorType != nil) {
474
- detectedCursorType = nsCursorType;
475
- }
476
- // 5. Everything else defaults to default
477
- else {
478
- detectedCursorType = @"default";
283
+ CFRelease(elementAtPosition);
479
284
  }
480
285
 
481
- // Layer 4: Ultra-responsive detection with minimal stabilization
482
- NSString *finalCursorType = detectedCursorType;
483
-
484
- // Ultra-fast response - immediate updates for all cursor types
485
- if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
486
- // Immediate update for all cursor types (no confirmation needed)
487
- g_stableCursorType = detectedCursorType;
488
- finalCursorType = detectedCursorType;
489
- g_lastCursorCheckTime = currentTime;
490
-
491
- // Reset pending state since we're updating immediately
492
- g_pendingCursorType = nil;
493
- g_sameCursorDetectionCount = 0;
494
- } else {
495
- // Still use immediate update for ultra-responsiveness
496
- // Only apply minimal delay for very rapid changes
497
- finalCursorType = detectedCursorType;
286
+ if (systemWide) {
287
+ CFRelease(systemWide);
498
288
  }
499
289
 
500
- // Final fallback validation
501
- if (!finalCursorType || [finalCursorType length] == 0) {
502
- finalCursorType = @"default";
290
+ // Son güvence - eğer cursorType hala nil veya geçersizse default'a çevir
291
+ if (!cursorType || [cursorType length] == 0) {
292
+ cursorType = @"default";
503
293
  }
504
294
 
505
- // Debug logging for stability tracking
506
- NSLog(@"🎯 CURSOR DETECTION - NSCursor: %@, Contextual: %@, Stable: %@, Count: %d",
507
- nsCursorType, contextualCursorType, finalCursorType, g_sameCursorDetectionCount);
508
-
509
- return finalCursorType;
510
-
295
+ NSLog(@"🎯 FINAL CURSOR TYPE: %@", cursorType);
296
+ return cursorType;
297
+
511
298
  } @catch (NSException *exception) {
512
299
  NSLog(@"Error in getCursorType: %@", exception);
513
- return g_stableCursorType ?: @"default";
300
+ return @"default";
514
301
  }
515
302
  }
516
303
  }
@@ -601,15 +388,13 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
601
388
  }
602
389
 
603
390
  // Cursor data oluştur
604
- // Cursor data oluştur - global koordinat sistemi kullan
605
391
  NSDictionary *cursorInfo = @{
606
392
  @"x": @((int)location.x),
607
393
  @"y": @((int)location.y),
608
394
  @"timestamp": @(timestamp),
609
395
  @"unixTimeMs": @(unixTimeMs),
610
396
  @"cursorType": cursorType,
611
- @"type": eventType,
612
- @"coordinateSystem": @"global"
397
+ @"type": eventType
613
398
  };
614
399
 
615
400
  // Direkt dosyaya yaz
@@ -623,16 +408,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
623
408
  void cursorTimerCallback() {
624
409
  @autoreleasepool {
625
410
  g_debugCallbackCount++; // Timer callback çağrıldığını say
626
-
627
- static int callCount = 0;
628
- callCount++;
629
- if (callCount <= 3) {
630
- NSLog(@"🔄 Timer callback #%d called", callCount);
631
- }
632
-
411
+
633
412
  if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
634
- NSLog(@"❌ Timer callback stopped - tracking:%d, startTime:%@, fileHandle:%@",
635
- g_isCursorTracking, g_trackingStartTime ? @"OK" : @"nil", g_fileHandle ? @"OK" : @"nil");
636
413
  return;
637
414
  }
638
415
 
@@ -661,15 +438,13 @@ void cursorTimerCallback() {
661
438
  NSString *cursorType = getCursorType();
662
439
 
663
440
  // Cursor data oluştur
664
- // Cursor data oluştur - global koordinat sistemi kullan
665
441
  NSDictionary *cursorInfo = @{
666
442
  @"x": @((int)location.x),
667
443
  @"y": @((int)location.y),
668
444
  @"timestamp": @(timestamp),
669
445
  @"unixTimeMs": @(unixTimeMs),
670
446
  @"cursorType": cursorType,
671
- @"type": @"move",
672
- @"coordinateSystem": @"global"
447
+ @"type": @"move"
673
448
  };
674
449
 
675
450
  // Direkt dosyaya yaz
@@ -681,14 +456,16 @@ void cursorTimerCallback() {
681
456
  void cleanupCursorTracking() {
682
457
  g_isCursorTracking = false;
683
458
 
684
- // GCD Timer temizle
459
+ // Timer temizle
685
460
  if (g_cursorTimer) {
686
- dispatch_source_cancel(g_cursorTimer);
687
- dispatch_release(g_cursorTimer);
461
+ [g_cursorTimer invalidate];
688
462
  g_cursorTimer = nil;
689
463
  }
690
464
 
691
- // Timer target artık kullanılmıyor (GCD kullanıyoruz)
465
+ if (g_timerTarget) {
466
+ [g_timerTarget autorelease];
467
+ g_timerTarget = nil;
468
+ }
692
469
 
693
470
  // Dosyayı önce kapat (en önemli işlem)
694
471
  if (g_fileHandle) {
@@ -724,11 +501,7 @@ void cleanupCursorTracking() {
724
501
  g_outputPath = nil;
725
502
  g_debugCallbackCount = 0;
726
503
  g_lastDetectedCursorType = nil;
727
- g_stableCursorType = @"default";
728
504
  g_cursorTypeCounter = 0;
729
- g_lastCursorCheckTime = 0;
730
- g_sameCursorDetectionCount = 0;
731
- g_pendingCursorType = nil;
732
505
  g_isFirstWrite = true;
733
506
  }
734
507
 
@@ -794,30 +567,19 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
794
567
  CGEventTapEnable(g_eventTap, true);
795
568
  }
796
569
 
797
- // GCD Timer kullan (Node.js ile uyumlu)
798
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
799
- g_cursorTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
800
-
801
- if (g_cursorTimer) {
802
- dispatch_source_set_timer(g_cursorTimer,
803
- dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC), // Start after 50ms
804
- 50 * NSEC_PER_MSEC, // Repeat every 50ms (20 FPS)
805
- 10 * NSEC_PER_MSEC); // Leeway of 10ms
806
-
807
- dispatch_source_set_event_handler(g_cursorTimer, ^{
808
- cursorTimerCallback();
809
- });
810
-
811
- dispatch_resume(g_cursorTimer);
812
- }
813
-
570
+ // NSTimer kullan (main thread'de çalışır)
571
+ g_timerTarget = [[CursorTimerTarget alloc] init];
572
+
573
+ g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
574
+ target:g_timerTarget
575
+ selector:@selector(timerCallback:)
576
+ userInfo:nil
577
+ repeats:YES];
578
+
579
+ // Main run loop'a ekle
580
+ [[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
581
+
814
582
  g_isCursorTracking = true;
815
-
816
- NSLog(@"🎯 Native cursor tracking started with timer and event tap");
817
- NSLog(@"🎯 File handle: %@", g_fileHandle ? @"OK" : @"FAILED");
818
- NSLog(@"🎯 Timer: %@", g_cursorTimer ? @"OK" : @"FAILED");
819
- NSLog(@"🎯 Event tap: %@", g_eventTap ? @"OK" : @"FAILED");
820
-
821
583
  return Napi::Boolean::New(env, true);
822
584
 
823
585
  } @catch (NSException *exception) {