node-mac-recorder 2.17.12 → 2.17.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cursor_tracker.mm +182 -41
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -43,9 +43,9 @@ static NSTimeInterval g_lastCursorCheckTime = 0;
|
|
|
43
43
|
static int g_sameCursorDetectionCount = 0;
|
|
44
44
|
static NSString *g_pendingCursorType = nil;
|
|
45
45
|
|
|
46
|
-
// Cursor stability constants
|
|
47
|
-
static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.
|
|
48
|
-
static const int CURSOR_CONFIRMATION_COUNT =
|
|
46
|
+
// Cursor stability constants - more responsive
|
|
47
|
+
static const NSTimeInterval CURSOR_STABILITY_THRESHOLD = 0.05; // 50ms (faster response)
|
|
48
|
+
static const int CURSOR_CONFIRMATION_COUNT = 1; // Need 1 detection (more responsive)
|
|
49
49
|
|
|
50
50
|
// Mouse button state tracking
|
|
51
51
|
static bool g_leftMouseDown = false;
|
|
@@ -57,12 +57,12 @@ static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEv
|
|
|
57
57
|
return event;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Enhanced cursor type detection with
|
|
60
|
+
// Enhanced cursor type detection with better NSCursor analysis
|
|
61
61
|
NSString* detectCursorTypeFromNSCursor() {
|
|
62
62
|
@try {
|
|
63
63
|
NSCursor *currentCursor = [NSCursor currentSystemCursor];
|
|
64
64
|
if (!currentCursor) {
|
|
65
|
-
return
|
|
65
|
+
return nil; // Return nil to indicate we should try other methods
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Compare with known system cursors using identity comparison
|
|
@@ -86,24 +86,43 @@ NSString* detectCursorTypeFromNSCursor() {
|
|
|
86
86
|
return @"not-allowed";
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
//
|
|
89
|
+
// Check for additional resize cursors using image analysis
|
|
90
90
|
NSImage *cursorImage = [currentCursor image];
|
|
91
91
|
if (cursorImage) {
|
|
92
92
|
NSSize imageSize = [cursorImage size];
|
|
93
|
+
NSPoint hotSpot = [currentCursor hotSpot];
|
|
94
|
+
|
|
95
|
+
// Analyze cursor image to detect resize cursors
|
|
96
|
+
if (imageSize.width > 10 && imageSize.height > 10) {
|
|
97
|
+
// Check for diagonal resize cursors (corner resize)
|
|
98
|
+
if (imageSize.width >= 15 && imageSize.height >= 15) {
|
|
99
|
+
// These are likely diagonal resize cursors
|
|
100
|
+
// hotSpot can help distinguish between nwse and nesw
|
|
101
|
+
if (hotSpot.x < imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
|
|
102
|
+
return @"nwse-resize"; // northwest-southeast
|
|
103
|
+
} else if (hotSpot.x > imageSize.width / 2 && hotSpot.y < imageSize.height / 2) {
|
|
104
|
+
return @"nesw-resize"; // northeast-southwest
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for horizontal/vertical resize cursors
|
|
109
|
+
if (imageSize.width > imageSize.height + 5) {
|
|
110
|
+
return @"col-resize"; // horizontal resize
|
|
111
|
+
} else if (imageSize.height > imageSize.width + 5) {
|
|
112
|
+
return @"ns-resize"; // vertical resize
|
|
113
|
+
}
|
|
114
|
+
}
|
|
93
115
|
|
|
94
116
|
// Text cursors typically have I-beam shape (narrow width)
|
|
95
117
|
if (imageSize.width < 8 && imageSize.height > 15) {
|
|
96
118
|
return @"text";
|
|
97
119
|
}
|
|
98
|
-
// Pointer cursors are typically hand-shaped
|
|
99
|
-
else if (imageSize.width > 15 && imageSize.height > 15) {
|
|
100
|
-
return @"pointer";
|
|
101
|
-
}
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
|
|
122
|
+
// Return nil to indicate we should use contextual detection
|
|
123
|
+
return nil;
|
|
105
124
|
} @catch (NSException *exception) {
|
|
106
|
-
return
|
|
125
|
+
return nil;
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
|
|
@@ -114,48 +133,170 @@ NSString* getCursorType() {
|
|
|
114
133
|
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
|
|
115
134
|
|
|
116
135
|
@try {
|
|
117
|
-
// Layer 1: Fast NSCursor detection (
|
|
136
|
+
// Layer 1: Fast NSCursor detection (high confidence cases)
|
|
118
137
|
NSString *nsCursorType = detectCursorTypeFromNSCursor();
|
|
119
138
|
|
|
120
|
-
// Layer 2:
|
|
121
|
-
NSString *contextualCursorType =
|
|
139
|
+
// Layer 2: Contextual detection using Accessibility API
|
|
140
|
+
NSString *contextualCursorType = nil;
|
|
122
141
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
AXUIElementRef elementAtPosition = NULL;
|
|
128
|
-
AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
|
|
142
|
+
CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
|
|
143
|
+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
144
|
+
AXUIElementRef elementAtPosition = NULL;
|
|
145
|
+
AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
if (error == kAXErrorSuccess && elementAtPosition) {
|
|
148
|
+
CFStringRef role = NULL;
|
|
149
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
|
|
133
150
|
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
if (error == kAXErrorSuccess && role) {
|
|
152
|
+
NSString *elementRole = (__bridge_transfer NSString*)role;
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
// TEXT CURSORS - high priority
|
|
155
|
+
if ([elementRole isEqualToString:@"AXTextField"] ||
|
|
156
|
+
[elementRole isEqualToString:@"AXTextArea"] ||
|
|
157
|
+
[elementRole isEqualToString:@"AXSearchField"]) {
|
|
158
|
+
contextualCursorType = @"text";
|
|
159
|
+
}
|
|
160
|
+
// POINTER CURSORS - interactive elements with broader detection
|
|
161
|
+
else if ([elementRole isEqualToString:@"AXLink"] ||
|
|
162
|
+
[elementRole isEqualToString:@"AXButton"] ||
|
|
163
|
+
[elementRole isEqualToString:@"AXMenuItem"] ||
|
|
164
|
+
[elementRole isEqualToString:@"AXRadioButton"] ||
|
|
165
|
+
[elementRole isEqualToString:@"AXCheckBox"] ||
|
|
166
|
+
[elementRole isEqualToString:@"AXPopUpButton"] ||
|
|
167
|
+
[elementRole isEqualToString:@"AXTab"]) {
|
|
168
|
+
contextualCursorType = @"pointer";
|
|
169
|
+
|
|
170
|
+
// Also check subroles for links and buttons
|
|
171
|
+
CFStringRef subrole = NULL;
|
|
172
|
+
AXError subroleError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
|
|
173
|
+
if (subroleError == kAXErrorSuccess && subrole) {
|
|
174
|
+
NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
|
|
175
|
+
if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
|
|
176
|
+
[elementSubrole isEqualToString:@"AXMinimizeButton"] ||
|
|
177
|
+
[elementSubrole isEqualToString:@"AXZoomButton"] ||
|
|
178
|
+
[elementSubrole isEqualToString:@"AXToolbarButton"]) {
|
|
179
|
+
contextualCursorType = @"pointer";
|
|
180
|
+
}
|
|
142
181
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
}
|
|
183
|
+
// WEB ELEMENTS - for web links that might not show as AXLink
|
|
184
|
+
else if ([elementRole isEqualToString:@"AXGroup"] ||
|
|
185
|
+
[elementRole isEqualToString:@"AXStaticText"]) {
|
|
186
|
+
// Check if it's clickable/has action
|
|
187
|
+
CFArrayRef actions = NULL;
|
|
188
|
+
AXError actionsError = AXUIElementCopyActionNames(elementAtPosition, &actions);
|
|
189
|
+
if (actionsError == kAXErrorSuccess && actions) {
|
|
190
|
+
CFIndex actionCount = CFArrayGetCount(actions);
|
|
191
|
+
for (CFIndex i = 0; i < actionCount; i++) {
|
|
192
|
+
CFStringRef action = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
|
|
193
|
+
NSString *actionStr = (__bridge NSString*)action;
|
|
194
|
+
if ([actionStr isEqualToString:@"AXPress"] ||
|
|
195
|
+
[actionStr isEqualToString:@"AXShowMenu"]) {
|
|
196
|
+
contextualCursorType = @"pointer";
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
CFRelease(actions);
|
|
147
201
|
}
|
|
148
|
-
|
|
149
|
-
|
|
202
|
+
}
|
|
203
|
+
// WINDOW BORDER RESIZE - critical for resize detection
|
|
204
|
+
else if ([elementRole isEqualToString:@"AXWindow"]) {
|
|
205
|
+
CFTypeRef position = NULL;
|
|
206
|
+
CFTypeRef size = NULL;
|
|
207
|
+
AXError posErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
|
|
208
|
+
AXError sizeErr = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
|
|
209
|
+
|
|
210
|
+
if (posErr == kAXErrorSuccess && sizeErr == kAXErrorSuccess && position && size) {
|
|
211
|
+
CGPoint windowPos;
|
|
212
|
+
CGSize windowSize;
|
|
213
|
+
AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
|
|
214
|
+
AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
|
|
215
|
+
|
|
216
|
+
CGFloat x = cursorPos.x - windowPos.x;
|
|
217
|
+
CGFloat y = cursorPos.y - windowPos.y;
|
|
218
|
+
CGFloat w = windowSize.width;
|
|
219
|
+
CGFloat h = windowSize.height;
|
|
220
|
+
CGFloat edge = 5.0; // 5px edge detection
|
|
221
|
+
|
|
222
|
+
// Corner resize detection
|
|
223
|
+
if (x <= edge && y <= edge) {
|
|
224
|
+
contextualCursorType = @"nwse-resize";
|
|
225
|
+
}
|
|
226
|
+
else if (x >= w-edge && y <= edge) {
|
|
227
|
+
contextualCursorType = @"nesw-resize";
|
|
228
|
+
}
|
|
229
|
+
else if (x <= edge && y >= h-edge) {
|
|
230
|
+
contextualCursorType = @"nesw-resize";
|
|
231
|
+
}
|
|
232
|
+
else if (x >= w-edge && y >= h-edge) {
|
|
233
|
+
contextualCursorType = @"nwse-resize";
|
|
234
|
+
}
|
|
235
|
+
// Edge resize detection
|
|
236
|
+
else if (x <= edge || x >= w-edge) {
|
|
237
|
+
contextualCursorType = @"col-resize";
|
|
238
|
+
}
|
|
239
|
+
else if (y <= edge || y >= h-edge) {
|
|
240
|
+
contextualCursorType = @"ns-resize";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (position) CFRelease(position);
|
|
244
|
+
if (size) CFRelease(size);
|
|
150
245
|
}
|
|
151
246
|
}
|
|
152
|
-
|
|
247
|
+
// SPLITTER RESIZE
|
|
248
|
+
else if ([elementRole isEqualToString:@"AXSplitter"]) {
|
|
249
|
+
CFStringRef orientation = NULL;
|
|
250
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
|
|
251
|
+
if (error == kAXErrorSuccess && orientation) {
|
|
252
|
+
NSString *orientationStr = (__bridge_transfer NSString*)orientation;
|
|
253
|
+
if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
|
|
254
|
+
contextualCursorType = @"ns-resize";
|
|
255
|
+
} else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
|
|
256
|
+
contextualCursorType = @"col-resize";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// PROGRESS INDICATORS
|
|
261
|
+
else if ([elementRole isEqualToString:@"AXProgressIndicator"]) {
|
|
262
|
+
contextualCursorType = @"progress";
|
|
263
|
+
}
|
|
264
|
+
// OTHER ELEMENTS - be conservative, don't assume pointer
|
|
265
|
+
else {
|
|
266
|
+
// Don't override NSCursor for unknown elements
|
|
267
|
+
contextualCursorType = nil;
|
|
268
|
+
}
|
|
153
269
|
}
|
|
154
|
-
|
|
270
|
+
CFRelease(elementAtPosition);
|
|
271
|
+
}
|
|
272
|
+
if (systemWide) CFRelease(systemWide);
|
|
273
|
+
|
|
274
|
+
// Layer 3: Intelligent fusion of NSCursor and contextual results
|
|
275
|
+
NSString *detectedCursorType = @"default";
|
|
276
|
+
|
|
277
|
+
// Priority logic with better fallback:
|
|
278
|
+
// 1. If contextual gives resize cursor, always use it (resize has highest priority)
|
|
279
|
+
if (contextualCursorType != nil &&
|
|
280
|
+
([contextualCursorType hasSuffix:@"resize"] ||
|
|
281
|
+
[contextualCursorType isEqualToString:@"col-resize"] ||
|
|
282
|
+
[contextualCursorType isEqualToString:@"ns-resize"])) {
|
|
283
|
+
detectedCursorType = contextualCursorType;
|
|
284
|
+
}
|
|
285
|
+
// 2. If NSCursor gives a definitive answer, use it
|
|
286
|
+
else if (nsCursorType != nil) {
|
|
287
|
+
detectedCursorType = nsCursorType;
|
|
288
|
+
}
|
|
289
|
+
// 3. If contextual gives specific non-resize cursor, use it
|
|
290
|
+
else if (contextualCursorType != nil &&
|
|
291
|
+
![contextualCursorType isEqualToString:@"default"]) {
|
|
292
|
+
detectedCursorType = contextualCursorType;
|
|
293
|
+
}
|
|
294
|
+
// 4. If both are nil or default, use true default
|
|
295
|
+
else {
|
|
296
|
+
detectedCursorType = @"default";
|
|
155
297
|
}
|
|
156
298
|
|
|
157
|
-
// Layer
|
|
158
|
-
NSString *detectedCursorType = contextualCursorType;
|
|
299
|
+
// Layer 4: Stability filtering to prevent oscillation
|
|
159
300
|
|
|
160
301
|
// Time-based stability check
|
|
161
302
|
if (currentTime - g_lastCursorCheckTime > CURSOR_STABILITY_THRESHOLD) {
|