node-mac-recorder 2.20.2 → 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.
- package/package.json +1 -1
- package/src/cursor_tracker.mm +232 -41
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -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;
|
|
@@ -49,16 +53,194 @@ static NSString* CopyAndReleaseCFString(CFStringRef value) {
|
|
|
49
53
|
return result;
|
|
50
54
|
}
|
|
51
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
|
+
|
|
52
239
|
// Mouse button state tracking
|
|
53
240
|
static bool g_leftMouseDown = false;
|
|
54
241
|
static bool g_rightMouseDown = false;
|
|
55
242
|
static NSString *g_lastEventType = @"move";
|
|
56
243
|
|
|
57
|
-
// Event tap callback
|
|
58
|
-
static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) {
|
|
59
|
-
return event;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
244
|
// Accessibility tabanlı cursor tip tespiti
|
|
63
245
|
static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
64
246
|
@autoreleasepool {
|
|
@@ -334,6 +516,43 @@ static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
|
334
516
|
}
|
|
335
517
|
|
|
336
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";
|
|
538
|
+
}
|
|
539
|
+
CFRelease(deepElement);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
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)) {
|
|
550
|
+
cursorType = @"pointer";
|
|
551
|
+
}
|
|
552
|
+
CFRelease(focusedElement);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
337
556
|
CFRelease(elementAtPosition);
|
|
338
557
|
}
|
|
339
558
|
if (systemWide) {
|
|
@@ -569,12 +788,10 @@ NSString* getCursorType() {
|
|
|
569
788
|
if (axCursorType && ![axCursorType isEqualToString:@"default"]) {
|
|
570
789
|
finalType = axCursorType;
|
|
571
790
|
} else if (systemCursorType && [systemCursorType length] > 0) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
finalType = systemCursorType;
|
|
577
|
-
}
|
|
791
|
+
// Prefer the system cursor when accessibility reports a generic value.
|
|
792
|
+
finalType = systemCursorType;
|
|
793
|
+
} else if (axCursorType && [axCursorType length] > 0) {
|
|
794
|
+
finalType = axCursorType;
|
|
578
795
|
} else {
|
|
579
796
|
finalType = @"default";
|
|
580
797
|
}
|
|
@@ -629,17 +846,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
629
846
|
|
|
630
847
|
CGPoint rawLocation = CGEventGetLocation(event);
|
|
631
848
|
|
|
632
|
-
//
|
|
633
|
-
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
849
|
+
// Coordinates are already in logical space; no additional scaling needed here.
|
|
634
850
|
CGPoint location = rawLocation;
|
|
635
|
-
|
|
636
|
-
if (scalingInfo) {
|
|
637
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
638
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
639
|
-
|
|
640
|
-
// Keep logical coordinates - no scaling needed here
|
|
641
|
-
location = rawLocation;
|
|
642
|
-
}
|
|
643
851
|
NSDate *currentDate = [NSDate date];
|
|
644
852
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
645
853
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
@@ -702,18 +910,9 @@ void cursorTimerCallback() {
|
|
|
702
910
|
CFRelease(event);
|
|
703
911
|
}
|
|
704
912
|
|
|
705
|
-
//
|
|
706
|
-
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
913
|
+
// Coordinates are already in logical space; no additional scaling needed here.
|
|
707
914
|
CGPoint location = rawLocation;
|
|
708
915
|
|
|
709
|
-
if (scalingInfo) {
|
|
710
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
711
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
712
|
-
|
|
713
|
-
// Keep logical coordinates - no scaling needed here
|
|
714
|
-
location = rawLocation;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
916
|
NSDate *currentDate = [NSDate date];
|
|
718
917
|
NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
|
|
719
918
|
NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
|
|
@@ -1042,16 +1241,8 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
|
|
|
1042
1241
|
// Get display scaling information
|
|
1043
1242
|
NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
|
|
1044
1243
|
CGPoint logicalLocation = rawLocation;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
|
|
1048
|
-
NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
|
|
1049
|
-
|
|
1050
|
-
// CGEventGetLocation returns LOGICAL coordinates (correct for JS layer)
|
|
1051
|
-
// Keep logical coordinates - transformation happens in JS layer
|
|
1052
|
-
logicalLocation = rawLocation;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1244
|
+
// CGEventGetLocation already returns logical coordinates; additional scaling happens in JS layer.
|
|
1245
|
+
|
|
1055
1246
|
NSString *cursorType = getCursorType();
|
|
1056
1247
|
|
|
1057
1248
|
// Mouse button state'ini kontrol et
|