node-mac-recorder 1.4.0 → 1.6.0
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/WINDOW_SELECTOR_README.md +283 -4
- package/debug-test.js +95 -0
- package/default-auto-front-test.js +86 -0
- package/package.json +1 -1
- package/src/window_selector.mm +641 -10
- package/window-selector.js +144 -1
package/src/window_selector.mm
CHANGED
|
@@ -17,12 +17,32 @@ static NSMutableArray *g_allWindows = nil;
|
|
|
17
17
|
static NSDictionary *g_currentWindowUnderCursor = nil;
|
|
18
18
|
static bool g_bringToFrontEnabled = true; // Default enabled
|
|
19
19
|
|
|
20
|
+
// Recording preview overlay state
|
|
21
|
+
static NSWindow *g_recordingPreviewWindow = nil;
|
|
22
|
+
static NSView *g_recordingPreviewView = nil;
|
|
23
|
+
static NSDictionary *g_recordingWindowInfo = nil;
|
|
24
|
+
|
|
25
|
+
// Screen selection overlay state
|
|
26
|
+
static bool g_isScreenSelecting = false;
|
|
27
|
+
static NSMutableArray *g_screenOverlayWindows = nil;
|
|
28
|
+
static NSDictionary *g_selectedScreenInfo = nil;
|
|
29
|
+
static NSArray *g_allScreens = nil;
|
|
30
|
+
|
|
20
31
|
// Forward declarations
|
|
21
32
|
void cleanupWindowSelector();
|
|
22
33
|
void updateOverlay();
|
|
23
34
|
NSDictionary* getWindowUnderCursor(CGPoint point);
|
|
24
35
|
NSArray* getAllSelectableWindows();
|
|
25
36
|
bool bringWindowToFront(int windowId);
|
|
37
|
+
void cleanupRecordingPreview();
|
|
38
|
+
bool showRecordingPreview(NSDictionary *windowInfo);
|
|
39
|
+
bool hideRecordingPreview();
|
|
40
|
+
void cleanupScreenSelector();
|
|
41
|
+
bool startScreenSelection();
|
|
42
|
+
bool stopScreenSelection();
|
|
43
|
+
NSDictionary* getSelectedScreenInfo();
|
|
44
|
+
bool showScreenRecordingPreview(NSDictionary *screenInfo);
|
|
45
|
+
bool hideScreenRecordingPreview();
|
|
26
46
|
|
|
27
47
|
// Custom overlay view class
|
|
28
48
|
@interface WindowSelectorOverlayView : NSView
|
|
@@ -35,8 +55,8 @@ bool bringWindowToFront(int windowId);
|
|
|
35
55
|
self = [super initWithFrame:frameRect];
|
|
36
56
|
if (self) {
|
|
37
57
|
self.wantsLayer = YES;
|
|
38
|
-
self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.
|
|
39
|
-
self.layer.borderColor = [[NSColor colorWithRed:
|
|
58
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] CGColor];
|
|
59
|
+
self.layer.borderColor = [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor];
|
|
40
60
|
self.layer.borderWidth = 5.0;
|
|
41
61
|
self.layer.cornerRadius = 8.0;
|
|
42
62
|
}
|
|
@@ -49,11 +69,11 @@ bool bringWindowToFront(int windowId);
|
|
|
49
69
|
if (!self.windowInfo) return;
|
|
50
70
|
|
|
51
71
|
// Background with transparency
|
|
52
|
-
[[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.
|
|
72
|
+
[[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] setFill];
|
|
53
73
|
NSRectFill(dirtyRect);
|
|
54
74
|
|
|
55
75
|
// Border
|
|
56
|
-
[[NSColor colorWithRed:
|
|
76
|
+
[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] setStroke];
|
|
57
77
|
NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:8 yRadius:8];
|
|
58
78
|
[border setLineWidth:3.0];
|
|
59
79
|
[border stroke];
|
|
@@ -67,14 +87,120 @@ bool bringWindowToFront(int windowId);
|
|
|
67
87
|
[style setAlignment:NSTextAlignmentCenter];
|
|
68
88
|
|
|
69
89
|
NSDictionary *attributes = @{
|
|
70
|
-
NSFontAttributeName: [NSFont systemFontOfSize:
|
|
90
|
+
NSFontAttributeName: [NSFont systemFontOfSize:21 weight:NSFontWeightMedium],
|
|
91
|
+
NSForegroundColorAttributeName: [NSColor whiteColor],
|
|
92
|
+
NSParagraphStyleAttributeName: style,
|
|
93
|
+
NSStrokeColorAttributeName: [NSColor blackColor],
|
|
94
|
+
NSStrokeWidthAttributeName: @(-2.0)
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
NSRect textRect = NSMakeRect(10, self.bounds.size.height - 90, self.bounds.size.width - 20, 80);
|
|
98
|
+
[infoText drawInRect:textRect withAttributes:attributes];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@end
|
|
102
|
+
|
|
103
|
+
// Recording preview overlay view - full screen with cutout
|
|
104
|
+
@interface RecordingPreviewView : NSView
|
|
105
|
+
@property (nonatomic, strong) NSDictionary *recordingWindowInfo;
|
|
106
|
+
@end
|
|
107
|
+
|
|
108
|
+
@implementation RecordingPreviewView
|
|
109
|
+
|
|
110
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
111
|
+
self = [super initWithFrame:frameRect];
|
|
112
|
+
if (self) {
|
|
113
|
+
self.wantsLayer = YES;
|
|
114
|
+
self.layer.backgroundColor = [[NSColor clearColor] CGColor];
|
|
115
|
+
}
|
|
116
|
+
return self;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
120
|
+
[super drawRect:dirtyRect];
|
|
121
|
+
|
|
122
|
+
if (!self.recordingWindowInfo) {
|
|
123
|
+
// No window info, fill with semi-transparent black
|
|
124
|
+
[[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
|
|
125
|
+
NSRectFill(dirtyRect);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get window coordinates
|
|
130
|
+
int windowX = [[self.recordingWindowInfo objectForKey:@"x"] intValue];
|
|
131
|
+
int windowY = [[self.recordingWindowInfo objectForKey:@"y"] intValue];
|
|
132
|
+
int windowWidth = [[self.recordingWindowInfo objectForKey:@"width"] intValue];
|
|
133
|
+
int windowHeight = [[self.recordingWindowInfo objectForKey:@"height"] intValue];
|
|
134
|
+
|
|
135
|
+
// Convert from CGWindow coordinates (top-left) to NSView coordinates (bottom-left)
|
|
136
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
137
|
+
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
138
|
+
CGFloat convertedY = screenHeight - windowY - windowHeight;
|
|
139
|
+
|
|
140
|
+
NSRect windowRect = NSMakeRect(windowX, convertedY, windowWidth, windowHeight);
|
|
141
|
+
|
|
142
|
+
// Fill entire view with semi-transparent black
|
|
143
|
+
[[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
|
|
144
|
+
NSRectFill(self.bounds);
|
|
145
|
+
|
|
146
|
+
// Cut out the window area (make it transparent)
|
|
147
|
+
[[NSColor clearColor] setFill];
|
|
148
|
+
NSRectFill(windowRect);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@end
|
|
152
|
+
|
|
153
|
+
// Screen selection overlay view
|
|
154
|
+
@interface ScreenSelectorOverlayView : NSView
|
|
155
|
+
@property (nonatomic, strong) NSDictionary *screenInfo;
|
|
156
|
+
@end
|
|
157
|
+
|
|
158
|
+
@implementation ScreenSelectorOverlayView
|
|
159
|
+
|
|
160
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
161
|
+
self = [super initWithFrame:frameRect];
|
|
162
|
+
if (self) {
|
|
163
|
+
self.wantsLayer = YES;
|
|
164
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] CGColor];
|
|
165
|
+
self.layer.borderColor = [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor];
|
|
166
|
+
self.layer.borderWidth = 5.0;
|
|
167
|
+
self.layer.cornerRadius = 8.0;
|
|
168
|
+
}
|
|
169
|
+
return self;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
173
|
+
[super drawRect:dirtyRect];
|
|
174
|
+
|
|
175
|
+
if (!self.screenInfo) return;
|
|
176
|
+
|
|
177
|
+
// Background with transparency
|
|
178
|
+
[[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] setFill];
|
|
179
|
+
NSRectFill(dirtyRect);
|
|
180
|
+
|
|
181
|
+
// Border
|
|
182
|
+
[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] setStroke];
|
|
183
|
+
NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:8 yRadius:8];
|
|
184
|
+
[border setLineWidth:3.0];
|
|
185
|
+
[border stroke];
|
|
186
|
+
|
|
187
|
+
// Screen info text
|
|
188
|
+
NSString *screenName = [self.screenInfo objectForKey:@"name"] ?: @"Unknown Screen";
|
|
189
|
+
NSString *resolution = [self.screenInfo objectForKey:@"resolution"] ?: @"Unknown Resolution";
|
|
190
|
+
NSString *infoText = [NSString stringWithFormat:@"%@\n%@", screenName, resolution];
|
|
191
|
+
|
|
192
|
+
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
|
193
|
+
[style setAlignment:NSTextAlignmentCenter];
|
|
194
|
+
|
|
195
|
+
NSDictionary *attributes = @{
|
|
196
|
+
NSFontAttributeName: [NSFont systemFontOfSize:21 weight:NSFontWeightMedium],
|
|
71
197
|
NSForegroundColorAttributeName: [NSColor whiteColor],
|
|
72
198
|
NSParagraphStyleAttributeName: style,
|
|
73
199
|
NSStrokeColorAttributeName: [NSColor blackColor],
|
|
74
200
|
NSStrokeWidthAttributeName: @(-2.0)
|
|
75
201
|
};
|
|
76
202
|
|
|
77
|
-
NSRect textRect = NSMakeRect(10, self.bounds.size.height -
|
|
203
|
+
NSRect textRect = NSMakeRect(10, self.bounds.size.height - 90, self.bounds.size.width - 20, 80);
|
|
78
204
|
[infoText drawInRect:textRect withAttributes:attributes];
|
|
79
205
|
}
|
|
80
206
|
|
|
@@ -83,6 +209,7 @@ bool bringWindowToFront(int windowId);
|
|
|
83
209
|
// Button action handler and timer target
|
|
84
210
|
@interface WindowSelectorDelegate : NSObject
|
|
85
211
|
- (void)selectButtonClicked:(id)sender;
|
|
212
|
+
- (void)screenSelectButtonClicked:(id)sender;
|
|
86
213
|
- (void)timerUpdate:(NSTimer *)timer;
|
|
87
214
|
@end
|
|
88
215
|
|
|
@@ -94,6 +221,18 @@ bool bringWindowToFront(int windowId);
|
|
|
94
221
|
}
|
|
95
222
|
}
|
|
96
223
|
|
|
224
|
+
- (void)screenSelectButtonClicked:(id)sender {
|
|
225
|
+
// Get the screen info from the button's superview
|
|
226
|
+
NSView *overlayView = [[sender superview] superview];
|
|
227
|
+
if ([overlayView isKindOfClass:[ScreenSelectorOverlayView class]]) {
|
|
228
|
+
ScreenSelectorOverlayView *screenView = (ScreenSelectorOverlayView *)overlayView;
|
|
229
|
+
if (screenView.screenInfo) {
|
|
230
|
+
g_selectedScreenInfo = [screenView.screenInfo retain];
|
|
231
|
+
cleanupScreenSelector();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
97
236
|
- (void)timerUpdate:(NSTimer *)timer {
|
|
98
237
|
updateOverlay();
|
|
99
238
|
}
|
|
@@ -313,7 +452,14 @@ void updateOverlay() {
|
|
|
313
452
|
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
314
453
|
CGFloat adjustedY = screenHeight - y - height;
|
|
315
454
|
|
|
316
|
-
|
|
455
|
+
// Clamp overlay to screen bounds to avoid partial off-screen issues
|
|
456
|
+
NSRect screenFrame = [mainScreen frame];
|
|
457
|
+
CGFloat clampedX = MAX(screenFrame.origin.x, MIN(x, screenFrame.origin.x + screenFrame.size.width - width));
|
|
458
|
+
CGFloat clampedY = MAX(screenFrame.origin.y, MIN(adjustedY, screenFrame.origin.y + screenFrame.size.height - height));
|
|
459
|
+
CGFloat clampedWidth = MIN(width, screenFrame.size.width - (clampedX - screenFrame.origin.x));
|
|
460
|
+
CGFloat clampedHeight = MIN(height, screenFrame.size.height - (clampedY - screenFrame.origin.y));
|
|
461
|
+
|
|
462
|
+
NSRect overlayFrame = NSMakeRect(clampedX, clampedY, clampedWidth, clampedHeight);
|
|
317
463
|
|
|
318
464
|
NSString *windowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Untitled";
|
|
319
465
|
NSString *appName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
|
|
@@ -407,6 +553,288 @@ void cleanupWindowSelector() {
|
|
|
407
553
|
}
|
|
408
554
|
}
|
|
409
555
|
|
|
556
|
+
// Recording preview functions
|
|
557
|
+
void cleanupRecordingPreview() {
|
|
558
|
+
if (g_recordingPreviewWindow) {
|
|
559
|
+
[g_recordingPreviewWindow close];
|
|
560
|
+
g_recordingPreviewWindow = nil;
|
|
561
|
+
g_recordingPreviewView = nil;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (g_recordingWindowInfo) {
|
|
565
|
+
[g_recordingWindowInfo release];
|
|
566
|
+
g_recordingWindowInfo = nil;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
bool showRecordingPreview(NSDictionary *windowInfo) {
|
|
571
|
+
@try {
|
|
572
|
+
// Clean up any existing preview
|
|
573
|
+
cleanupRecordingPreview();
|
|
574
|
+
|
|
575
|
+
if (!windowInfo) return false;
|
|
576
|
+
|
|
577
|
+
// Store window info
|
|
578
|
+
g_recordingWindowInfo = [windowInfo retain];
|
|
579
|
+
|
|
580
|
+
// Get main screen bounds for full screen overlay
|
|
581
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
582
|
+
NSRect screenFrame = [mainScreen frame];
|
|
583
|
+
|
|
584
|
+
// Create full-screen overlay window
|
|
585
|
+
g_recordingPreviewWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
586
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
587
|
+
backing:NSBackingStoreBuffered
|
|
588
|
+
defer:NO];
|
|
589
|
+
|
|
590
|
+
[g_recordingPreviewWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)]; // High level but below selection
|
|
591
|
+
[g_recordingPreviewWindow setOpaque:NO];
|
|
592
|
+
[g_recordingPreviewWindow setBackgroundColor:[NSColor clearColor]];
|
|
593
|
+
[g_recordingPreviewWindow setIgnoresMouseEvents:YES]; // Don't interfere with user interaction
|
|
594
|
+
[g_recordingPreviewWindow setAcceptsMouseMovedEvents:NO];
|
|
595
|
+
[g_recordingPreviewWindow setHasShadow:NO];
|
|
596
|
+
[g_recordingPreviewWindow setAlphaValue:1.0];
|
|
597
|
+
[g_recordingPreviewWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
598
|
+
|
|
599
|
+
// Create preview view
|
|
600
|
+
g_recordingPreviewView = [[RecordingPreviewView alloc] initWithFrame:screenFrame];
|
|
601
|
+
[(RecordingPreviewView *)g_recordingPreviewView setRecordingWindowInfo:windowInfo];
|
|
602
|
+
[g_recordingPreviewWindow setContentView:g_recordingPreviewView];
|
|
603
|
+
|
|
604
|
+
// Show the preview
|
|
605
|
+
[g_recordingPreviewWindow orderFront:nil];
|
|
606
|
+
[g_recordingPreviewWindow makeKeyAndOrderFront:nil];
|
|
607
|
+
|
|
608
|
+
NSLog(@"🎬 RECORDING PREVIEW: Showing overlay for %@ - \"%@\"",
|
|
609
|
+
[windowInfo objectForKey:@"appName"],
|
|
610
|
+
[windowInfo objectForKey:@"title"]);
|
|
611
|
+
|
|
612
|
+
return true;
|
|
613
|
+
|
|
614
|
+
} @catch (NSException *exception) {
|
|
615
|
+
NSLog(@"❌ Error showing recording preview: %@", exception);
|
|
616
|
+
cleanupRecordingPreview();
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
bool hideRecordingPreview() {
|
|
622
|
+
@try {
|
|
623
|
+
if (g_recordingPreviewWindow) {
|
|
624
|
+
NSLog(@"🎬 RECORDING PREVIEW: Hiding overlay");
|
|
625
|
+
cleanupRecordingPreview();
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
|
|
630
|
+
} @catch (NSException *exception) {
|
|
631
|
+
NSLog(@"❌ Error hiding recording preview: %@", exception);
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Screen selection functions
|
|
637
|
+
void cleanupScreenSelector() {
|
|
638
|
+
g_isScreenSelecting = false;
|
|
639
|
+
|
|
640
|
+
// Close all screen overlay windows
|
|
641
|
+
if (g_screenOverlayWindows) {
|
|
642
|
+
for (NSWindow *overlayWindow in g_screenOverlayWindows) {
|
|
643
|
+
[overlayWindow close];
|
|
644
|
+
}
|
|
645
|
+
[g_screenOverlayWindows release];
|
|
646
|
+
g_screenOverlayWindows = nil;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Clean up screen data
|
|
650
|
+
if (g_allScreens) {
|
|
651
|
+
[g_allScreens release];
|
|
652
|
+
g_allScreens = nil;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
bool startScreenSelection() {
|
|
657
|
+
@try {
|
|
658
|
+
if (g_isScreenSelecting) return false;
|
|
659
|
+
|
|
660
|
+
// Get all available screens
|
|
661
|
+
NSArray *screens = [NSScreen screens];
|
|
662
|
+
if (!screens || [screens count] == 0) return false;
|
|
663
|
+
|
|
664
|
+
// Create screen info array
|
|
665
|
+
NSMutableArray *screenInfoArray = [[NSMutableArray alloc] init];
|
|
666
|
+
g_screenOverlayWindows = [[NSMutableArray alloc] init];
|
|
667
|
+
|
|
668
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
669
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
670
|
+
NSRect screenFrame = [screen frame];
|
|
671
|
+
|
|
672
|
+
// Create screen info dictionary
|
|
673
|
+
NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
|
|
674
|
+
[screenInfo setObject:[NSNumber numberWithInteger:i] forKey:@"id"];
|
|
675
|
+
[screenInfo setObject:[NSString stringWithFormat:@"Display %ld", (long)(i + 1)] forKey:@"name"];
|
|
676
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.x] forKey:@"x"];
|
|
677
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.y] forKey:@"y"];
|
|
678
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.width] forKey:@"width"];
|
|
679
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.height] forKey:@"height"];
|
|
680
|
+
[screenInfo setObject:[NSString stringWithFormat:@"%.0fx%.0f", screenFrame.size.width, screenFrame.size.height] forKey:@"resolution"];
|
|
681
|
+
[screenInfo setObject:[NSNumber numberWithBool:(i == 0)] forKey:@"isPrimary"]; // First screen is primary
|
|
682
|
+
[screenInfoArray addObject:screenInfo];
|
|
683
|
+
|
|
684
|
+
// Create overlay window for this screen (FULL screen including menu bar)
|
|
685
|
+
NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
686
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
687
|
+
backing:NSBackingStoreBuffered
|
|
688
|
+
defer:NO
|
|
689
|
+
screen:screen];
|
|
690
|
+
|
|
691
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
|
|
692
|
+
[overlayWindow setOpaque:NO];
|
|
693
|
+
[overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
694
|
+
[overlayWindow setIgnoresMouseEvents:NO];
|
|
695
|
+
[overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
696
|
+
[overlayWindow setHasShadow:NO];
|
|
697
|
+
[overlayWindow setAlphaValue:1.0];
|
|
698
|
+
[overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
699
|
+
|
|
700
|
+
// Create overlay view
|
|
701
|
+
ScreenSelectorOverlayView *overlayView = [[ScreenSelectorOverlayView alloc] initWithFrame:screenFrame];
|
|
702
|
+
[overlayView setScreenInfo:screenInfo];
|
|
703
|
+
[overlayWindow setContentView:overlayView];
|
|
704
|
+
|
|
705
|
+
// Create select button
|
|
706
|
+
NSButton *selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 180, 60)];
|
|
707
|
+
[selectButton setTitle:@"Select Screen"];
|
|
708
|
+
[selectButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
709
|
+
[selectButton setBezelStyle:NSBezelStyleRounded];
|
|
710
|
+
[selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]];
|
|
711
|
+
[selectButton setWantsLayer:NO]; // Use system default
|
|
712
|
+
|
|
713
|
+
// Add shadow for better visibility
|
|
714
|
+
[selectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
715
|
+
[selectButton.layer setShadowOffset:NSMakeSize(0, -2)];
|
|
716
|
+
[selectButton.layer setShadowRadius:4.0];
|
|
717
|
+
[selectButton.layer setShadowOpacity:0.3];
|
|
718
|
+
|
|
719
|
+
// Set button target and action
|
|
720
|
+
WindowSelectorDelegate *delegate = [[WindowSelectorDelegate alloc] init];
|
|
721
|
+
[selectButton setTarget:delegate];
|
|
722
|
+
[selectButton setAction:@selector(screenSelectButtonClicked:)];
|
|
723
|
+
|
|
724
|
+
// Position button in center of screen
|
|
725
|
+
NSPoint buttonCenter = NSMakePoint(
|
|
726
|
+
(screenFrame.size.width - [selectButton frame].size.width) / 2,
|
|
727
|
+
(screenFrame.size.height - [selectButton frame].size.height) / 2
|
|
728
|
+
);
|
|
729
|
+
[selectButton setFrameOrigin:buttonCenter];
|
|
730
|
+
|
|
731
|
+
[overlayView addSubview:selectButton];
|
|
732
|
+
[overlayWindow orderFront:nil];
|
|
733
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
734
|
+
|
|
735
|
+
[g_screenOverlayWindows addObject:overlayWindow];
|
|
736
|
+
[screenInfo release];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
g_allScreens = [screenInfoArray retain];
|
|
740
|
+
[screenInfoArray release];
|
|
741
|
+
g_isScreenSelecting = true;
|
|
742
|
+
|
|
743
|
+
NSLog(@"🖥️ SCREEN SELECTION: Started with %lu screens", (unsigned long)[screens count]);
|
|
744
|
+
|
|
745
|
+
return true;
|
|
746
|
+
|
|
747
|
+
} @catch (NSException *exception) {
|
|
748
|
+
NSLog(@"❌ Error starting screen selection: %@", exception);
|
|
749
|
+
cleanupScreenSelector();
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
bool stopScreenSelection() {
|
|
755
|
+
@try {
|
|
756
|
+
if (!g_isScreenSelecting) return false;
|
|
757
|
+
|
|
758
|
+
cleanupScreenSelector();
|
|
759
|
+
NSLog(@"🖥️ SCREEN SELECTION: Stopped");
|
|
760
|
+
return true;
|
|
761
|
+
|
|
762
|
+
} @catch (NSException *exception) {
|
|
763
|
+
NSLog(@"❌ Error stopping screen selection: %@", exception);
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
NSDictionary* getSelectedScreenInfo() {
|
|
769
|
+
if (!g_selectedScreenInfo) return nil;
|
|
770
|
+
|
|
771
|
+
NSDictionary *result = [g_selectedScreenInfo retain];
|
|
772
|
+
[g_selectedScreenInfo release];
|
|
773
|
+
g_selectedScreenInfo = nil;
|
|
774
|
+
|
|
775
|
+
return [result autorelease];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
bool showScreenRecordingPreview(NSDictionary *screenInfo) {
|
|
779
|
+
@try {
|
|
780
|
+
// Clean up any existing preview
|
|
781
|
+
cleanupRecordingPreview();
|
|
782
|
+
|
|
783
|
+
if (!screenInfo) return false;
|
|
784
|
+
|
|
785
|
+
// For screen recording preview, we show all OTHER screens as black overlay
|
|
786
|
+
// and keep the selected screen transparent
|
|
787
|
+
NSArray *screens = [NSScreen screens];
|
|
788
|
+
if (!screens || [screens count] == 0) return false;
|
|
789
|
+
|
|
790
|
+
int selectedScreenId = [[screenInfo objectForKey:@"id"] intValue];
|
|
791
|
+
|
|
792
|
+
// Create overlay for each screen except the selected one
|
|
793
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
794
|
+
if (i == selectedScreenId) continue; // Skip selected screen
|
|
795
|
+
|
|
796
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
797
|
+
NSRect screenFrame = [screen frame];
|
|
798
|
+
|
|
799
|
+
// Create full-screen black overlay for non-selected screens
|
|
800
|
+
NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
801
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
802
|
+
backing:NSBackingStoreBuffered
|
|
803
|
+
defer:NO
|
|
804
|
+
screen:screen];
|
|
805
|
+
|
|
806
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)];
|
|
807
|
+
[overlayWindow setOpaque:NO];
|
|
808
|
+
[overlayWindow setBackgroundColor:[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5]];
|
|
809
|
+
[overlayWindow setIgnoresMouseEvents:YES];
|
|
810
|
+
[overlayWindow setAcceptsMouseMovedEvents:NO];
|
|
811
|
+
[overlayWindow setHasShadow:NO];
|
|
812
|
+
[overlayWindow setAlphaValue:1.0];
|
|
813
|
+
[overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
814
|
+
|
|
815
|
+
[overlayWindow orderFront:nil];
|
|
816
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
817
|
+
|
|
818
|
+
// Store for cleanup (reuse recording preview window variable)
|
|
819
|
+
if (!g_recordingPreviewWindow) {
|
|
820
|
+
g_recordingPreviewWindow = overlayWindow;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
NSLog(@"🎬 SCREEN RECORDING PREVIEW: Showing overlay for Screen %d", selectedScreenId);
|
|
825
|
+
|
|
826
|
+
return true;
|
|
827
|
+
|
|
828
|
+
} @catch (NSException *exception) {
|
|
829
|
+
NSLog(@"❌ Error showing screen recording preview: %@", exception);
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
bool hideScreenRecordingPreview() {
|
|
835
|
+
return hideRecordingPreview(); // Reuse existing function
|
|
836
|
+
}
|
|
837
|
+
|
|
410
838
|
// NAPI Function: Start Window Selection
|
|
411
839
|
Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
412
840
|
Napi::Env env = info.Env();
|
|
@@ -445,12 +873,21 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
|
445
873
|
g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:initialFrame];
|
|
446
874
|
[g_overlayWindow setContentView:g_overlayView];
|
|
447
875
|
|
|
448
|
-
// Create select button
|
|
449
|
-
g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0,
|
|
876
|
+
// Create select button with blue theme
|
|
877
|
+
g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 160, 60)];
|
|
450
878
|
[g_selectButton setTitle:@"Select Window"];
|
|
451
879
|
[g_selectButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
452
880
|
[g_selectButton setBezelStyle:NSBezelStyleRounded];
|
|
453
|
-
[g_selectButton setFont:[NSFont systemFontOfSize:
|
|
881
|
+
[g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]];
|
|
882
|
+
|
|
883
|
+
// Remove custom button styling to use system default
|
|
884
|
+
[g_selectButton setWantsLayer:NO];
|
|
885
|
+
|
|
886
|
+
// Add shadow for better visibility
|
|
887
|
+
[g_selectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
888
|
+
[g_selectButton.layer setShadowOffset:NSMakeSize(0, -2)];
|
|
889
|
+
[g_selectButton.layer setShadowRadius:4.0];
|
|
890
|
+
[g_selectButton.layer setShadowOpacity:0.3];
|
|
454
891
|
|
|
455
892
|
// Create delegate for button action and timer
|
|
456
893
|
g_delegate = [[WindowSelectorDelegate alloc] init];
|
|
@@ -627,6 +1064,193 @@ Napi::Value GetWindowSelectionStatus(const Napi::CallbackInfo& info) {
|
|
|
627
1064
|
return result;
|
|
628
1065
|
}
|
|
629
1066
|
|
|
1067
|
+
// NAPI Function: Show Recording Preview
|
|
1068
|
+
Napi::Value ShowRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1069
|
+
Napi::Env env = info.Env();
|
|
1070
|
+
|
|
1071
|
+
if (info.Length() < 1) {
|
|
1072
|
+
Napi::TypeError::New(env, "Window info object required").ThrowAsJavaScriptException();
|
|
1073
|
+
return env.Null();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (!info[0].IsObject()) {
|
|
1077
|
+
Napi::TypeError::New(env, "Window info must be an object").ThrowAsJavaScriptException();
|
|
1078
|
+
return env.Null();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
@try {
|
|
1082
|
+
Napi::Object windowInfoObj = info[0].As<Napi::Object>();
|
|
1083
|
+
|
|
1084
|
+
// Convert NAPI object to NSDictionary
|
|
1085
|
+
NSMutableDictionary *windowInfo = [[NSMutableDictionary alloc] init];
|
|
1086
|
+
|
|
1087
|
+
if (windowInfoObj.Has("id")) {
|
|
1088
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
|
|
1089
|
+
}
|
|
1090
|
+
if (windowInfoObj.Has("title")) {
|
|
1091
|
+
[windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("title").As<Napi::String>().Utf8Value().c_str()] forKey:@"title"];
|
|
1092
|
+
}
|
|
1093
|
+
if (windowInfoObj.Has("appName")) {
|
|
1094
|
+
[windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("appName").As<Napi::String>().Utf8Value().c_str()] forKey:@"appName"];
|
|
1095
|
+
}
|
|
1096
|
+
if (windowInfoObj.Has("x")) {
|
|
1097
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
|
|
1098
|
+
}
|
|
1099
|
+
if (windowInfoObj.Has("y")) {
|
|
1100
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
|
|
1101
|
+
}
|
|
1102
|
+
if (windowInfoObj.Has("width")) {
|
|
1103
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
|
|
1104
|
+
}
|
|
1105
|
+
if (windowInfoObj.Has("height")) {
|
|
1106
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
bool success = showRecordingPreview(windowInfo);
|
|
1110
|
+
[windowInfo release];
|
|
1111
|
+
|
|
1112
|
+
return Napi::Boolean::New(env, success);
|
|
1113
|
+
|
|
1114
|
+
} @catch (NSException *exception) {
|
|
1115
|
+
return Napi::Boolean::New(env, false);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// NAPI Function: Hide Recording Preview
|
|
1120
|
+
Napi::Value HideRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1121
|
+
Napi::Env env = info.Env();
|
|
1122
|
+
|
|
1123
|
+
@try {
|
|
1124
|
+
bool success = hideRecordingPreview();
|
|
1125
|
+
return Napi::Boolean::New(env, success);
|
|
1126
|
+
|
|
1127
|
+
} @catch (NSException *exception) {
|
|
1128
|
+
return Napi::Boolean::New(env, false);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// NAPI Function: Start Screen Selection
|
|
1133
|
+
Napi::Value StartScreenSelection(const Napi::CallbackInfo& info) {
|
|
1134
|
+
Napi::Env env = info.Env();
|
|
1135
|
+
|
|
1136
|
+
@try {
|
|
1137
|
+
bool success = startScreenSelection();
|
|
1138
|
+
return Napi::Boolean::New(env, success);
|
|
1139
|
+
|
|
1140
|
+
} @catch (NSException *exception) {
|
|
1141
|
+
return Napi::Boolean::New(env, false);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// NAPI Function: Stop Screen Selection
|
|
1146
|
+
Napi::Value StopScreenSelection(const Napi::CallbackInfo& info) {
|
|
1147
|
+
Napi::Env env = info.Env();
|
|
1148
|
+
|
|
1149
|
+
@try {
|
|
1150
|
+
bool success = stopScreenSelection();
|
|
1151
|
+
return Napi::Boolean::New(env, success);
|
|
1152
|
+
|
|
1153
|
+
} @catch (NSException *exception) {
|
|
1154
|
+
return Napi::Boolean::New(env, false);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// NAPI Function: Get Selected Screen Info
|
|
1159
|
+
Napi::Value GetSelectedScreenInfo(const Napi::CallbackInfo& info) {
|
|
1160
|
+
Napi::Env env = info.Env();
|
|
1161
|
+
|
|
1162
|
+
@try {
|
|
1163
|
+
NSDictionary *screenInfo = getSelectedScreenInfo();
|
|
1164
|
+
if (!screenInfo) {
|
|
1165
|
+
return env.Null();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1169
|
+
result.Set("id", Napi::Number::New(env, [[screenInfo objectForKey:@"id"] intValue]));
|
|
1170
|
+
result.Set("name", Napi::String::New(env, [[screenInfo objectForKey:@"name"] UTF8String]));
|
|
1171
|
+
result.Set("x", Napi::Number::New(env, [[screenInfo objectForKey:@"x"] intValue]));
|
|
1172
|
+
result.Set("y", Napi::Number::New(env, [[screenInfo objectForKey:@"y"] intValue]));
|
|
1173
|
+
result.Set("width", Napi::Number::New(env, [[screenInfo objectForKey:@"width"] intValue]));
|
|
1174
|
+
result.Set("height", Napi::Number::New(env, [[screenInfo objectForKey:@"height"] intValue]));
|
|
1175
|
+
result.Set("resolution", Napi::String::New(env, [[screenInfo objectForKey:@"resolution"] UTF8String]));
|
|
1176
|
+
result.Set("isPrimary", Napi::Boolean::New(env, [[screenInfo objectForKey:@"isPrimary"] boolValue]));
|
|
1177
|
+
|
|
1178
|
+
NSLog(@"🖥️ SCREEN SELECTED: %@ (%@)",
|
|
1179
|
+
[screenInfo objectForKey:@"name"],
|
|
1180
|
+
[screenInfo objectForKey:@"resolution"]);
|
|
1181
|
+
|
|
1182
|
+
return result;
|
|
1183
|
+
|
|
1184
|
+
} @catch (NSException *exception) {
|
|
1185
|
+
return env.Null();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// NAPI Function: Show Screen Recording Preview
|
|
1190
|
+
Napi::Value ShowScreenRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1191
|
+
Napi::Env env = info.Env();
|
|
1192
|
+
|
|
1193
|
+
if (info.Length() < 1) {
|
|
1194
|
+
Napi::TypeError::New(env, "Screen info object required").ThrowAsJavaScriptException();
|
|
1195
|
+
return env.Null();
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (!info[0].IsObject()) {
|
|
1199
|
+
Napi::TypeError::New(env, "Screen info must be an object").ThrowAsJavaScriptException();
|
|
1200
|
+
return env.Null();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
@try {
|
|
1204
|
+
Napi::Object screenInfoObj = info[0].As<Napi::Object>();
|
|
1205
|
+
|
|
1206
|
+
// Convert NAPI object to NSDictionary
|
|
1207
|
+
NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
|
|
1208
|
+
|
|
1209
|
+
if (screenInfoObj.Has("id")) {
|
|
1210
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
|
|
1211
|
+
}
|
|
1212
|
+
if (screenInfoObj.Has("name")) {
|
|
1213
|
+
[screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("name").As<Napi::String>().Utf8Value().c_str()] forKey:@"name"];
|
|
1214
|
+
}
|
|
1215
|
+
if (screenInfoObj.Has("resolution")) {
|
|
1216
|
+
[screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("resolution").As<Napi::String>().Utf8Value().c_str()] forKey:@"resolution"];
|
|
1217
|
+
}
|
|
1218
|
+
if (screenInfoObj.Has("x")) {
|
|
1219
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
|
|
1220
|
+
}
|
|
1221
|
+
if (screenInfoObj.Has("y")) {
|
|
1222
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
|
|
1223
|
+
}
|
|
1224
|
+
if (screenInfoObj.Has("width")) {
|
|
1225
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
|
|
1226
|
+
}
|
|
1227
|
+
if (screenInfoObj.Has("height")) {
|
|
1228
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
bool success = showScreenRecordingPreview(screenInfo);
|
|
1232
|
+
[screenInfo release];
|
|
1233
|
+
|
|
1234
|
+
return Napi::Boolean::New(env, success);
|
|
1235
|
+
|
|
1236
|
+
} @catch (NSException *exception) {
|
|
1237
|
+
return Napi::Boolean::New(env, false);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// NAPI Function: Hide Screen Recording Preview
|
|
1242
|
+
Napi::Value HideScreenRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1243
|
+
Napi::Env env = info.Env();
|
|
1244
|
+
|
|
1245
|
+
@try {
|
|
1246
|
+
bool success = hideScreenRecordingPreview();
|
|
1247
|
+
return Napi::Boolean::New(env, success);
|
|
1248
|
+
|
|
1249
|
+
} @catch (NSException *exception) {
|
|
1250
|
+
return Napi::Boolean::New(env, false);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
630
1254
|
// Export functions
|
|
631
1255
|
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
|
|
632
1256
|
exports.Set("startWindowSelection", Napi::Function::New(env, StartWindowSelection));
|
|
@@ -635,6 +1259,13 @@ Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
|
|
|
635
1259
|
exports.Set("getWindowSelectionStatus", Napi::Function::New(env, GetWindowSelectionStatus));
|
|
636
1260
|
exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
|
|
637
1261
|
exports.Set("setBringToFrontEnabled", Napi::Function::New(env, SetBringToFrontEnabled));
|
|
1262
|
+
exports.Set("showRecordingPreview", Napi::Function::New(env, ShowRecordingPreview));
|
|
1263
|
+
exports.Set("hideRecordingPreview", Napi::Function::New(env, HideRecordingPreview));
|
|
1264
|
+
exports.Set("startScreenSelection", Napi::Function::New(env, StartScreenSelection));
|
|
1265
|
+
exports.Set("stopScreenSelection", Napi::Function::New(env, StopScreenSelection));
|
|
1266
|
+
exports.Set("getSelectedScreenInfo", Napi::Function::New(env, GetSelectedScreenInfo));
|
|
1267
|
+
exports.Set("showScreenRecordingPreview", Napi::Function::New(env, ShowScreenRecordingPreview));
|
|
1268
|
+
exports.Set("hideScreenRecordingPreview", Napi::Function::New(env, HideScreenRecordingPreview));
|
|
638
1269
|
|
|
639
1270
|
return exports;
|
|
640
1271
|
}
|