node-mac-recorder 2.17.5 → 2.17.7

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 +203 -181
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.17.5",
3
+ "version": "2.17.7",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -55,217 +55,239 @@ NSString* getCursorType() {
55
55
  g_cursorTypeCounter++;
56
56
 
57
57
  @try {
58
- // Get current cursor info
59
- NSCursor *currentCursor = [NSCursor currentSystemCursor];
60
- NSString *cursorType = @"default";
61
-
62
- // Get cursor image info
63
- NSImage *cursorImage = [currentCursor image];
64
- NSPoint hotSpot = [currentCursor hotSpot];
65
- NSSize imageSize = [cursorImage size];
66
-
67
- // DEBUG: Log cursor details
68
- NSLog(@"🖱️ CURSOR DEBUG: size=(%.0f,%.0f), hotspot=(%.0f,%.0f)",
69
- imageSize.width, imageSize.height, hotSpot.x, hotSpot.y);
70
-
71
- // Check cursor type by comparing with standard cursors
72
- if ([currentCursor isEqual:[NSCursor pointingHandCursor]] ||
73
- (hotSpot.x >= 5 && hotSpot.x <= 7 && hotSpot.y >= 0 && hotSpot.y <= 4) ||
74
- (hotSpot.x >= 12 && hotSpot.x <= 14 && hotSpot.y >= 7 && hotSpot.y <= 9)) {
75
- return @"pointer";
76
- } else if ([currentCursor isEqual:[NSCursor IBeamCursor]] ||
77
- (hotSpot.x >= 3 && hotSpot.x <= 5 && hotSpot.y >= 8 && hotSpot.y <= 10 &&
78
- imageSize.width <= 10 && imageSize.height >= 16)) {
79
- return @"text";
80
- } else if ([currentCursor isEqual:[NSCursor resizeLeftRightCursor]]) {
81
- return @"col-resize";
82
- } else if ([currentCursor isEqual:[NSCursor resizeUpDownCursor]]) {
83
- return @"ns-resize";
84
- } else if ([currentCursor isEqual:[NSCursor openHandCursor]]) {
85
- return @"grab";
86
- } else if ([currentCursor isEqual:[NSCursor closedHandCursor]]) {
87
- return @"grabbing";
88
- } else if ([currentCursor isEqual:[NSCursor crosshairCursor]]) {
89
- return @"crosshair";
90
- } else if ([currentCursor isEqual:[NSCursor disappearingItemCursor]]) {
91
- return @"alias";
92
- } else if ([currentCursor isEqual:[NSCursor dragCopyCursor]]) {
93
- return @"copy";
94
- } else if ([currentCursor isEqual:[NSCursor operationNotAllowedCursor]]) {
95
- return @"not-allowed";
96
- } else if ([currentCursor isEqual:[NSCursor contextualMenuCursor]]) {
97
- return @"help";
98
- }
99
-
100
- // Additional detection based on image characteristics
101
- if (imageSize.width > 0 && imageSize.height > 0) {
102
- NSLog(@"🖱️ Checking cursor: size=(%.0f,%.0f), hotspot=(%.0f,%.0f)",
103
- imageSize.width, imageSize.height, hotSpot.x, hotSpot.y);
58
+ // ACCESSIBILITY API BASED CURSOR DETECTION
59
+ // Determine cursor type based on the UI element under the cursor
104
60
 
105
- // Hand cursors (grab/grabbing) - different from regular cursors
106
- // Looking for cursors that are NOT the common patterns we know
107
- bool isKnownPattern = false;
61
+ CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
62
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
63
+ AXUIElementRef elementAtPosition = NULL;
64
+ AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
108
65
 
109
- // Text cursor: narrow tall with centered hotspot
110
- if (imageSize.width <= 12 && imageSize.height >= 16 &&
111
- hotSpot.x >= imageSize.width/2 - 2 && hotSpot.x <= imageSize.width/2 + 2) {
112
- isKnownPattern = true;
113
- }
66
+ NSString *cursorType = @"default"; // Default fallback
114
67
 
115
- // Pointer cursor: larger with off-center hotspot
116
- if ((imageSize.width >= 28 && imageSize.height >= 32 && hotSpot.x <= 8) ||
117
- (imageSize.width == 32 && imageSize.height == 32 && hotSpot.x <= 15 && hotSpot.y <= 10)) {
118
- isKnownPattern = true;
119
- }
68
+ if (error == kAXErrorSuccess && elementAtPosition) {
69
+ CFStringRef role = NULL;
70
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
120
71
 
121
- // Default cursor: 32x32 with center hotspot
122
- if (imageSize.width == 32 && imageSize.height == 32 &&
123
- hotSpot.x >= 14 && hotSpot.x <= 18 && hotSpot.y >= 14 && hotSpot.y <= 18) {
124
- isKnownPattern = true;
125
- }
72
+ if (error == kAXErrorSuccess && role) {
73
+ NSString *elementRole = (__bridge_transfer NSString*)role;
74
+ NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
126
75
 
127
- // Special cursor patterns detection
128
- if (!isKnownPattern) {
129
- // Progress cursor (loading/spinning) - usually larger, center hotspot
130
- if (imageSize.width >= 24 && imageSize.width <= 40 &&
131
- imageSize.height >= 24 && imageSize.height <= 40 &&
132
- hotSpot.x >= imageSize.width/2 - 4 && hotSpot.x <= imageSize.width/2 + 4 &&
133
- hotSpot.y >= imageSize.height/2 - 4 && hotSpot.y <= imageSize.height/2 + 4) {
134
- NSLog(@"🖱️ DETECTED PROGRESS: size=(%.0f,%.0f), hotspot=(%.0f,%.0f)",
135
- imageSize.width, imageSize.height, hotSpot.x, hotSpot.y);
136
- return @"progress";
76
+ // TEXT CURSORS
77
+ if ([elementRole isEqualToString:@"AXTextField"] ||
78
+ [elementRole isEqualToString:@"AXTextArea"] ||
79
+ [elementRole isEqualToString:@"AXStaticText"] ||
80
+ [elementRole isEqualToString:@"AXSearchField"]) {
81
+ cursorType = @"text";
137
82
  }
138
-
139
- // Zoom cursors (magnifying glass) - distinctive size/hotspot patterns
140
- if (imageSize.width >= 18 && imageSize.width <= 30 &&
141
- imageSize.height >= 18 && imageSize.height <= 30) {
142
- // Zoom-in usually has hotspot in center or slightly off
143
- if (hotSpot.x >= imageSize.width/2 - 3 && hotSpot.x <= imageSize.width/2 + 3) {
144
- NSLog(@"🖱️ DETECTED ZOOM-IN: size=(%.0f,%.0f)", imageSize.width, imageSize.height);
145
- return @"zoom-in";
146
- }
147
- // Zoom-out usually has different hotspot pattern
148
- else {
149
- NSLog(@"🖱️ DETECTED ZOOM-OUT: size=(%.0f,%.0f)", imageSize.width, imageSize.height);
150
- return @"zoom-out";
83
+ // POINTER CURSORS (clickable elements)
84
+ else if ([elementRole isEqualToString:@"AXLink"] ||
85
+ [elementRole isEqualToString:@"AXButton"] ||
86
+ [elementRole isEqualToString:@"AXMenuItem"] ||
87
+ [elementRole isEqualToString:@"AXRadioButton"] ||
88
+ [elementRole isEqualToString:@"AXCheckBox"] ||
89
+ [elementRole isEqualToString:@"AXPopUpButton"] ||
90
+ [elementRole isEqualToString:@"AXTab"]) {
91
+ cursorType = @"pointer";
92
+ }
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";
151
103
  }
152
104
  }
153
-
154
- // All-scroll cursor (move in all directions) - usually square with center hotspot
155
- if (imageSize.width >= 16 && imageSize.width <= 24 &&
156
- imageSize.height >= 16 && imageSize.height <= 24 &&
157
- abs(imageSize.width - imageSize.height) <= 2 &&
158
- hotSpot.x >= imageSize.width/2 - 2 && hotSpot.x <= imageSize.width/2 + 2 &&
159
- hotSpot.y >= imageSize.height/2 - 2 && hotSpot.y <= imageSize.height/2 + 2) {
160
- NSLog(@"🖱️ DETECTED ALL-SCROLL: size=(%.0f,%.0f)", imageSize.width, imageSize.height);
161
- return @"all-scroll";
105
+ // PROGRESS CURSORS (loading/busy elements)
106
+ else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
107
+ [elementRole isEqualToString:@"AXBusyIndicator"]) {
108
+ cursorType = @"progress";
162
109
  }
163
-
164
- // Diagonal resize cursors - typically 16x16 or similar
165
- if (imageSize.width >= 14 && imageSize.width <= 20 &&
166
- imageSize.height >= 14 && imageSize.height <= 20) {
167
- // NW-SE resize (diagonal top-left to bottom-right)
168
- if (hotSpot.x <= imageSize.width/2 && hotSpot.y <= imageSize.height/2) {
169
- NSLog(@"🖱️ DETECTED NWSE-RESIZE: size=(%.0f,%.0f)", imageSize.width, imageSize.height);
170
- return @"nwse-resize";
110
+ // HELP CURSORS (help buttons/tooltips)
111
+ else if ([elementRole isEqualToString:@"AXHelpTag"] ||
112
+ [elementRole isEqualToString:@"AXTooltip"]) {
113
+ cursorType = @"help";
114
+ }
115
+ // RESIZE CURSORS
116
+ else if ([elementRole isEqualToString:@"AXSplitter"]) {
117
+ // Get splitter orientation to determine resize direction
118
+ CFStringRef orientation = NULL;
119
+ error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
120
+ if (error == kAXErrorSuccess && orientation) {
121
+ NSString *orientationStr = (__bridge_transfer NSString*)orientation;
122
+ if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
123
+ cursorType = @"row-resize"; // vertical splitter -> row resize
124
+ } else {
125
+ cursorType = @"col-resize"; // horizontal splitter -> col resize
126
+ }
127
+ } else {
128
+ cursorType = @"col-resize"; // default
171
129
  }
172
- // NE-SW resize (diagonal top-right to bottom-left)
173
- else if (hotSpot.x >= imageSize.width/2 && hotSpot.y <= imageSize.height/2) {
174
- NSLog(@"🖱️ DETECTED NESW-RESIZE: size=(%.0f,%.0f)", imageSize.width, imageSize.height);
175
- return @"nesw-resize";
130
+ }
131
+ // SCROLL CURSORS - sadece gerçek scroll kontrollerinde
132
+ else if ([elementRole isEqualToString:@"AXScrollBar"]) {
133
+ // Check orientation for scroll direction
134
+ CFStringRef orientation = NULL;
135
+ error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
136
+ if (error == kAXErrorSuccess && orientation) {
137
+ NSString *orientationStr = (__bridge_transfer NSString*)orientation;
138
+ if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
139
+ cursorType = @"ns-resize"; // vertical scrollbar
140
+ } else {
141
+ cursorType = @"col-resize"; // horizontal scrollbar
142
+ }
143
+ } else {
144
+ cursorType = @"default"; // fallback to default
176
145
  }
177
- // Row resize (vertical resize)
178
- else if (abs(hotSpot.x - imageSize.width/2) <= 2) {
179
- NSLog(@"🖱️ DETECTED ROW-RESIZE: size=(%.0f,%.0f)", imageSize.width, imageSize.height);
180
- return @"row-resize";
146
+ }
147
+ // AXScrollArea için sadece belirli durumlarda all-scroll, çoğunlukla default
148
+ else if ([elementRole isEqualToString:@"AXScrollArea"]) {
149
+ // ScrollArea genellikle default olmalı, sadece özel durumlar için all-scroll
150
+ cursorType = @"default";
151
+ }
152
+ // CROSSHAIR CURSORS (drawing/selection tools)
153
+ else if ([elementRole isEqualToString:@"AXCanvas"] ||
154
+ [elementRole isEqualToString:@"AXDrawingArea"]) {
155
+ cursorType = @"crosshair";
156
+ }
157
+ // ZOOM CURSORS (zoom controls)
158
+ else if ([elementRole isEqualToString:@"AXZoomButton"]) {
159
+ cursorType = @"zoom-in";
160
+ }
161
+ // NOT-ALLOWED CURSORS (disabled elements)
162
+ else if ([elementRole isEqualToString:@"AXStaticText"] ||
163
+ [elementRole isEqualToString:@"AXGroup"]) {
164
+ // Check if element is disabled/readonly
165
+ CFBooleanRef enabled = NULL;
166
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
167
+ if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
168
+ cursorType = @"not-allowed";
181
169
  }
182
170
  }
183
-
184
- // Hand cursors (grab/grabbing) - medium-sized, various hotspots
185
- if (imageSize.width >= 14 && imageSize.width <= 24 &&
186
- imageSize.height >= 14 && imageSize.height <= 24) {
187
- NSLog(@"🖱️ POTENTIAL GRAB/GRABBING: size=(%.0f,%.0f), hotspot=(%.0f,%.0f)",
188
- imageSize.width, imageSize.height, hotSpot.x, hotSpot.y);
189
-
190
- // Try to differentiate grab vs grabbing by hotspot position
191
- if (hotSpot.x >= imageSize.width/2 - 2 && hotSpot.x <= imageSize.width/2 + 2 &&
192
- hotSpot.y >= imageSize.height/2 - 2 && hotSpot.y <= imageSize.height/2 + 2) {
193
- NSLog(@"🖱️ DETECTED GRAB (center hotspot)");
194
- return @"grab";
171
+ // WINDOW BORDER RESIZE - sadece pencere kenarlarında
172
+ else if ([elementRole isEqualToString:@"AXWindow"]) {
173
+ // Check window attributes to see if it's resizable
174
+ CFBooleanRef resizable = NULL;
175
+ AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
176
+
177
+ // Sadece resize edilebilir pencereler için cursor değişimi
178
+ if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
179
+ CFTypeRef position = NULL;
180
+ CFTypeRef size = NULL;
181
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
182
+ AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
183
+
184
+ if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
185
+ CGPoint windowPos;
186
+ CGSize windowSize;
187
+ AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
188
+ AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
189
+
190
+ CGFloat x = cursorPos.x - windowPos.x;
191
+ CGFloat y = cursorPos.y - windowPos.y;
192
+ CGFloat w = windowSize.width;
193
+ CGFloat h = windowSize.height;
194
+ CGFloat edge = 3.0; // Daha küçük edge detection (3px)
195
+
196
+ // Sadece çok kenar köşelerde resize cursor'ı göster
197
+ BOOL isOnBorder = NO;
198
+
199
+ // Corner resize detection - çok dar alanda
200
+ if ((x <= edge && y <= edge) || (x >= w-edge && y >= h-edge)) {
201
+ cursorType = @"nwse-resize";
202
+ isOnBorder = YES;
203
+ }
204
+ else if ((x >= w-edge && y <= edge) || (x <= edge && y >= h-edge)) {
205
+ cursorType = @"nesw-resize";
206
+ isOnBorder = YES;
207
+ }
208
+ // Edge resize detection - sadece çok kenarlarda
209
+ else if ((x <= edge || x >= w-edge) && y > edge && y < h-edge) {
210
+ cursorType = @"col-resize";
211
+ isOnBorder = YES;
212
+ }
213
+ else if ((y <= edge || y >= h-edge) && x > edge && x < w-edge) {
214
+ cursorType = @"row-resize";
215
+ isOnBorder = YES;
216
+ }
217
+
218
+ // Eğer border'da değilse default
219
+ if (!isOnBorder) {
220
+ cursorType = @"default";
221
+ }
222
+
223
+ if (position) CFRelease(position);
224
+ if (size) CFRelease(size);
225
+ } else {
226
+ cursorType = @"default";
227
+ }
195
228
  } else {
196
- NSLog(@"🖱️ DETECTED GRABBING (off-center hotspot)");
197
- return @"grabbing";
229
+ cursorType = @"default";
198
230
  }
199
231
  }
200
- }
201
- }
202
-
203
-
204
- // Check if we're in a drag operation
205
- CGEventRef event = CGEventCreate(NULL);
206
- if (event) {
207
- CGEventType eventType = (CGEventType)CGEventGetType(event);
208
- if (eventType == kCGEventLeftMouseDragged ||
209
- eventType == kCGEventRightMouseDragged) {
210
- CFRelease(event);
211
- return @"grabbing";
212
- }
213
- CFRelease(event);
214
- }
215
-
216
- // Get the window under the cursor
217
- CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
218
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
219
- AXUIElementRef elementAtPosition = NULL;
220
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
221
-
222
- if (error == kAXErrorSuccess && elementAtPosition) {
223
- CFStringRef role = NULL;
224
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
225
-
226
- if (error == kAXErrorSuccess && role) {
227
- NSString *elementRole = (__bridge_transfer NSString*)role;
228
-
229
- // Check for clickable elements that should show pointer cursor
230
- if ([elementRole isEqualToString:@"AXLink"] ||
231
- [elementRole isEqualToString:@"AXButton"] ||
232
- [elementRole isEqualToString:@"AXMenuItem"] ||
233
- [elementRole isEqualToString:@"AXRadioButton"] ||
234
- [elementRole isEqualToString:@"AXCheckBox"]) {
235
- return @"pointer";
232
+ // HER DURUM İÇİN DEFAULT FALLBACK
233
+ else {
234
+ // Bilinmeyen elementler için her zaman default
235
+ cursorType = @"default";
236
236
  }
237
-
238
- // Check subrole for additional pointer cursor elements
237
+
238
+ // Check subroles for additional context
239
239
  CFStringRef subrole = NULL;
240
240
  error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
241
241
  if (error == kAXErrorSuccess && subrole) {
242
242
  NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
243
-
244
- if ([elementSubrole isEqualToString:@"AXClickable"] ||
245
- [elementSubrole isEqualToString:@"AXDisclosureTriangle"] ||
246
- [elementSubrole isEqualToString:@"AXToolbarButton"] ||
247
- [elementSubrole isEqualToString:@"AXCloseButton"] ||
243
+ NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
244
+
245
+ // Subrole override'ları - sadece çok spesifik durumlar için
246
+ if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
248
247
  [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
249
- [elementSubrole isEqualToString:@"AXZoomButton"]) {
250
- return @"pointer";
248
+ [elementSubrole isEqualToString:@"AXZoomButton"] ||
249
+ [elementSubrole isEqualToString:@"AXToolbarButton"]) {
250
+ cursorType = @"pointer";
251
251
  }
252
- }
253
-
254
- // Check for text elements
255
- if ([elementRole isEqualToString:@"AXTextField"] ||
256
- [elementRole isEqualToString:@"AXTextArea"] ||
257
- [elementRole isEqualToString:@"AXStaticText"]) {
258
- return @"text";
252
+ // Copy/alias subroles - sadece bu durumlar için override
253
+ else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
254
+ [elementSubrole isEqualToString:@"AXDropTarget"]) {
255
+ cursorType = @"copy";
256
+ }
257
+ // Alias/shortcut subroles
258
+ else if ([elementSubrole isEqualToString:@"AXAlias"] ||
259
+ [elementSubrole isEqualToString:@"AXShortcut"]) {
260
+ cursorType = @"alias";
261
+ }
262
+ // Grabbing state (being dragged) - sadece gerçek drag sırasında
263
+ else if ([elementSubrole isEqualToString:@"AXDragging"] ||
264
+ [elementSubrole isEqualToString:@"AXMoving"]) {
265
+ cursorType = @"grabbing";
266
+ }
267
+ // Zoom controls - sadece spesifik zoom butonları için
268
+ else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
269
+ cursorType = @"zoom-in";
270
+ }
271
+ else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
272
+ cursorType = @"zoom-out";
273
+ }
274
+ // Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
259
275
  }
260
276
  }
261
-
277
+
262
278
  CFRelease(elementAtPosition);
263
279
  }
264
-
280
+
265
281
  if (systemWide) {
266
282
  CFRelease(systemWide);
267
283
  }
268
-
284
+
285
+ // Son güvence - eğer cursorType hala nil veya geçersizse default'a çevir
286
+ if (!cursorType || [cursorType length] == 0) {
287
+ cursorType = @"default";
288
+ }
289
+
290
+ NSLog(@"🎯 FINAL CURSOR TYPE: %@", cursorType);
269
291
  return cursorType;
270
292
 
271
293
  } @catch (NSException *exception) {