node-mac-recorder 2.10.15 ā 2.10.17
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/window_selector.mm +68 -43
- package/test-overlay-tracking.js +175 -0
package/package.json
CHANGED
package/src/window_selector.mm
CHANGED
|
@@ -56,6 +56,8 @@ void updateScreenOverlays();
|
|
|
56
56
|
@property (nonatomic, strong) NSDictionary *windowInfo;
|
|
57
57
|
@property (nonatomic) BOOL isActiveWindow;
|
|
58
58
|
@property (nonatomic) BOOL isToggled;
|
|
59
|
+
@property (nonatomic) NSRect highlightFrame;
|
|
60
|
+
- (void)setHighlightFrame:(NSRect)frame;
|
|
59
61
|
@end
|
|
60
62
|
|
|
61
63
|
@implementation WindowSelectorOverlayView
|
|
@@ -63,41 +65,62 @@ void updateScreenOverlays();
|
|
|
63
65
|
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
64
66
|
self = [super initWithFrame:frameRect];
|
|
65
67
|
if (self) {
|
|
66
|
-
//
|
|
68
|
+
// Full-screen transparent overlay for window highlighting
|
|
67
69
|
self.wantsLayer = YES;
|
|
68
|
-
self.isActiveWindow = YES;
|
|
70
|
+
self.isActiveWindow = YES;
|
|
71
|
+
self.highlightFrame = NSZeroRect;
|
|
69
72
|
|
|
70
|
-
//
|
|
71
|
-
|
|
73
|
+
// Transparent background for full-screen overlay
|
|
74
|
+
self.layer.backgroundColor = [[NSColor clearColor] CGColor];
|
|
72
75
|
|
|
73
76
|
// Window selector overlay view created
|
|
74
77
|
}
|
|
75
78
|
return self;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
- (void)
|
|
81
|
+
- (void)setHighlightFrame:(NSRect)frame {
|
|
82
|
+
_highlightFrame = frame;
|
|
83
|
+
[self setNeedsDisplay:YES]; // Trigger redraw
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
87
|
+
[super drawRect:dirtyRect];
|
|
88
|
+
|
|
89
|
+
if (NSEqualRects(self.highlightFrame, NSZeroRect)) {
|
|
90
|
+
return; // No window to highlight
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Draw highlight rectangle for the selected window
|
|
94
|
+
NSBezierPath *highlightPath = [NSBezierPath bezierPathWithRoundedRect:self.highlightFrame
|
|
95
|
+
xRadius:8.0
|
|
96
|
+
yRadius:8.0];
|
|
97
|
+
|
|
98
|
+
// Fill color based on toggle state
|
|
99
|
+
NSColor *fillColor;
|
|
100
|
+
NSColor *strokeColor;
|
|
101
|
+
CGFloat lineWidth;
|
|
102
|
+
|
|
79
103
|
if (self.isToggled) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.layer.borderWidth = 3.0;
|
|
84
|
-
} else if (self.isActiveWindow) {
|
|
85
|
-
// Active window: brighter background, thin white border
|
|
86
|
-
self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] CGColor];
|
|
87
|
-
self.layer.borderColor = [[NSColor whiteColor] CGColor];
|
|
88
|
-
self.layer.borderWidth = 1.0;
|
|
104
|
+
fillColor = [NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4];
|
|
105
|
+
strokeColor = [NSColor colorWithRed:0.45 green:0.25 blue:0.75 alpha:0.9];
|
|
106
|
+
lineWidth = 3.0;
|
|
89
107
|
} else {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
self.layer.borderWidth = 1.0;
|
|
108
|
+
fillColor = [NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4];
|
|
109
|
+
strokeColor = [NSColor whiteColor];
|
|
110
|
+
lineWidth = 1.0;
|
|
94
111
|
}
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
[fillColor setFill];
|
|
114
|
+
[highlightPath fill];
|
|
115
|
+
|
|
116
|
+
[strokeColor setStroke];
|
|
117
|
+
[highlightPath setLineWidth:lineWidth];
|
|
118
|
+
[highlightPath stroke];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- (void)updateAppearance {
|
|
122
|
+
// Appearance is now handled in drawRect
|
|
123
|
+
[self setNeedsDisplay:YES];
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
- (void)setIsActiveWindow:(BOOL)isActiveWindow {
|
|
@@ -500,7 +523,7 @@ void updateOverlay() {
|
|
|
500
523
|
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
501
524
|
CGPoint globalPoint = CGPointMake(mouseLocation.x, screenHeight - mouseLocation.y);
|
|
502
525
|
|
|
503
|
-
// Find window under cursor
|
|
526
|
+
// Find window under cursor (no need to refresh g_allWindows frequently since windows can't move)
|
|
504
527
|
NSDictionary *windowUnderCursor = getWindowUnderCursor(globalPoint);
|
|
505
528
|
|
|
506
529
|
// Check if we need to update overlay (new window or position change of current window)
|
|
@@ -653,11 +676,10 @@ void updateOverlay() {
|
|
|
653
676
|
}
|
|
654
677
|
}
|
|
655
678
|
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
// Update overlay view window info
|
|
679
|
+
// No need to resize window since it's full-screen, just update the highlight area
|
|
680
|
+
// Update overlay view window info for highlighting
|
|
660
681
|
[(WindowSelectorOverlayView *)g_overlayView setWindowInfo:targetWindow];
|
|
682
|
+
[(WindowSelectorOverlayView *)g_overlayView setHighlightFrame:NSMakeRect(x, [g_overlayView frame].size.height - y - height, width, height)];
|
|
661
683
|
|
|
662
684
|
// Only reset toggle state when switching to different window (not for position updates)
|
|
663
685
|
if (!g_hasToggledWindow) {
|
|
@@ -755,18 +777,20 @@ void updateOverlay() {
|
|
|
755
777
|
NSString *labelAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown App";
|
|
756
778
|
[infoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", labelAppName, labelWindowTitle]];
|
|
757
779
|
|
|
758
|
-
// Position buttons - Start Record in center
|
|
780
|
+
// Position buttons - Start Record in center of highlighted window
|
|
759
781
|
if (g_selectButton) {
|
|
760
782
|
NSSize buttonSize = [g_selectButton frame].size;
|
|
783
|
+
// Convert window coordinates to overlay view coordinates
|
|
784
|
+
NSRect highlightFrame = NSMakeRect(x, [g_overlayView frame].size.height - y - height, width, height);
|
|
761
785
|
NSPoint buttonCenter = NSMakePoint(
|
|
762
|
-
(width - buttonSize.width) / 2,
|
|
763
|
-
(height - buttonSize.height) / 2 + 15
|
|
786
|
+
highlightFrame.origin.x + (highlightFrame.size.width - buttonSize.width) / 2,
|
|
787
|
+
highlightFrame.origin.y + (highlightFrame.size.height - buttonSize.height) / 2 + 15
|
|
764
788
|
);
|
|
765
789
|
[g_selectButton setFrameOrigin:buttonCenter];
|
|
766
790
|
|
|
767
|
-
// Position app icon above text label
|
|
791
|
+
// Position app icon above text label within highlighted area
|
|
768
792
|
NSPoint iconCenter = NSMakePoint(
|
|
769
|
-
(width - 96) / 2, // Center horizontally
|
|
793
|
+
highlightFrame.origin.x + (highlightFrame.size.width - 96) / 2, // Center horizontally within highlight
|
|
770
794
|
buttonCenter.y + buttonSize.height + 60 + 10 // Above label + text height + margin
|
|
771
795
|
);
|
|
772
796
|
[appIconView setFrameOrigin:iconCenter];
|
|
@@ -787,9 +811,9 @@ void updateOverlay() {
|
|
|
787
811
|
floatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
|
788
812
|
[appIconView.layer addAnimation:floatAnimationX forKey:@"floatAnimationX"];
|
|
789
813
|
|
|
790
|
-
// Position info label
|
|
814
|
+
// Position info label within highlighted area, above button
|
|
791
815
|
NSPoint labelCenter = NSMakePoint(
|
|
792
|
-
(width - [infoLabel frame].size.width) / 2, // Center horizontally
|
|
816
|
+
highlightFrame.origin.x + (highlightFrame.size.width - [infoLabel frame].size.width) / 2, // Center horizontally within highlight
|
|
793
817
|
buttonCenter.y + buttonSize.height + 10 // 10px above button, below icon
|
|
794
818
|
);
|
|
795
819
|
[infoLabel setFrameOrigin:labelCenter];
|
|
@@ -807,7 +831,7 @@ void updateOverlay() {
|
|
|
807
831
|
if (cancelButton) {
|
|
808
832
|
NSSize cancelButtonSize = [cancelButton frame].size;
|
|
809
833
|
NSPoint cancelButtonCenter = NSMakePoint(
|
|
810
|
-
(width - cancelButtonSize.width) / 2,
|
|
834
|
+
highlightFrame.origin.x + (highlightFrame.size.width - cancelButtonSize.width) / 2,
|
|
811
835
|
buttonCenter.y - buttonSize.height - 20 // 20px below main button
|
|
812
836
|
);
|
|
813
837
|
[cancelButton setFrameOrigin:cancelButtonCenter];
|
|
@@ -1570,9 +1594,10 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
1570
1594
|
return env.Null();
|
|
1571
1595
|
}
|
|
1572
1596
|
|
|
1573
|
-
// Create overlay window
|
|
1574
|
-
|
|
1575
|
-
|
|
1597
|
+
// Create full-screen overlay window to prevent window dragging
|
|
1598
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
1599
|
+
NSRect fullScreenFrame = [mainScreen frame];
|
|
1600
|
+
g_overlayWindow = [[NSWindow alloc] initWithContentRect:fullScreenFrame
|
|
1576
1601
|
styleMask:NSWindowStyleMaskBorderless
|
|
1577
1602
|
backing:NSBackingStoreBuffered
|
|
1578
1603
|
defer:NO];
|
|
@@ -1583,11 +1608,11 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
1583
1608
|
[g_overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Absolute highest level
|
|
1584
1609
|
[g_overlayWindow setOpaque:NO];
|
|
1585
1610
|
[g_overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1586
|
-
[g_overlayWindow setIgnoresMouseEvents:NO];
|
|
1611
|
+
[g_overlayWindow setIgnoresMouseEvents:NO]; // Capture mouse events to prevent window dragging
|
|
1587
1612
|
[g_overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
1588
1613
|
[g_overlayWindow setHasShadow:NO];
|
|
1589
1614
|
[g_overlayWindow setAlphaValue:1.0];
|
|
1590
|
-
[g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
1615
|
+
[g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorFullScreenAuxiliary];
|
|
1591
1616
|
|
|
1592
1617
|
// Remove any default window decorations and borders
|
|
1593
1618
|
[g_overlayWindow setTitlebarAppearsTransparent:YES];
|
|
@@ -1600,8 +1625,8 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
1600
1625
|
[g_overlayWindow setOpaque:NO];
|
|
1601
1626
|
[g_overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1602
1627
|
|
|
1603
|
-
// Create overlay view
|
|
1604
|
-
g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:
|
|
1628
|
+
// Create overlay view covering full screen
|
|
1629
|
+
g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:fullScreenFrame];
|
|
1605
1630
|
[g_overlayWindow setContentView:g_overlayView];
|
|
1606
1631
|
|
|
1607
1632
|
// Note: NSWindow doesn't have setWantsLayer method, only NSView does
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script to analyze overlay tracking issues during window movement
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const WindowSelector = require('./window-selector');
|
|
8
|
+
|
|
9
|
+
async function testOverlayTracking() {
|
|
10
|
+
console.log('š¬ OVERLAY TRACKING ANALYSIS');
|
|
11
|
+
console.log('=============================\n');
|
|
12
|
+
|
|
13
|
+
const selector = new WindowSelector();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Track mouse vs overlay position data
|
|
17
|
+
let mousePositions = [];
|
|
18
|
+
let overlayUpdates = [];
|
|
19
|
+
let windowMovements = [];
|
|
20
|
+
|
|
21
|
+
selector.on('windowEntered', (window) => {
|
|
22
|
+
const timestamp = Date.now();
|
|
23
|
+
overlayUpdates.push({
|
|
24
|
+
timestamp,
|
|
25
|
+
type: 'entered',
|
|
26
|
+
window: {
|
|
27
|
+
id: window.id,
|
|
28
|
+
title: window.title,
|
|
29
|
+
appName: window.appName,
|
|
30
|
+
x: window.x,
|
|
31
|
+
y: window.y,
|
|
32
|
+
width: window.width,
|
|
33
|
+
height: window.height
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log(`š [${new Date(timestamp).toISOString().substr(11,12)}] ENTERED: "${window.title}" at (${window.x}, ${window.y}) ${window.width}Ć${window.height}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
selector.on('windowLeft', (window) => {
|
|
41
|
+
const timestamp = Date.now();
|
|
42
|
+
overlayUpdates.push({
|
|
43
|
+
timestamp,
|
|
44
|
+
type: 'left',
|
|
45
|
+
window: {
|
|
46
|
+
id: window.id,
|
|
47
|
+
title: window.title,
|
|
48
|
+
x: window.x,
|
|
49
|
+
y: window.y
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(`šŖ [${new Date(timestamp).toISOString().substr(11,12)}] LEFT: "${window.title}" from (${window.x}, ${window.y})`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log('šÆ Starting window selection...');
|
|
57
|
+
console.log('š Move your cursor over windows and drag them around');
|
|
58
|
+
console.log('ā° Test will run for 30 seconds\n');
|
|
59
|
+
|
|
60
|
+
await selector.startSelection();
|
|
61
|
+
|
|
62
|
+
// Periodically log mouse position and compare with overlay state
|
|
63
|
+
const trackingInterval = setInterval(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const status = selector.getStatus();
|
|
66
|
+
const timestamp = Date.now();
|
|
67
|
+
|
|
68
|
+
if (status.nativeStatus?.currentWindow) {
|
|
69
|
+
const window = status.nativeStatus.currentWindow;
|
|
70
|
+
mousePositions.push({
|
|
71
|
+
timestamp,
|
|
72
|
+
windowId: window.id,
|
|
73
|
+
windowPos: { x: window.x, y: window.y },
|
|
74
|
+
windowSize: { width: window.width, height: window.height }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Check for window movement by comparing with previous position
|
|
78
|
+
const prevPos = mousePositions.find(p =>
|
|
79
|
+
p.windowId === window.id &&
|
|
80
|
+
p.timestamp < timestamp - 100 && // at least 100ms ago
|
|
81
|
+
(p.windowPos.x !== window.x || p.windowPos.y !== window.y)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (prevPos) {
|
|
85
|
+
windowMovements.push({
|
|
86
|
+
timestamp,
|
|
87
|
+
windowId: window.id,
|
|
88
|
+
title: window.title,
|
|
89
|
+
from: prevPos.windowPos,
|
|
90
|
+
to: { x: window.x, y: window.y },
|
|
91
|
+
deltaX: window.x - prevPos.windowPos.x,
|
|
92
|
+
deltaY: window.y - prevPos.windowPos.y
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log(`š [${new Date(timestamp).toISOString().substr(11,12)}] MOVED: "${window.title}" (${prevPos.windowPos.x}, ${prevPos.windowPos.y}) ā (${window.x}, ${window.y}) Ī(${window.x - prevPos.windowPos.x}, ${window.y - prevPos.windowPos.y})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Ignore errors during status check
|
|
100
|
+
}
|
|
101
|
+
}, 50); // Check every 50ms for high resolution tracking
|
|
102
|
+
|
|
103
|
+
// Run test for 30 seconds
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
105
|
+
|
|
106
|
+
clearInterval(trackingInterval);
|
|
107
|
+
console.log('\nā¹ļø Test completed. Analyzing data...\n');
|
|
108
|
+
|
|
109
|
+
// Analysis
|
|
110
|
+
console.log('š ANALYSIS RESULTS:');
|
|
111
|
+
console.log('====================\n');
|
|
112
|
+
|
|
113
|
+
console.log(`š Total overlay updates: ${overlayUpdates.length}`);
|
|
114
|
+
console.log(`š Total mouse position samples: ${mousePositions.length}`);
|
|
115
|
+
console.log(`š Detected window movements: ${windowMovements.length}\n`);
|
|
116
|
+
|
|
117
|
+
// Analyze window movement patterns
|
|
118
|
+
if (windowMovements.length > 0) {
|
|
119
|
+
console.log('š WINDOW MOVEMENT PATTERNS:');
|
|
120
|
+
|
|
121
|
+
const movementsByWindow = {};
|
|
122
|
+
windowMovements.forEach(move => {
|
|
123
|
+
if (!movementsByWindow[move.windowId]) {
|
|
124
|
+
movementsByWindow[move.windowId] = [];
|
|
125
|
+
}
|
|
126
|
+
movementsByWindow[move.windowId].push(move);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
for (const [windowId, moves] of Object.entries(movementsByWindow)) {
|
|
130
|
+
const firstMove = moves[0];
|
|
131
|
+
const lastMove = moves[moves.length - 1];
|
|
132
|
+
const totalDeltaX = Math.abs(lastMove.to.x - firstMove.from.x);
|
|
133
|
+
const totalDeltaY = Math.abs(lastMove.to.y - firstMove.from.y);
|
|
134
|
+
const duration = lastMove.timestamp - firstMove.timestamp;
|
|
135
|
+
|
|
136
|
+
console.log(` Window "${firstMove.title}" (ID: ${windowId}):`);
|
|
137
|
+
console.log(` Movements: ${moves.length}`);
|
|
138
|
+
console.log(` Total displacement: (${totalDeltaX}, ${totalDeltaY}) pixels`);
|
|
139
|
+
console.log(` Duration: ${duration}ms`);
|
|
140
|
+
console.log(` Average speed: ${(Math.sqrt(totalDeltaX*totalDeltaX + totalDeltaY*totalDeltaY) / (duration/1000)).toFixed(1)} px/sec\n`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Analyze update frequency
|
|
145
|
+
if (overlayUpdates.length > 1) {
|
|
146
|
+
console.log('ā±ļø OVERLAY UPDATE TIMING:');
|
|
147
|
+
const intervals = [];
|
|
148
|
+
for (let i = 1; i < overlayUpdates.length; i++) {
|
|
149
|
+
intervals.push(overlayUpdates[i].timestamp - overlayUpdates[i-1].timestamp);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (intervals.length > 0) {
|
|
153
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
154
|
+
const minInterval = Math.min(...intervals);
|
|
155
|
+
const maxInterval = Math.max(...intervals);
|
|
156
|
+
|
|
157
|
+
console.log(` Average update interval: ${avgInterval.toFixed(1)}ms (${(1000/avgInterval).toFixed(1)} FPS)`);
|
|
158
|
+
console.log(` Min interval: ${minInterval}ms`);
|
|
159
|
+
console.log(` Max interval: ${maxInterval}ms\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('ā Test failed:', error.message);
|
|
165
|
+
} finally {
|
|
166
|
+
console.log('š Cleaning up...');
|
|
167
|
+
await selector.cleanup();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (require.main === module) {
|
|
172
|
+
testOverlayTracking().catch(console.error);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = testOverlayTracking;
|