node-mac-recorder 2.17.5 → 2.17.6

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 +173 -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.6",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -55,217 +55,209 @@ 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
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 = @"all-scroll";
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
+ else if ([elementRole isEqualToString:@"AXScrollArea"]) {
148
+ cursorType = @"all-scroll";
149
+ }
150
+ // CROSSHAIR CURSORS (drawing/selection tools)
151
+ else if ([elementRole isEqualToString:@"AXCanvas"] ||
152
+ [elementRole isEqualToString:@"AXDrawingArea"]) {
153
+ cursorType = @"crosshair";
154
+ }
155
+ // ZOOM CURSORS (zoom controls)
156
+ else if ([elementRole isEqualToString:@"AXZoomButton"]) {
157
+ cursorType = @"zoom-in";
158
+ }
159
+ // NOT-ALLOWED CURSORS (disabled elements)
160
+ else if ([elementRole isEqualToString:@"AXStaticText"] ||
161
+ [elementRole isEqualToString:@"AXGroup"]) {
162
+ // Check if element is disabled/readonly
163
+ CFBooleanRef enabled = NULL;
164
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
165
+ if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
166
+ cursorType = @"not-allowed";
181
167
  }
182
168
  }
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";
169
+ // WINDOW BORDER RESIZE
170
+ else if ([elementRole isEqualToString:@"AXWindow"]) {
171
+ // Check mouse position relative to window bounds for resize detection
172
+ CFTypeRef position = NULL;
173
+ CFTypeRef size = NULL;
174
+ error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
175
+ AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
176
+
177
+ if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
178
+ CGPoint windowPos;
179
+ CGSize windowSize;
180
+ AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
181
+ AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
182
+
183
+ CGFloat x = cursorPos.x - windowPos.x;
184
+ CGFloat y = cursorPos.y - windowPos.y;
185
+ CGFloat w = windowSize.width;
186
+ CGFloat h = windowSize.height;
187
+ CGFloat edge = 5.0; // 5px edge detection
188
+
189
+ // Corner resize detection
190
+ if ((x <= edge && y <= edge) || (x >= w-edge && y >= h-edge)) {
191
+ cursorType = @"nwse-resize"; // Top-left or bottom-right corner
192
+ }
193
+ else if ((x >= w-edge && y <= edge) || (x <= edge && y >= h-edge)) {
194
+ cursorType = @"nesw-resize"; // Top-right or bottom-left corner
195
+ }
196
+ // Edge resize detection
197
+ else if (x <= edge || x >= w-edge) {
198
+ cursorType = @"col-resize"; // Left or right edge
199
+ }
200
+ else if (y <= edge || y >= h-edge) {
201
+ cursorType = @"row-resize"; // Top or bottom edge
202
+ }
203
+ else {
204
+ cursorType = @"default";
205
+ }
206
+
207
+ if (position) CFRelease(position);
208
+ if (size) CFRelease(size);
195
209
  } else {
196
- NSLog(@"🖱️ DETECTED GRABBING (off-center hotspot)");
197
- return @"grabbing";
210
+ cursorType = @"default";
198
211
  }
199
212
  }
200
- }
201
- }
202
213
 
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";
236
- }
237
-
238
- // Check subrole for additional pointer cursor elements
214
+ // Check subroles for additional context
239
215
  CFStringRef subrole = NULL;
240
216
  error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
241
217
  if (error == kAXErrorSuccess && subrole) {
242
218
  NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
243
-
244
- if ([elementSubrole isEqualToString:@"AXClickable"] ||
245
- [elementSubrole isEqualToString:@"AXDisclosureTriangle"] ||
246
- [elementSubrole isEqualToString:@"AXToolbarButton"] ||
247
- [elementSubrole isEqualToString:@"AXCloseButton"] ||
219
+ NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
220
+
221
+ // Special button subroles
222
+ if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
248
223
  [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
249
- [elementSubrole isEqualToString:@"AXZoomButton"]) {
250
- return @"pointer";
224
+ [elementSubrole isEqualToString:@"AXZoomButton"] ||
225
+ [elementSubrole isEqualToString:@"AXToolbarButton"]) {
226
+ cursorType = @"pointer";
227
+ }
228
+ // Copy/alias subroles
229
+ else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
230
+ [elementSubrole isEqualToString:@"AXDropTarget"]) {
231
+ cursorType = @"copy";
232
+ }
233
+ // Alias/shortcut subroles
234
+ else if ([elementSubrole isEqualToString:@"AXAlias"] ||
235
+ [elementSubrole isEqualToString:@"AXShortcut"]) {
236
+ cursorType = @"alias";
237
+ }
238
+ // Grabbing state (being dragged)
239
+ else if ([elementSubrole isEqualToString:@"AXDragging"] ||
240
+ [elementSubrole isEqualToString:@"AXMoving"]) {
241
+ cursorType = @"grabbing";
242
+ }
243
+ // Zoom controls
244
+ else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
245
+ cursorType = @"zoom-in";
246
+ }
247
+ else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
248
+ cursorType = @"zoom-out";
251
249
  }
252
- }
253
-
254
- // Check for text elements
255
- if ([elementRole isEqualToString:@"AXTextField"] ||
256
- [elementRole isEqualToString:@"AXTextArea"] ||
257
- [elementRole isEqualToString:@"AXStaticText"]) {
258
- return @"text";
259
250
  }
260
251
  }
261
-
252
+
262
253
  CFRelease(elementAtPosition);
263
254
  }
264
-
255
+
265
256
  if (systemWide) {
266
257
  CFRelease(systemWide);
267
258
  }
268
-
259
+
260
+ NSLog(@"🎯 FINAL CURSOR TYPE: %@", cursorType);
269
261
  return cursorType;
270
262
 
271
263
  } @catch (NSException *exception) {