node-mac-recorder 2.1.3 → 2.2.1
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/.claude/settings.local.json +6 -1
- package/backup/binding.gyp +44 -0
- package/backup/src/audio_capture.mm +116 -0
- package/backup/src/cursor_tracker.mm +518 -0
- package/backup/src/mac_recorder.mm +829 -0
- package/backup/src/screen_capture.h +19 -0
- package/backup/src/screen_capture.mm +162 -0
- package/backup/src/screen_capture_kit.h +15 -0
- package/backup/src/window_selector.mm +1457 -0
- package/binding.gyp +20 -4
- package/index.js +14 -0
- package/package.json +1 -1
- package/src/audio_capture.mm +96 -40
- package/src/cursor_tracker.mm +3 -4
- package/src/mac_recorder.mm +669 -419
- package/src/screen_capture.h +5 -0
- package/src/screen_capture.mm +150 -53
- package/src/window_selector.mm +12 -22
- package/test-api-compatibility.js +92 -0
- package/test-audio.js +94 -0
- package/test-comprehensive.js +164 -0
- package/test-recording.js +142 -0
- package/test-sck-simple.js +37 -0
- package/test-sck.js +60 -1
- package/test-sync.js +52 -0
- package/test-windows.js +57 -0
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
#import <napi.h>
|
|
2
|
+
#import <AppKit/AppKit.h>
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
5
|
+
#import <ApplicationServices/ApplicationServices.h>
|
|
6
|
+
#import <Carbon/Carbon.h>
|
|
7
|
+
#import <Accessibility/Accessibility.h>
|
|
8
|
+
|
|
9
|
+
// Global state for window selection
|
|
10
|
+
static bool g_isWindowSelecting = false;
|
|
11
|
+
static NSWindow *g_overlayWindow = nil;
|
|
12
|
+
static NSView *g_overlayView = nil;
|
|
13
|
+
static NSButton *g_selectButton = nil;
|
|
14
|
+
static NSTimer *g_trackingTimer = nil;
|
|
15
|
+
static NSDictionary *g_selectedWindowInfo = nil;
|
|
16
|
+
static NSMutableArray *g_allWindows = nil;
|
|
17
|
+
static NSDictionary *g_currentWindowUnderCursor = nil;
|
|
18
|
+
static bool g_bringToFrontEnabled = true; // Default enabled
|
|
19
|
+
static id g_windowKeyEventMonitor = nil;
|
|
20
|
+
|
|
21
|
+
// Recording preview overlay state
|
|
22
|
+
static NSWindow *g_recordingPreviewWindow = nil;
|
|
23
|
+
static NSView *g_recordingPreviewView = nil;
|
|
24
|
+
static NSDictionary *g_recordingWindowInfo = nil;
|
|
25
|
+
|
|
26
|
+
// Screen selection overlay state
|
|
27
|
+
static bool g_isScreenSelecting = false;
|
|
28
|
+
static NSMutableArray *g_screenOverlayWindows = nil;
|
|
29
|
+
static NSDictionary *g_selectedScreenInfo = nil;
|
|
30
|
+
static NSArray *g_allScreens = nil;
|
|
31
|
+
static id g_screenKeyEventMonitor = nil;
|
|
32
|
+
|
|
33
|
+
// Forward declarations
|
|
34
|
+
void cleanupWindowSelector();
|
|
35
|
+
void updateOverlay();
|
|
36
|
+
NSDictionary* getWindowUnderCursor(CGPoint point);
|
|
37
|
+
NSArray* getAllSelectableWindows();
|
|
38
|
+
bool bringWindowToFront(int windowId);
|
|
39
|
+
void cleanupRecordingPreview();
|
|
40
|
+
bool showRecordingPreview(NSDictionary *windowInfo);
|
|
41
|
+
bool hideRecordingPreview();
|
|
42
|
+
void cleanupScreenSelector();
|
|
43
|
+
bool startScreenSelection();
|
|
44
|
+
bool stopScreenSelection();
|
|
45
|
+
NSDictionary* getSelectedScreenInfo();
|
|
46
|
+
bool showScreenRecordingPreview(NSDictionary *screenInfo);
|
|
47
|
+
bool hideScreenRecordingPreview();
|
|
48
|
+
|
|
49
|
+
// Custom overlay view class
|
|
50
|
+
@interface WindowSelectorOverlayView : NSView
|
|
51
|
+
@property (nonatomic, strong) NSDictionary *windowInfo;
|
|
52
|
+
@end
|
|
53
|
+
|
|
54
|
+
@implementation WindowSelectorOverlayView
|
|
55
|
+
|
|
56
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
57
|
+
self = [super initWithFrame:frameRect];
|
|
58
|
+
if (self) {
|
|
59
|
+
self.wantsLayer = YES;
|
|
60
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] CGColor];
|
|
61
|
+
self.layer.borderColor = [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor];
|
|
62
|
+
self.layer.borderWidth = 5.0;
|
|
63
|
+
self.layer.cornerRadius = 8.0;
|
|
64
|
+
}
|
|
65
|
+
return self;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
69
|
+
[super drawRect:dirtyRect];
|
|
70
|
+
|
|
71
|
+
if (!self.windowInfo) return;
|
|
72
|
+
|
|
73
|
+
// Background with transparency
|
|
74
|
+
[[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] setFill];
|
|
75
|
+
NSRectFill(dirtyRect);
|
|
76
|
+
|
|
77
|
+
// Border
|
|
78
|
+
[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] setStroke];
|
|
79
|
+
NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:8 yRadius:8];
|
|
80
|
+
[border setLineWidth:3.0];
|
|
81
|
+
[border stroke];
|
|
82
|
+
|
|
83
|
+
// Window info text
|
|
84
|
+
NSString *windowTitle = [self.windowInfo objectForKey:@"title"] ?: @"Unknown Window";
|
|
85
|
+
NSString *appName = [self.windowInfo objectForKey:@"appName"] ?: @"Unknown App";
|
|
86
|
+
NSString *infoText = [NSString stringWithFormat:@"%@\n%@", appName, windowTitle];
|
|
87
|
+
|
|
88
|
+
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
|
89
|
+
[style setAlignment:NSTextAlignmentCenter];
|
|
90
|
+
|
|
91
|
+
NSDictionary *attributes = @{
|
|
92
|
+
NSFontAttributeName: [NSFont systemFontOfSize:21 weight:NSFontWeightMedium],
|
|
93
|
+
NSForegroundColorAttributeName: [NSColor whiteColor],
|
|
94
|
+
NSParagraphStyleAttributeName: style,
|
|
95
|
+
NSStrokeColorAttributeName: [NSColor blackColor],
|
|
96
|
+
NSStrokeWidthAttributeName: @(-2.0)
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
NSRect textRect = NSMakeRect(10, self.bounds.size.height - 90, self.bounds.size.width - 20, 80);
|
|
100
|
+
[infoText drawInRect:textRect withAttributes:attributes];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@end
|
|
104
|
+
|
|
105
|
+
// Recording preview overlay view - full screen with cutout
|
|
106
|
+
@interface RecordingPreviewView : NSView
|
|
107
|
+
@property (nonatomic, strong) NSDictionary *recordingWindowInfo;
|
|
108
|
+
@end
|
|
109
|
+
|
|
110
|
+
@implementation RecordingPreviewView
|
|
111
|
+
|
|
112
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
113
|
+
self = [super initWithFrame:frameRect];
|
|
114
|
+
if (self) {
|
|
115
|
+
self.wantsLayer = YES;
|
|
116
|
+
self.layer.backgroundColor = [[NSColor clearColor] CGColor];
|
|
117
|
+
}
|
|
118
|
+
return self;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
122
|
+
[super drawRect:dirtyRect];
|
|
123
|
+
|
|
124
|
+
if (!self.recordingWindowInfo) {
|
|
125
|
+
// No window info, fill with semi-transparent black
|
|
126
|
+
[[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
|
|
127
|
+
NSRectFill(dirtyRect);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Get window coordinates
|
|
132
|
+
int windowX = [[self.recordingWindowInfo objectForKey:@"x"] intValue];
|
|
133
|
+
int windowY = [[self.recordingWindowInfo objectForKey:@"y"] intValue];
|
|
134
|
+
int windowWidth = [[self.recordingWindowInfo objectForKey:@"width"] intValue];
|
|
135
|
+
int windowHeight = [[self.recordingWindowInfo objectForKey:@"height"] intValue];
|
|
136
|
+
|
|
137
|
+
// Convert from CGWindow coordinates (top-left) to NSView coordinates (bottom-left)
|
|
138
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
139
|
+
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
140
|
+
CGFloat convertedY = screenHeight - windowY - windowHeight;
|
|
141
|
+
|
|
142
|
+
NSRect windowRect = NSMakeRect(windowX, convertedY, windowWidth, windowHeight);
|
|
143
|
+
|
|
144
|
+
// Create a path that covers the entire view but excludes the window area
|
|
145
|
+
NSBezierPath *maskPath = [NSBezierPath bezierPathWithRect:self.bounds];
|
|
146
|
+
NSBezierPath *windowPath = [NSBezierPath bezierPathWithRect:windowRect];
|
|
147
|
+
[maskPath appendBezierPath:windowPath];
|
|
148
|
+
[maskPath setWindingRule:NSWindingRuleEvenOdd]; // Creates hole effect
|
|
149
|
+
|
|
150
|
+
// Fill with semi-transparent black, excluding window area
|
|
151
|
+
[[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
|
|
152
|
+
[maskPath fill];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@end
|
|
156
|
+
|
|
157
|
+
// Screen selection overlay view
|
|
158
|
+
@interface ScreenSelectorOverlayView : NSView
|
|
159
|
+
@property (nonatomic, strong) NSDictionary *screenInfo;
|
|
160
|
+
@end
|
|
161
|
+
|
|
162
|
+
@implementation ScreenSelectorOverlayView
|
|
163
|
+
|
|
164
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
165
|
+
self = [super initWithFrame:frameRect];
|
|
166
|
+
if (self) {
|
|
167
|
+
self.wantsLayer = YES;
|
|
168
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] CGColor];
|
|
169
|
+
self.layer.borderColor = [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor];
|
|
170
|
+
self.layer.borderWidth = 5.0;
|
|
171
|
+
self.layer.cornerRadius = 8.0;
|
|
172
|
+
}
|
|
173
|
+
return self;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
177
|
+
[super drawRect:dirtyRect];
|
|
178
|
+
|
|
179
|
+
if (!self.screenInfo) return;
|
|
180
|
+
|
|
181
|
+
// Background with transparency
|
|
182
|
+
[[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] setFill];
|
|
183
|
+
NSRectFill(dirtyRect);
|
|
184
|
+
|
|
185
|
+
// Border
|
|
186
|
+
[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] setStroke];
|
|
187
|
+
NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:8 yRadius:8];
|
|
188
|
+
[border setLineWidth:3.0];
|
|
189
|
+
[border stroke];
|
|
190
|
+
|
|
191
|
+
// Screen info text
|
|
192
|
+
NSString *screenName = [self.screenInfo objectForKey:@"name"] ?: @"Unknown Screen";
|
|
193
|
+
NSString *resolution = [self.screenInfo objectForKey:@"resolution"] ?: @"Unknown Resolution";
|
|
194
|
+
NSString *infoText = [NSString stringWithFormat:@"%@\n%@", screenName, resolution];
|
|
195
|
+
|
|
196
|
+
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
|
197
|
+
[style setAlignment:NSTextAlignmentCenter];
|
|
198
|
+
|
|
199
|
+
NSDictionary *attributes = @{
|
|
200
|
+
NSFontAttributeName: [NSFont systemFontOfSize:21 weight:NSFontWeightMedium],
|
|
201
|
+
NSForegroundColorAttributeName: [NSColor whiteColor],
|
|
202
|
+
NSParagraphStyleAttributeName: style,
|
|
203
|
+
NSStrokeColorAttributeName: [NSColor blackColor],
|
|
204
|
+
NSStrokeWidthAttributeName: @(-2.0)
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
NSRect textRect = NSMakeRect(10, self.bounds.size.height - 90, self.bounds.size.width - 20, 80);
|
|
208
|
+
[infoText drawInRect:textRect withAttributes:attributes];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@end
|
|
212
|
+
|
|
213
|
+
// Button action handler and timer target
|
|
214
|
+
@interface WindowSelectorDelegate : NSObject
|
|
215
|
+
- (void)selectButtonClicked:(id)sender;
|
|
216
|
+
- (void)screenSelectButtonClicked:(id)sender;
|
|
217
|
+
- (void)cancelButtonClicked:(id)sender;
|
|
218
|
+
- (void)timerUpdate:(NSTimer *)timer;
|
|
219
|
+
@end
|
|
220
|
+
|
|
221
|
+
@implementation WindowSelectorDelegate
|
|
222
|
+
- (void)selectButtonClicked:(id)sender {
|
|
223
|
+
if (g_currentWindowUnderCursor) {
|
|
224
|
+
g_selectedWindowInfo = [g_currentWindowUnderCursor retain];
|
|
225
|
+
cleanupWindowSelector();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
- (void)screenSelectButtonClicked:(id)sender {
|
|
230
|
+
NSButton *button = (NSButton *)sender;
|
|
231
|
+
NSInteger screenIndex = [button tag];
|
|
232
|
+
|
|
233
|
+
// Get screen info from global array using button tag
|
|
234
|
+
if (g_allScreens && screenIndex >= 0 && screenIndex < [g_allScreens count]) {
|
|
235
|
+
NSDictionary *screenInfo = [g_allScreens objectAtIndex:screenIndex];
|
|
236
|
+
g_selectedScreenInfo = [screenInfo retain];
|
|
237
|
+
|
|
238
|
+
NSLog(@"🖥️ SCREEN BUTTON CLICKED: %@ (%@)",
|
|
239
|
+
[screenInfo objectForKey:@"name"],
|
|
240
|
+
[screenInfo objectForKey:@"resolution"]);
|
|
241
|
+
|
|
242
|
+
cleanupScreenSelector();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
- (void)cancelButtonClicked:(id)sender {
|
|
247
|
+
NSLog(@"🚫 CANCEL BUTTON CLICKED: Selection cancelled");
|
|
248
|
+
// Clean up without selecting anything
|
|
249
|
+
if (g_isScreenSelecting) {
|
|
250
|
+
cleanupScreenSelector();
|
|
251
|
+
} else {
|
|
252
|
+
cleanupWindowSelector();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
- (void)timerUpdate:(NSTimer *)timer {
|
|
257
|
+
updateOverlay();
|
|
258
|
+
}
|
|
259
|
+
@end
|
|
260
|
+
|
|
261
|
+
static WindowSelectorDelegate *g_delegate = nil;
|
|
262
|
+
|
|
263
|
+
// Bring window to front using Accessibility API
|
|
264
|
+
bool bringWindowToFront(int windowId) {
|
|
265
|
+
@autoreleasepool {
|
|
266
|
+
@try {
|
|
267
|
+
// Method 1: Using Accessibility API (most reliable)
|
|
268
|
+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
269
|
+
if (!systemWide) return false;
|
|
270
|
+
|
|
271
|
+
CFArrayRef windowList = NULL;
|
|
272
|
+
AXError error = AXUIElementCopyAttributeValue(systemWide, kAXWindowsAttribute, (CFTypeRef*)&windowList);
|
|
273
|
+
|
|
274
|
+
if (error == kAXErrorSuccess && windowList) {
|
|
275
|
+
CFIndex windowCount = CFArrayGetCount(windowList);
|
|
276
|
+
|
|
277
|
+
for (CFIndex i = 0; i < windowCount; i++) {
|
|
278
|
+
AXUIElementRef windowElement = (AXUIElementRef)CFArrayGetValueAtIndex(windowList, i);
|
|
279
|
+
|
|
280
|
+
// Get window ID by comparing with CGWindowList
|
|
281
|
+
// Since _AXUIElementGetWindow is not available, we'll use app PID approach
|
|
282
|
+
pid_t windowPid;
|
|
283
|
+
error = AXUIElementGetPid(windowElement, &windowPid);
|
|
284
|
+
|
|
285
|
+
if (error == kAXErrorSuccess) {
|
|
286
|
+
// Get window info for this PID from CGWindowList
|
|
287
|
+
CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
|
|
288
|
+
if (cgWindowList) {
|
|
289
|
+
NSArray *windowArray = (__bridge NSArray *)cgWindowList;
|
|
290
|
+
|
|
291
|
+
for (NSDictionary *windowInfo in windowArray) {
|
|
292
|
+
NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
|
|
293
|
+
NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
|
|
294
|
+
|
|
295
|
+
if ([cgWindowId intValue] == windowId && [processId intValue] == windowPid) {
|
|
296
|
+
// Found the window, bring it to front
|
|
297
|
+
NSLog(@"🔝 BRINGING TO FRONT: Window ID %d (PID: %d)", windowId, windowPid);
|
|
298
|
+
|
|
299
|
+
// Method 1: Raise specific window (not the whole app)
|
|
300
|
+
error = AXUIElementPerformAction(windowElement, kAXRaiseAction);
|
|
301
|
+
if (error == kAXErrorSuccess) {
|
|
302
|
+
NSLog(@" ✅ Specific window raised successfully");
|
|
303
|
+
} else {
|
|
304
|
+
NSLog(@" ⚠️ Raise action failed: %d", error);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Method 2: Focus specific window (not main window)
|
|
308
|
+
error = AXUIElementSetAttributeValue(windowElement, kAXFocusedAttribute, kCFBooleanTrue);
|
|
309
|
+
if (error == kAXErrorSuccess) {
|
|
310
|
+
NSLog(@" ✅ Specific window focused");
|
|
311
|
+
} else {
|
|
312
|
+
NSLog(@" ⚠️ Focus failed: %d", error);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
CFRelease(cgWindowList);
|
|
316
|
+
CFRelease(windowList);
|
|
317
|
+
CFRelease(systemWide);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
CFRelease(cgWindowList);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
CFRelease(windowList);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
CFRelease(systemWide);
|
|
329
|
+
|
|
330
|
+
// Method 2: Light activation fallback (minimal app activation)
|
|
331
|
+
NSLog(@" 🔄 Trying minimal activation for window %d", windowId);
|
|
332
|
+
|
|
333
|
+
// Get window info to find the process
|
|
334
|
+
CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
|
|
335
|
+
if (cgWindowList) {
|
|
336
|
+
NSArray *windowArray = (__bridge NSArray *)cgWindowList;
|
|
337
|
+
|
|
338
|
+
for (NSDictionary *windowInfo in windowArray) {
|
|
339
|
+
NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
|
|
340
|
+
if ([cgWindowId intValue] == windowId) {
|
|
341
|
+
// Get process ID
|
|
342
|
+
NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
|
|
343
|
+
if (processId) {
|
|
344
|
+
// Light activation - only bring app to front, don't activate all windows
|
|
345
|
+
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:[processId intValue]];
|
|
346
|
+
if (app) {
|
|
347
|
+
// Use NSApplicationActivateIgnoringOtherApps only (no NSApplicationActivateAllWindows)
|
|
348
|
+
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
|
349
|
+
NSLog(@" ✅ App minimally activated: PID %d (specific window should be frontmost)", [processId intValue]);
|
|
350
|
+
CFRelease(cgWindowList);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
CFRelease(cgWindowList);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return false;
|
|
361
|
+
|
|
362
|
+
} @catch (NSException *exception) {
|
|
363
|
+
NSLog(@"❌ Error bringing window to front: %@", exception);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Get all selectable windows
|
|
370
|
+
NSArray* getAllSelectableWindows() {
|
|
371
|
+
@autoreleasepool {
|
|
372
|
+
NSMutableArray *windows = [NSMutableArray array];
|
|
373
|
+
|
|
374
|
+
// Get all windows using CGWindowListCopyWindowInfo
|
|
375
|
+
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
|
|
376
|
+
|
|
377
|
+
if (windowList) {
|
|
378
|
+
NSArray *windowArray = (__bridge NSArray *)windowList;
|
|
379
|
+
|
|
380
|
+
for (NSDictionary *windowInfo in windowArray) {
|
|
381
|
+
NSString *windowOwner = [windowInfo objectForKey:(NSString *)kCGWindowOwnerName];
|
|
382
|
+
NSString *windowName = [windowInfo objectForKey:(NSString *)kCGWindowName];
|
|
383
|
+
NSNumber *windowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
|
|
384
|
+
NSNumber *windowLayer = [windowInfo objectForKey:(NSString *)kCGWindowLayer];
|
|
385
|
+
NSDictionary *bounds = [windowInfo objectForKey:(NSString *)kCGWindowBounds];
|
|
386
|
+
|
|
387
|
+
// Skip system windows, dock, menu bar, etc.
|
|
388
|
+
if ([windowLayer intValue] != 0) continue; // Only normal windows
|
|
389
|
+
if (!windowOwner || [windowOwner length] == 0) continue;
|
|
390
|
+
if ([windowOwner isEqualToString:@"WindowServer"]) continue;
|
|
391
|
+
if ([windowOwner isEqualToString:@"Dock"]) continue;
|
|
392
|
+
|
|
393
|
+
// Extract bounds
|
|
394
|
+
int x = [[bounds objectForKey:@"X"] intValue];
|
|
395
|
+
int y = [[bounds objectForKey:@"Y"] intValue];
|
|
396
|
+
int width = [[bounds objectForKey:@"Width"] intValue];
|
|
397
|
+
int height = [[bounds objectForKey:@"Height"] intValue];
|
|
398
|
+
|
|
399
|
+
// Skip too small windows
|
|
400
|
+
if (width < 50 || height < 50) continue;
|
|
401
|
+
|
|
402
|
+
NSDictionary *window = @{
|
|
403
|
+
@"id": windowId ?: @(0),
|
|
404
|
+
@"title": windowName ?: @"Untitled",
|
|
405
|
+
@"appName": windowOwner,
|
|
406
|
+
@"x": @(x),
|
|
407
|
+
@"y": @(y),
|
|
408
|
+
@"width": @(width),
|
|
409
|
+
@"height": @(height)
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
[windows addObject:window];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
CFRelease(windowList);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return [windows copy];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Get window under cursor point
|
|
423
|
+
NSDictionary* getWindowUnderCursor(CGPoint point) {
|
|
424
|
+
@autoreleasepool {
|
|
425
|
+
if (!g_allWindows) return nil;
|
|
426
|
+
|
|
427
|
+
// Find window that contains the cursor point
|
|
428
|
+
for (NSDictionary *window in g_allWindows) {
|
|
429
|
+
int x = [[window objectForKey:@"x"] intValue];
|
|
430
|
+
int y = [[window objectForKey:@"y"] intValue];
|
|
431
|
+
int width = [[window objectForKey:@"width"] intValue];
|
|
432
|
+
int height = [[window objectForKey:@"height"] intValue];
|
|
433
|
+
|
|
434
|
+
if (point.x >= x && point.x <= x + width &&
|
|
435
|
+
point.y >= y && point.y <= y + height) {
|
|
436
|
+
return window;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return nil;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Update overlay to highlight window under cursor
|
|
445
|
+
void updateOverlay() {
|
|
446
|
+
@autoreleasepool {
|
|
447
|
+
if (!g_isWindowSelecting || !g_overlayWindow) return;
|
|
448
|
+
|
|
449
|
+
// Get current cursor position
|
|
450
|
+
NSPoint mouseLocation = [NSEvent mouseLocation];
|
|
451
|
+
// Convert from NSEvent coordinates (bottom-left) to CGWindow coordinates (top-left)
|
|
452
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
453
|
+
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
454
|
+
CGPoint globalPoint = CGPointMake(mouseLocation.x, screenHeight - mouseLocation.y);
|
|
455
|
+
|
|
456
|
+
// Find window under cursor
|
|
457
|
+
NSDictionary *windowUnderCursor = getWindowUnderCursor(globalPoint);
|
|
458
|
+
|
|
459
|
+
if (windowUnderCursor && ![windowUnderCursor isEqualToDictionary:g_currentWindowUnderCursor]) {
|
|
460
|
+
// Update current window
|
|
461
|
+
[g_currentWindowUnderCursor release];
|
|
462
|
+
g_currentWindowUnderCursor = [windowUnderCursor retain];
|
|
463
|
+
|
|
464
|
+
// Update overlay position and size
|
|
465
|
+
int x = [[windowUnderCursor objectForKey:@"x"] intValue];
|
|
466
|
+
int y = [[windowUnderCursor objectForKey:@"y"] intValue];
|
|
467
|
+
int width = [[windowUnderCursor objectForKey:@"width"] intValue];
|
|
468
|
+
int height = [[windowUnderCursor objectForKey:@"height"] intValue];
|
|
469
|
+
|
|
470
|
+
// Convert coordinates from CGWindow (top-left) to NSWindow (bottom-left)
|
|
471
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
472
|
+
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
473
|
+
CGFloat adjustedY = screenHeight - y - height;
|
|
474
|
+
|
|
475
|
+
// Clamp overlay to screen bounds to avoid partial off-screen issues
|
|
476
|
+
NSRect screenFrame = [mainScreen frame];
|
|
477
|
+
CGFloat clampedX = MAX(screenFrame.origin.x, MIN(x, screenFrame.origin.x + screenFrame.size.width - width));
|
|
478
|
+
CGFloat clampedY = MAX(screenFrame.origin.y, MIN(adjustedY, screenFrame.origin.y + screenFrame.size.height - height));
|
|
479
|
+
CGFloat clampedWidth = MIN(width, screenFrame.size.width - (clampedX - screenFrame.origin.x));
|
|
480
|
+
CGFloat clampedHeight = MIN(height, screenFrame.size.height - (clampedY - screenFrame.origin.y));
|
|
481
|
+
|
|
482
|
+
NSRect overlayFrame = NSMakeRect(clampedX, clampedY, clampedWidth, clampedHeight);
|
|
483
|
+
|
|
484
|
+
NSString *windowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Untitled";
|
|
485
|
+
NSString *appName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
|
|
486
|
+
|
|
487
|
+
NSLog(@"🎯 WINDOW DETECTED: %@ - \"%@\"", appName, windowTitle);
|
|
488
|
+
NSLog(@" 📍 Position: (%d, %d) 📏 Size: %d × %d", x, y, width, height);
|
|
489
|
+
NSLog(@" 🖥️ NSRect: (%.0f, %.0f, %.0f, %.0f) 🔝 Level: %ld",
|
|
490
|
+
overlayFrame.origin.x, overlayFrame.origin.y,
|
|
491
|
+
overlayFrame.size.width, overlayFrame.size.height,
|
|
492
|
+
[g_overlayWindow level]);
|
|
493
|
+
|
|
494
|
+
// Bring window to front if enabled
|
|
495
|
+
if (g_bringToFrontEnabled) {
|
|
496
|
+
int windowId = [[windowUnderCursor objectForKey:@"id"] intValue];
|
|
497
|
+
if (windowId > 0) {
|
|
498
|
+
bool success = bringWindowToFront(windowId);
|
|
499
|
+
if (!success) {
|
|
500
|
+
NSLog(@" ⚠️ Failed to bring window to front");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
[g_overlayWindow setFrame:overlayFrame display:YES];
|
|
505
|
+
|
|
506
|
+
// Update overlay view window info
|
|
507
|
+
[(WindowSelectorOverlayView *)g_overlayView setWindowInfo:windowUnderCursor];
|
|
508
|
+
[g_overlayView setNeedsDisplay:YES];
|
|
509
|
+
|
|
510
|
+
// Position buttons - Start Record in center, Cancel below it
|
|
511
|
+
if (g_selectButton) {
|
|
512
|
+
NSSize buttonSize = [g_selectButton frame].size;
|
|
513
|
+
NSPoint buttonCenter = NSMakePoint(
|
|
514
|
+
(width - buttonSize.width) / 2,
|
|
515
|
+
(height - buttonSize.height) / 2 + 30 // Slightly above center
|
|
516
|
+
);
|
|
517
|
+
[g_selectButton setFrameOrigin:buttonCenter];
|
|
518
|
+
|
|
519
|
+
// Position cancel button below the main button
|
|
520
|
+
NSButton *cancelButton = nil;
|
|
521
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
522
|
+
if ([subview isKindOfClass:[NSButton class]] &&
|
|
523
|
+
[[(NSButton*)subview title] isEqualToString:@"Cancel"]) {
|
|
524
|
+
cancelButton = (NSButton*)subview;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (cancelButton) {
|
|
530
|
+
NSSize cancelButtonSize = [cancelButton frame].size;
|
|
531
|
+
NSPoint cancelButtonCenter = NSMakePoint(
|
|
532
|
+
(width - cancelButtonSize.width) / 2,
|
|
533
|
+
buttonCenter.y - buttonSize.height - 20 // 20px below main button
|
|
534
|
+
);
|
|
535
|
+
[cancelButton setFrameOrigin:cancelButtonCenter];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
[g_overlayWindow orderFront:nil];
|
|
540
|
+
[g_overlayWindow makeKeyAndOrderFront:nil];
|
|
541
|
+
|
|
542
|
+
NSLog(@" ✅ Overlay Status: Level=%ld, Alpha=%.1f, Visible=%s, Frame Set=YES",
|
|
543
|
+
[g_overlayWindow level], [g_overlayWindow alphaValue],
|
|
544
|
+
[g_overlayWindow isVisible] ? "YES" : "NO");
|
|
545
|
+
} else if (!windowUnderCursor && g_currentWindowUnderCursor) {
|
|
546
|
+
// No window under cursor, hide overlay
|
|
547
|
+
NSString *leftWindowTitle = [g_currentWindowUnderCursor objectForKey:@"title"] ?: @"Untitled";
|
|
548
|
+
NSString *leftAppName = [g_currentWindowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
|
|
549
|
+
|
|
550
|
+
NSLog(@"🚪 WINDOW LEFT: %@ - \"%@\"", leftAppName, leftWindowTitle);
|
|
551
|
+
|
|
552
|
+
[g_overlayWindow orderOut:nil];
|
|
553
|
+
[g_currentWindowUnderCursor release];
|
|
554
|
+
g_currentWindowUnderCursor = nil;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Cleanup function
|
|
560
|
+
void cleanupWindowSelector() {
|
|
561
|
+
g_isWindowSelecting = false;
|
|
562
|
+
|
|
563
|
+
// Stop tracking timer
|
|
564
|
+
if (g_trackingTimer) {
|
|
565
|
+
[g_trackingTimer invalidate];
|
|
566
|
+
g_trackingTimer = nil;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Remove key event monitor
|
|
570
|
+
if (g_windowKeyEventMonitor) {
|
|
571
|
+
[NSEvent removeMonitor:g_windowKeyEventMonitor];
|
|
572
|
+
g_windowKeyEventMonitor = nil;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Close overlay window
|
|
576
|
+
if (g_overlayWindow) {
|
|
577
|
+
[g_overlayWindow close];
|
|
578
|
+
g_overlayWindow = nil;
|
|
579
|
+
g_overlayView = nil;
|
|
580
|
+
g_selectButton = nil;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Clean up delegate
|
|
584
|
+
if (g_delegate) {
|
|
585
|
+
[g_delegate release];
|
|
586
|
+
g_delegate = nil;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Clean up data
|
|
590
|
+
if (g_allWindows) {
|
|
591
|
+
[g_allWindows release];
|
|
592
|
+
g_allWindows = nil;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (g_currentWindowUnderCursor) {
|
|
596
|
+
[g_currentWindowUnderCursor release];
|
|
597
|
+
g_currentWindowUnderCursor = nil;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Recording preview functions
|
|
602
|
+
void cleanupRecordingPreview() {
|
|
603
|
+
if (g_recordingPreviewWindow) {
|
|
604
|
+
[g_recordingPreviewWindow close];
|
|
605
|
+
g_recordingPreviewWindow = nil;
|
|
606
|
+
g_recordingPreviewView = nil;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (g_recordingWindowInfo) {
|
|
610
|
+
[g_recordingWindowInfo release];
|
|
611
|
+
g_recordingWindowInfo = nil;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
bool showRecordingPreview(NSDictionary *windowInfo) {
|
|
616
|
+
@try {
|
|
617
|
+
// Clean up any existing preview
|
|
618
|
+
cleanupRecordingPreview();
|
|
619
|
+
|
|
620
|
+
if (!windowInfo) return false;
|
|
621
|
+
|
|
622
|
+
// Store window info
|
|
623
|
+
g_recordingWindowInfo = [windowInfo retain];
|
|
624
|
+
|
|
625
|
+
// Get main screen bounds for full screen overlay
|
|
626
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
627
|
+
NSRect screenFrame = [mainScreen frame];
|
|
628
|
+
|
|
629
|
+
// Create full-screen overlay window
|
|
630
|
+
g_recordingPreviewWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
631
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
632
|
+
backing:NSBackingStoreBuffered
|
|
633
|
+
defer:NO];
|
|
634
|
+
|
|
635
|
+
[g_recordingPreviewWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)]; // High level but below selection
|
|
636
|
+
[g_recordingPreviewWindow setOpaque:NO];
|
|
637
|
+
[g_recordingPreviewWindow setBackgroundColor:[NSColor clearColor]];
|
|
638
|
+
[g_recordingPreviewWindow setIgnoresMouseEvents:YES]; // Don't interfere with user interaction
|
|
639
|
+
[g_recordingPreviewWindow setAcceptsMouseMovedEvents:NO];
|
|
640
|
+
[g_recordingPreviewWindow setHasShadow:NO];
|
|
641
|
+
[g_recordingPreviewWindow setAlphaValue:1.0];
|
|
642
|
+
[g_recordingPreviewWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
643
|
+
|
|
644
|
+
// Create preview view
|
|
645
|
+
g_recordingPreviewView = [[RecordingPreviewView alloc] initWithFrame:screenFrame];
|
|
646
|
+
[(RecordingPreviewView *)g_recordingPreviewView setRecordingWindowInfo:windowInfo];
|
|
647
|
+
[g_recordingPreviewWindow setContentView:g_recordingPreviewView];
|
|
648
|
+
|
|
649
|
+
// Show the preview
|
|
650
|
+
[g_recordingPreviewWindow orderFront:nil];
|
|
651
|
+
[g_recordingPreviewWindow makeKeyAndOrderFront:nil];
|
|
652
|
+
|
|
653
|
+
NSLog(@"🎬 RECORDING PREVIEW: Showing overlay for %@ - \"%@\"",
|
|
654
|
+
[windowInfo objectForKey:@"appName"],
|
|
655
|
+
[windowInfo objectForKey:@"title"]);
|
|
656
|
+
|
|
657
|
+
return true;
|
|
658
|
+
|
|
659
|
+
} @catch (NSException *exception) {
|
|
660
|
+
NSLog(@"❌ Error showing recording preview: %@", exception);
|
|
661
|
+
cleanupRecordingPreview();
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
bool hideRecordingPreview() {
|
|
667
|
+
@try {
|
|
668
|
+
if (g_recordingPreviewWindow) {
|
|
669
|
+
NSLog(@"🎬 RECORDING PREVIEW: Hiding overlay");
|
|
670
|
+
cleanupRecordingPreview();
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
|
|
675
|
+
} @catch (NSException *exception) {
|
|
676
|
+
NSLog(@"❌ Error hiding recording preview: %@", exception);
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Screen selection functions
|
|
682
|
+
void cleanupScreenSelector() {
|
|
683
|
+
g_isScreenSelecting = false;
|
|
684
|
+
|
|
685
|
+
// Remove key event monitor
|
|
686
|
+
if (g_screenKeyEventMonitor) {
|
|
687
|
+
[NSEvent removeMonitor:g_screenKeyEventMonitor];
|
|
688
|
+
g_screenKeyEventMonitor = nil;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Close all screen overlay windows
|
|
692
|
+
if (g_screenOverlayWindows) {
|
|
693
|
+
for (NSWindow *overlayWindow in g_screenOverlayWindows) {
|
|
694
|
+
[overlayWindow close];
|
|
695
|
+
}
|
|
696
|
+
[g_screenOverlayWindows release];
|
|
697
|
+
g_screenOverlayWindows = nil;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Clean up screen data
|
|
701
|
+
if (g_allScreens) {
|
|
702
|
+
[g_allScreens release];
|
|
703
|
+
g_allScreens = nil;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
bool startScreenSelection() {
|
|
708
|
+
@try {
|
|
709
|
+
if (g_isScreenSelecting) return false;
|
|
710
|
+
|
|
711
|
+
// Get all available screens
|
|
712
|
+
NSArray *screens = [NSScreen screens];
|
|
713
|
+
if (!screens || [screens count] == 0) return false;
|
|
714
|
+
|
|
715
|
+
// Create screen info array
|
|
716
|
+
NSMutableArray *screenInfoArray = [[NSMutableArray alloc] init];
|
|
717
|
+
g_screenOverlayWindows = [[NSMutableArray alloc] init];
|
|
718
|
+
|
|
719
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
720
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
721
|
+
NSRect screenFrame = [screen frame];
|
|
722
|
+
|
|
723
|
+
// Create screen info dictionary
|
|
724
|
+
NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
|
|
725
|
+
[screenInfo setObject:[NSNumber numberWithInteger:i] forKey:@"id"];
|
|
726
|
+
[screenInfo setObject:[NSString stringWithFormat:@"Display %ld", (long)(i + 1)] forKey:@"name"];
|
|
727
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.x] forKey:@"x"];
|
|
728
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.y] forKey:@"y"];
|
|
729
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.width] forKey:@"width"];
|
|
730
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.height] forKey:@"height"];
|
|
731
|
+
[screenInfo setObject:[NSString stringWithFormat:@"%.0fx%.0f", screenFrame.size.width, screenFrame.size.height] forKey:@"resolution"];
|
|
732
|
+
[screenInfo setObject:[NSNumber numberWithBool:(i == 0)] forKey:@"isPrimary"]; // First screen is primary
|
|
733
|
+
[screenInfoArray addObject:screenInfo];
|
|
734
|
+
|
|
735
|
+
// Create overlay window for this screen (FULL screen including menu bar)
|
|
736
|
+
NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
737
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
738
|
+
backing:NSBackingStoreBuffered
|
|
739
|
+
defer:NO
|
|
740
|
+
screen:screen];
|
|
741
|
+
|
|
742
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
|
|
743
|
+
[overlayWindow setOpaque:NO];
|
|
744
|
+
[overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
745
|
+
[overlayWindow setIgnoresMouseEvents:NO];
|
|
746
|
+
[overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
747
|
+
[overlayWindow setHasShadow:NO];
|
|
748
|
+
[overlayWindow setAlphaValue:1.0];
|
|
749
|
+
[overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
750
|
+
|
|
751
|
+
// Create overlay view
|
|
752
|
+
ScreenSelectorOverlayView *overlayView = [[ScreenSelectorOverlayView alloc] initWithFrame:screenFrame];
|
|
753
|
+
[overlayView setScreenInfo:screenInfo];
|
|
754
|
+
[overlayWindow setContentView:overlayView];
|
|
755
|
+
|
|
756
|
+
// Create select button
|
|
757
|
+
NSButton *selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 180, 60)];
|
|
758
|
+
[selectButton setTitle:@"Start Record"];
|
|
759
|
+
[selectButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
760
|
+
[selectButton setBezelStyle:NSBezelStyleRounded];
|
|
761
|
+
[selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]];
|
|
762
|
+
[selectButton setTag:i]; // Set screen index as tag
|
|
763
|
+
|
|
764
|
+
// Blue background with white text
|
|
765
|
+
[selectButton setWantsLayer:YES];
|
|
766
|
+
[selectButton.layer setBackgroundColor:[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor]];
|
|
767
|
+
[selectButton.layer setCornerRadius:8.0];
|
|
768
|
+
[selectButton.layer setBorderColor:[[NSColor colorWithRed:0.0 green:0.3 blue:0.7 alpha:1.0] CGColor]];
|
|
769
|
+
[selectButton.layer setBorderWidth:2.0];
|
|
770
|
+
|
|
771
|
+
// White text color
|
|
772
|
+
NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
|
|
773
|
+
initWithString:[selectButton title]];
|
|
774
|
+
[titleString addAttribute:NSForegroundColorAttributeName
|
|
775
|
+
value:[NSColor whiteColor]
|
|
776
|
+
range:NSMakeRange(0, [titleString length])];
|
|
777
|
+
[selectButton setAttributedTitle:titleString];
|
|
778
|
+
|
|
779
|
+
// Add shadow for better visibility
|
|
780
|
+
[selectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
781
|
+
[selectButton.layer setShadowOffset:NSMakeSize(0, -2)];
|
|
782
|
+
[selectButton.layer setShadowRadius:4.0];
|
|
783
|
+
[selectButton.layer setShadowOpacity:0.3];
|
|
784
|
+
|
|
785
|
+
// Set button target and action (reuse global delegate)
|
|
786
|
+
if (!g_delegate) {
|
|
787
|
+
g_delegate = [[WindowSelectorDelegate alloc] init];
|
|
788
|
+
}
|
|
789
|
+
[selectButton setTarget:g_delegate];
|
|
790
|
+
[selectButton setAction:@selector(screenSelectButtonClicked:)];
|
|
791
|
+
|
|
792
|
+
// Create cancel button for screen selection
|
|
793
|
+
NSButton *screenCancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
|
|
794
|
+
[screenCancelButton setTitle:@"Cancel"];
|
|
795
|
+
[screenCancelButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
796
|
+
[screenCancelButton setBezelStyle:NSBezelStyleRounded];
|
|
797
|
+
[screenCancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
|
|
798
|
+
|
|
799
|
+
// Gray cancel button styling
|
|
800
|
+
[screenCancelButton setWantsLayer:YES];
|
|
801
|
+
[screenCancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.8] CGColor]];
|
|
802
|
+
[screenCancelButton.layer setCornerRadius:6.0];
|
|
803
|
+
[screenCancelButton.layer setBorderColor:[[NSColor colorWithRed:0.4 green:0.4 blue:0.4 alpha:1.0] CGColor]];
|
|
804
|
+
[screenCancelButton.layer setBorderWidth:1.0];
|
|
805
|
+
|
|
806
|
+
// White text for cancel button
|
|
807
|
+
NSMutableAttributedString *screenCancelTitleString = [[NSMutableAttributedString alloc]
|
|
808
|
+
initWithString:[screenCancelButton title]];
|
|
809
|
+
[screenCancelTitleString addAttribute:NSForegroundColorAttributeName
|
|
810
|
+
value:[NSColor whiteColor]
|
|
811
|
+
range:NSMakeRange(0, [screenCancelTitleString length])];
|
|
812
|
+
[screenCancelButton setAttributedTitle:screenCancelTitleString];
|
|
813
|
+
|
|
814
|
+
// Add shadow for cancel button
|
|
815
|
+
[screenCancelButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
816
|
+
[screenCancelButton.layer setShadowOffset:NSMakeSize(0, -1)];
|
|
817
|
+
[screenCancelButton.layer setShadowRadius:2.0];
|
|
818
|
+
[screenCancelButton.layer setShadowOpacity:0.2];
|
|
819
|
+
|
|
820
|
+
[screenCancelButton setTarget:g_delegate];
|
|
821
|
+
[screenCancelButton setAction:@selector(cancelButtonClicked:)];
|
|
822
|
+
|
|
823
|
+
// Position buttons - Start Record in center, Cancel below it
|
|
824
|
+
NSPoint buttonCenter = NSMakePoint(
|
|
825
|
+
(screenFrame.size.width - [selectButton frame].size.width) / 2,
|
|
826
|
+
(screenFrame.size.height - [selectButton frame].size.height) / 2 + 30 // Slightly above center
|
|
827
|
+
);
|
|
828
|
+
[selectButton setFrameOrigin:buttonCenter];
|
|
829
|
+
|
|
830
|
+
NSPoint cancelButtonCenter = NSMakePoint(
|
|
831
|
+
(screenFrame.size.width - [screenCancelButton frame].size.width) / 2,
|
|
832
|
+
buttonCenter.y - [selectButton frame].size.height - 20 // 20px below main button
|
|
833
|
+
);
|
|
834
|
+
[screenCancelButton setFrameOrigin:cancelButtonCenter];
|
|
835
|
+
|
|
836
|
+
[overlayView addSubview:selectButton];
|
|
837
|
+
[overlayView addSubview:screenCancelButton];
|
|
838
|
+
[overlayWindow orderFront:nil];
|
|
839
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
840
|
+
|
|
841
|
+
[g_screenOverlayWindows addObject:overlayWindow];
|
|
842
|
+
[screenInfo release];
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
g_allScreens = [screenInfoArray retain];
|
|
846
|
+
[screenInfoArray release];
|
|
847
|
+
g_isScreenSelecting = true;
|
|
848
|
+
|
|
849
|
+
// Add ESC key event monitor to cancel selection
|
|
850
|
+
g_screenKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
|
|
851
|
+
handler:^(NSEvent *event) {
|
|
852
|
+
if ([event keyCode] == 53) { // ESC key
|
|
853
|
+
NSLog(@"🖥️ SCREEN SELECTION: ESC pressed - cancelling selection");
|
|
854
|
+
cleanupScreenSelector();
|
|
855
|
+
}
|
|
856
|
+
}];
|
|
857
|
+
|
|
858
|
+
NSLog(@"🖥️ SCREEN SELECTION: Started with %lu screens (ESC to cancel)", (unsigned long)[screens count]);
|
|
859
|
+
|
|
860
|
+
return true;
|
|
861
|
+
|
|
862
|
+
} @catch (NSException *exception) {
|
|
863
|
+
NSLog(@"❌ Error starting screen selection: %@", exception);
|
|
864
|
+
cleanupScreenSelector();
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
bool stopScreenSelection() {
|
|
870
|
+
@try {
|
|
871
|
+
if (!g_isScreenSelecting) return false;
|
|
872
|
+
|
|
873
|
+
cleanupScreenSelector();
|
|
874
|
+
NSLog(@"🖥️ SCREEN SELECTION: Stopped");
|
|
875
|
+
return true;
|
|
876
|
+
|
|
877
|
+
} @catch (NSException *exception) {
|
|
878
|
+
NSLog(@"❌ Error stopping screen selection: %@", exception);
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
NSDictionary* getSelectedScreenInfo() {
|
|
884
|
+
if (!g_selectedScreenInfo) return nil;
|
|
885
|
+
|
|
886
|
+
NSDictionary *result = [g_selectedScreenInfo retain];
|
|
887
|
+
[g_selectedScreenInfo release];
|
|
888
|
+
g_selectedScreenInfo = nil;
|
|
889
|
+
|
|
890
|
+
return [result autorelease];
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
bool showScreenRecordingPreview(NSDictionary *screenInfo) {
|
|
894
|
+
@try {
|
|
895
|
+
// Clean up any existing preview
|
|
896
|
+
cleanupRecordingPreview();
|
|
897
|
+
|
|
898
|
+
if (!screenInfo) return false;
|
|
899
|
+
|
|
900
|
+
// For screen recording preview, we show all OTHER screens as black overlay
|
|
901
|
+
// and keep the selected screen transparent
|
|
902
|
+
NSArray *screens = [NSScreen screens];
|
|
903
|
+
if (!screens || [screens count] == 0) return false;
|
|
904
|
+
|
|
905
|
+
int selectedScreenId = [[screenInfo objectForKey:@"id"] intValue];
|
|
906
|
+
|
|
907
|
+
// Create overlay for each screen except the selected one
|
|
908
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
909
|
+
if (i == selectedScreenId) continue; // Skip selected screen
|
|
910
|
+
|
|
911
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
912
|
+
NSRect screenFrame = [screen frame];
|
|
913
|
+
|
|
914
|
+
// Create full-screen black overlay for non-selected screens
|
|
915
|
+
NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
916
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
917
|
+
backing:NSBackingStoreBuffered
|
|
918
|
+
defer:NO
|
|
919
|
+
screen:screen];
|
|
920
|
+
|
|
921
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)];
|
|
922
|
+
[overlayWindow setOpaque:NO];
|
|
923
|
+
[overlayWindow setBackgroundColor:[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5]];
|
|
924
|
+
[overlayWindow setIgnoresMouseEvents:YES];
|
|
925
|
+
[overlayWindow setAcceptsMouseMovedEvents:NO];
|
|
926
|
+
[overlayWindow setHasShadow:NO];
|
|
927
|
+
[overlayWindow setAlphaValue:1.0];
|
|
928
|
+
[overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
929
|
+
|
|
930
|
+
[overlayWindow orderFront:nil];
|
|
931
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
932
|
+
|
|
933
|
+
// Store for cleanup (reuse recording preview window variable)
|
|
934
|
+
if (!g_recordingPreviewWindow) {
|
|
935
|
+
g_recordingPreviewWindow = overlayWindow;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
NSLog(@"🎬 SCREEN RECORDING PREVIEW: Showing overlay for Screen %d", selectedScreenId);
|
|
940
|
+
|
|
941
|
+
return true;
|
|
942
|
+
|
|
943
|
+
} @catch (NSException *exception) {
|
|
944
|
+
NSLog(@"❌ Error showing screen recording preview: %@", exception);
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
bool hideScreenRecordingPreview() {
|
|
950
|
+
return hideRecordingPreview(); // Reuse existing function
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// NAPI Function: Start Window Selection
|
|
954
|
+
Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
955
|
+
Napi::Env env = info.Env();
|
|
956
|
+
|
|
957
|
+
if (g_isWindowSelecting) {
|
|
958
|
+
Napi::TypeError::New(env, "Window selection already in progress").ThrowAsJavaScriptException();
|
|
959
|
+
return env.Null();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
@try {
|
|
963
|
+
// Get all windows
|
|
964
|
+
g_allWindows = [getAllSelectableWindows() retain];
|
|
965
|
+
|
|
966
|
+
if (!g_allWindows || [g_allWindows count] == 0) {
|
|
967
|
+
Napi::Error::New(env, "No selectable windows found").ThrowAsJavaScriptException();
|
|
968
|
+
return env.Null();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Create overlay window (initially hidden)
|
|
972
|
+
NSRect initialFrame = NSMakeRect(0, 0, 100, 100);
|
|
973
|
+
g_overlayWindow = [[NSWindow alloc] initWithContentRect:initialFrame
|
|
974
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
975
|
+
backing:NSBackingStoreBuffered
|
|
976
|
+
defer:NO];
|
|
977
|
+
|
|
978
|
+
[g_overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Absolute highest level
|
|
979
|
+
[g_overlayWindow setOpaque:NO];
|
|
980
|
+
[g_overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
981
|
+
[g_overlayWindow setIgnoresMouseEvents:NO];
|
|
982
|
+
[g_overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
983
|
+
[g_overlayWindow setHasShadow:NO];
|
|
984
|
+
[g_overlayWindow setAlphaValue:1.0];
|
|
985
|
+
[g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
986
|
+
|
|
987
|
+
// Create overlay view
|
|
988
|
+
g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:initialFrame];
|
|
989
|
+
[g_overlayWindow setContentView:g_overlayView];
|
|
990
|
+
|
|
991
|
+
// Create select button with blue theme
|
|
992
|
+
g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 160, 60)];
|
|
993
|
+
[g_selectButton setTitle:@"Start Record"];
|
|
994
|
+
[g_selectButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
995
|
+
[g_selectButton setBezelStyle:NSBezelStyleRounded];
|
|
996
|
+
[g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]];
|
|
997
|
+
|
|
998
|
+
// Blue background with white text
|
|
999
|
+
[g_selectButton setWantsLayer:YES];
|
|
1000
|
+
[g_selectButton.layer setBackgroundColor:[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor]];
|
|
1001
|
+
[g_selectButton.layer setCornerRadius:8.0];
|
|
1002
|
+
[g_selectButton.layer setBorderColor:[[NSColor colorWithRed:0.0 green:0.3 blue:0.7 alpha:1.0] CGColor]];
|
|
1003
|
+
[g_selectButton.layer setBorderWidth:2.0];
|
|
1004
|
+
|
|
1005
|
+
// White text color
|
|
1006
|
+
NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
|
|
1007
|
+
initWithString:[g_selectButton title]];
|
|
1008
|
+
[titleString addAttribute:NSForegroundColorAttributeName
|
|
1009
|
+
value:[NSColor whiteColor]
|
|
1010
|
+
range:NSMakeRange(0, [titleString length])];
|
|
1011
|
+
[g_selectButton setAttributedTitle:titleString];
|
|
1012
|
+
|
|
1013
|
+
// Add shadow for better visibility
|
|
1014
|
+
[g_selectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
1015
|
+
[g_selectButton.layer setShadowOffset:NSMakeSize(0, -2)];
|
|
1016
|
+
[g_selectButton.layer setShadowRadius:4.0];
|
|
1017
|
+
[g_selectButton.layer setShadowOpacity:0.3];
|
|
1018
|
+
|
|
1019
|
+
// Create delegate for button action and timer
|
|
1020
|
+
g_delegate = [[WindowSelectorDelegate alloc] init];
|
|
1021
|
+
[g_selectButton setTarget:g_delegate];
|
|
1022
|
+
[g_selectButton setAction:@selector(selectButtonClicked:)];
|
|
1023
|
+
|
|
1024
|
+
// Add select button directly to window (not view) for proper layering
|
|
1025
|
+
[g_overlayWindow.contentView addSubview:g_selectButton];
|
|
1026
|
+
|
|
1027
|
+
// Create cancel button
|
|
1028
|
+
NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
|
|
1029
|
+
[cancelButton setTitle:@"Cancel"];
|
|
1030
|
+
[cancelButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
1031
|
+
[cancelButton setBezelStyle:NSBezelStyleRounded];
|
|
1032
|
+
[cancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
|
|
1033
|
+
|
|
1034
|
+
// Gray cancel button styling
|
|
1035
|
+
[cancelButton setWantsLayer:YES];
|
|
1036
|
+
[cancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.8] CGColor]];
|
|
1037
|
+
[cancelButton.layer setCornerRadius:6.0];
|
|
1038
|
+
[cancelButton.layer setBorderColor:[[NSColor colorWithRed:0.4 green:0.4 blue:0.4 alpha:1.0] CGColor]];
|
|
1039
|
+
[cancelButton.layer setBorderWidth:1.0];
|
|
1040
|
+
|
|
1041
|
+
// White text for cancel button
|
|
1042
|
+
NSMutableAttributedString *cancelTitleString = [[NSMutableAttributedString alloc]
|
|
1043
|
+
initWithString:[cancelButton title]];
|
|
1044
|
+
[cancelTitleString addAttribute:NSForegroundColorAttributeName
|
|
1045
|
+
value:[NSColor whiteColor]
|
|
1046
|
+
range:NSMakeRange(0, [cancelTitleString length])];
|
|
1047
|
+
[cancelButton setAttributedTitle:cancelTitleString];
|
|
1048
|
+
|
|
1049
|
+
// Add shadow for cancel button
|
|
1050
|
+
[cancelButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
|
|
1051
|
+
[cancelButton.layer setShadowOffset:NSMakeSize(0, -1)];
|
|
1052
|
+
[cancelButton.layer setShadowRadius:2.0];
|
|
1053
|
+
[cancelButton.layer setShadowOpacity:0.2];
|
|
1054
|
+
|
|
1055
|
+
[cancelButton setTarget:g_delegate];
|
|
1056
|
+
[cancelButton setAction:@selector(cancelButtonClicked:)];
|
|
1057
|
+
|
|
1058
|
+
// Add cancel button to window
|
|
1059
|
+
[g_overlayWindow.contentView addSubview:cancelButton];
|
|
1060
|
+
|
|
1061
|
+
// Cancel button reference will be found dynamically in positioning code
|
|
1062
|
+
|
|
1063
|
+
// Timer approach doesn't work well with Node.js
|
|
1064
|
+
// Instead, we'll use JavaScript polling via getWindowSelectionStatus
|
|
1065
|
+
// The JS side will call this function repeatedly to trigger overlay updates
|
|
1066
|
+
g_trackingTimer = nil; // No timer for now
|
|
1067
|
+
|
|
1068
|
+
// Add ESC key event monitor to cancel selection
|
|
1069
|
+
g_windowKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
|
|
1070
|
+
handler:^(NSEvent *event) {
|
|
1071
|
+
if ([event keyCode] == 53) { // ESC key
|
|
1072
|
+
NSLog(@"🪟 WINDOW SELECTION: ESC pressed - cancelling selection");
|
|
1073
|
+
cleanupWindowSelector();
|
|
1074
|
+
}
|
|
1075
|
+
}];
|
|
1076
|
+
|
|
1077
|
+
g_isWindowSelecting = true;
|
|
1078
|
+
g_selectedWindowInfo = nil;
|
|
1079
|
+
|
|
1080
|
+
return Napi::Boolean::New(env, true);
|
|
1081
|
+
|
|
1082
|
+
} @catch (NSException *exception) {
|
|
1083
|
+
cleanupWindowSelector();
|
|
1084
|
+
Napi::Error::New(env, [[exception reason] UTF8String]).ThrowAsJavaScriptException();
|
|
1085
|
+
return env.Null();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// NAPI Function: Stop Window Selection
|
|
1090
|
+
Napi::Value StopWindowSelection(const Napi::CallbackInfo& info) {
|
|
1091
|
+
Napi::Env env = info.Env();
|
|
1092
|
+
|
|
1093
|
+
if (!g_isWindowSelecting) {
|
|
1094
|
+
return Napi::Boolean::New(env, false);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
cleanupWindowSelector();
|
|
1098
|
+
return Napi::Boolean::New(env, true);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// NAPI Function: Get Selected Window Info
|
|
1102
|
+
Napi::Value GetSelectedWindowInfo(const Napi::CallbackInfo& info) {
|
|
1103
|
+
Napi::Env env = info.Env();
|
|
1104
|
+
|
|
1105
|
+
if (!g_selectedWindowInfo) {
|
|
1106
|
+
return env.Null();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
@try {
|
|
1110
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1111
|
+
result.Set("id", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"id"] intValue]));
|
|
1112
|
+
result.Set("title", Napi::String::New(env, [[g_selectedWindowInfo objectForKey:@"title"] UTF8String]));
|
|
1113
|
+
result.Set("appName", Napi::String::New(env, [[g_selectedWindowInfo objectForKey:@"appName"] UTF8String]));
|
|
1114
|
+
// Original CGWindow coordinates
|
|
1115
|
+
result.Set("x", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"x"] intValue]));
|
|
1116
|
+
result.Set("y", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"y"] intValue]));
|
|
1117
|
+
result.Set("width", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"width"] intValue]));
|
|
1118
|
+
result.Set("height", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"height"] intValue]));
|
|
1119
|
+
|
|
1120
|
+
// Add overlay coordinates for direct use in recording
|
|
1121
|
+
// These are the exact coordinates used by the recording preview overlay
|
|
1122
|
+
int windowX = [[g_selectedWindowInfo objectForKey:@"x"] intValue];
|
|
1123
|
+
int windowY = [[g_selectedWindowInfo objectForKey:@"y"] intValue];
|
|
1124
|
+
int windowWidth = [[g_selectedWindowInfo objectForKey:@"width"] intValue];
|
|
1125
|
+
int windowHeight = [[g_selectedWindowInfo objectForKey:@"height"] intValue];
|
|
1126
|
+
|
|
1127
|
+
result.Set("overlayX", Napi::Number::New(env, windowX));
|
|
1128
|
+
result.Set("overlayY", Napi::Number::New(env, windowY));
|
|
1129
|
+
result.Set("overlayWidth", Napi::Number::New(env, windowWidth));
|
|
1130
|
+
result.Set("overlayHeight", Napi::Number::New(env, windowHeight));
|
|
1131
|
+
|
|
1132
|
+
// Determine which screen this window is on
|
|
1133
|
+
int x = [[g_selectedWindowInfo objectForKey:@"x"] intValue];
|
|
1134
|
+
int y = [[g_selectedWindowInfo objectForKey:@"y"] intValue];
|
|
1135
|
+
int width = [[g_selectedWindowInfo objectForKey:@"width"] intValue];
|
|
1136
|
+
int height = [[g_selectedWindowInfo objectForKey:@"height"] intValue];
|
|
1137
|
+
|
|
1138
|
+
NSLog(@"🎯 WINDOW SELECTED: %@ - \"%@\"",
|
|
1139
|
+
[g_selectedWindowInfo objectForKey:@"appName"],
|
|
1140
|
+
[g_selectedWindowInfo objectForKey:@"title"]);
|
|
1141
|
+
NSLog(@" 📊 Details: ID=%@, Pos=(%d,%d), Size=%dx%d",
|
|
1142
|
+
[g_selectedWindowInfo objectForKey:@"id"], x, y, width, height);
|
|
1143
|
+
|
|
1144
|
+
// Get all screens
|
|
1145
|
+
NSArray *screens = [NSScreen screens];
|
|
1146
|
+
NSScreen *windowScreen = nil;
|
|
1147
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
1148
|
+
|
|
1149
|
+
for (NSScreen *screen in screens) {
|
|
1150
|
+
NSRect screenFrame = [screen frame];
|
|
1151
|
+
|
|
1152
|
+
// Convert window coordinates to screen-relative
|
|
1153
|
+
if (x >= screenFrame.origin.x &&
|
|
1154
|
+
x < screenFrame.origin.x + screenFrame.size.width &&
|
|
1155
|
+
y >= screenFrame.origin.y &&
|
|
1156
|
+
y < screenFrame.origin.y + screenFrame.size.height) {
|
|
1157
|
+
windowScreen = screen;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (!windowScreen) {
|
|
1163
|
+
windowScreen = mainScreen;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Add screen information
|
|
1167
|
+
NSRect screenFrame = [windowScreen frame];
|
|
1168
|
+
result.Set("screenId", Napi::Number::New(env, [[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] ?
|
|
1169
|
+
[[[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue] : 0));
|
|
1170
|
+
result.Set("screenX", Napi::Number::New(env, (int)screenFrame.origin.x));
|
|
1171
|
+
result.Set("screenY", Napi::Number::New(env, (int)screenFrame.origin.y));
|
|
1172
|
+
result.Set("screenWidth", Napi::Number::New(env, (int)screenFrame.size.width));
|
|
1173
|
+
result.Set("screenHeight", Napi::Number::New(env, (int)screenFrame.size.height));
|
|
1174
|
+
|
|
1175
|
+
// Clear selected window info after reading
|
|
1176
|
+
[g_selectedWindowInfo release];
|
|
1177
|
+
g_selectedWindowInfo = nil;
|
|
1178
|
+
|
|
1179
|
+
return result;
|
|
1180
|
+
|
|
1181
|
+
} @catch (NSException *exception) {
|
|
1182
|
+
return env.Null();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// NAPI Function: Bring Window To Front
|
|
1187
|
+
Napi::Value BringWindowToFront(const Napi::CallbackInfo& info) {
|
|
1188
|
+
Napi::Env env = info.Env();
|
|
1189
|
+
|
|
1190
|
+
if (info.Length() < 1) {
|
|
1191
|
+
Napi::TypeError::New(env, "Window ID required").ThrowAsJavaScriptException();
|
|
1192
|
+
return env.Null();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
int windowId = info[0].As<Napi::Number>().Int32Value();
|
|
1196
|
+
|
|
1197
|
+
@try {
|
|
1198
|
+
bool success = bringWindowToFront(windowId);
|
|
1199
|
+
return Napi::Boolean::New(env, success);
|
|
1200
|
+
|
|
1201
|
+
} @catch (NSException *exception) {
|
|
1202
|
+
return Napi::Boolean::New(env, false);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// NAPI Function: Enable/Disable Auto Bring To Front
|
|
1207
|
+
Napi::Value SetBringToFrontEnabled(const Napi::CallbackInfo& info) {
|
|
1208
|
+
Napi::Env env = info.Env();
|
|
1209
|
+
|
|
1210
|
+
if (info.Length() < 1) {
|
|
1211
|
+
Napi::TypeError::New(env, "Boolean value required").ThrowAsJavaScriptException();
|
|
1212
|
+
return env.Null();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
bool enabled = info[0].As<Napi::Boolean>();
|
|
1216
|
+
g_bringToFrontEnabled = enabled;
|
|
1217
|
+
|
|
1218
|
+
NSLog(@"🔄 Auto bring-to-front: %s", enabled ? "ENABLED" : "DISABLED");
|
|
1219
|
+
|
|
1220
|
+
return Napi::Boolean::New(env, true);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// NAPI Function: Get Window Selection Status
|
|
1224
|
+
Napi::Value GetWindowSelectionStatus(const Napi::CallbackInfo& info) {
|
|
1225
|
+
Napi::Env env = info.Env();
|
|
1226
|
+
|
|
1227
|
+
// Update overlay each time status is requested (JavaScript polling approach)
|
|
1228
|
+
if (g_isWindowSelecting) {
|
|
1229
|
+
updateOverlay();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1233
|
+
result.Set("isSelecting", Napi::Boolean::New(env, g_isWindowSelecting));
|
|
1234
|
+
result.Set("hasSelectedWindow", Napi::Boolean::New(env, g_selectedWindowInfo != nil));
|
|
1235
|
+
result.Set("windowCount", Napi::Number::New(env, g_allWindows ? [g_allWindows count] : 0));
|
|
1236
|
+
result.Set("hasOverlay", Napi::Boolean::New(env, g_overlayWindow != nil));
|
|
1237
|
+
|
|
1238
|
+
if (g_currentWindowUnderCursor) {
|
|
1239
|
+
Napi::Object currentWindow = Napi::Object::New(env);
|
|
1240
|
+
currentWindow.Set("id", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"id"] intValue]));
|
|
1241
|
+
currentWindow.Set("title", Napi::String::New(env, [[g_currentWindowUnderCursor objectForKey:@"title"] UTF8String]));
|
|
1242
|
+
currentWindow.Set("appName", Napi::String::New(env, [[g_currentWindowUnderCursor objectForKey:@"appName"] UTF8String]));
|
|
1243
|
+
currentWindow.Set("x", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"x"] intValue]));
|
|
1244
|
+
currentWindow.Set("y", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"y"] intValue]));
|
|
1245
|
+
currentWindow.Set("width", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"width"] intValue]));
|
|
1246
|
+
currentWindow.Set("height", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"height"] intValue]));
|
|
1247
|
+
result.Set("currentWindow", currentWindow);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return result;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// NAPI Function: Show Recording Preview
|
|
1254
|
+
Napi::Value ShowRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1255
|
+
Napi::Env env = info.Env();
|
|
1256
|
+
|
|
1257
|
+
if (info.Length() < 1) {
|
|
1258
|
+
Napi::TypeError::New(env, "Window info object required").ThrowAsJavaScriptException();
|
|
1259
|
+
return env.Null();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (!info[0].IsObject()) {
|
|
1263
|
+
Napi::TypeError::New(env, "Window info must be an object").ThrowAsJavaScriptException();
|
|
1264
|
+
return env.Null();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
@try {
|
|
1268
|
+
Napi::Object windowInfoObj = info[0].As<Napi::Object>();
|
|
1269
|
+
|
|
1270
|
+
// Convert NAPI object to NSDictionary
|
|
1271
|
+
NSMutableDictionary *windowInfo = [[NSMutableDictionary alloc] init];
|
|
1272
|
+
|
|
1273
|
+
if (windowInfoObj.Has("id")) {
|
|
1274
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
|
|
1275
|
+
}
|
|
1276
|
+
if (windowInfoObj.Has("title")) {
|
|
1277
|
+
[windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("title").As<Napi::String>().Utf8Value().c_str()] forKey:@"title"];
|
|
1278
|
+
}
|
|
1279
|
+
if (windowInfoObj.Has("appName")) {
|
|
1280
|
+
[windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("appName").As<Napi::String>().Utf8Value().c_str()] forKey:@"appName"];
|
|
1281
|
+
}
|
|
1282
|
+
if (windowInfoObj.Has("x")) {
|
|
1283
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
|
|
1284
|
+
}
|
|
1285
|
+
if (windowInfoObj.Has("y")) {
|
|
1286
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
|
|
1287
|
+
}
|
|
1288
|
+
if (windowInfoObj.Has("width")) {
|
|
1289
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
|
|
1290
|
+
}
|
|
1291
|
+
if (windowInfoObj.Has("height")) {
|
|
1292
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
bool success = showRecordingPreview(windowInfo);
|
|
1296
|
+
[windowInfo release];
|
|
1297
|
+
|
|
1298
|
+
return Napi::Boolean::New(env, success);
|
|
1299
|
+
|
|
1300
|
+
} @catch (NSException *exception) {
|
|
1301
|
+
return Napi::Boolean::New(env, false);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// NAPI Function: Hide Recording Preview
|
|
1306
|
+
Napi::Value HideRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1307
|
+
Napi::Env env = info.Env();
|
|
1308
|
+
|
|
1309
|
+
@try {
|
|
1310
|
+
bool success = hideRecordingPreview();
|
|
1311
|
+
return Napi::Boolean::New(env, success);
|
|
1312
|
+
|
|
1313
|
+
} @catch (NSException *exception) {
|
|
1314
|
+
return Napi::Boolean::New(env, false);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// NAPI Function: Start Screen Selection
|
|
1319
|
+
Napi::Value StartScreenSelection(const Napi::CallbackInfo& info) {
|
|
1320
|
+
Napi::Env env = info.Env();
|
|
1321
|
+
|
|
1322
|
+
@try {
|
|
1323
|
+
bool success = startScreenSelection();
|
|
1324
|
+
return Napi::Boolean::New(env, success);
|
|
1325
|
+
|
|
1326
|
+
} @catch (NSException *exception) {
|
|
1327
|
+
return Napi::Boolean::New(env, false);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// NAPI Function: Stop Screen Selection
|
|
1332
|
+
Napi::Value StopScreenSelection(const Napi::CallbackInfo& info) {
|
|
1333
|
+
Napi::Env env = info.Env();
|
|
1334
|
+
|
|
1335
|
+
@try {
|
|
1336
|
+
bool success = stopScreenSelection();
|
|
1337
|
+
return Napi::Boolean::New(env, success);
|
|
1338
|
+
|
|
1339
|
+
} @catch (NSException *exception) {
|
|
1340
|
+
return Napi::Boolean::New(env, false);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// NAPI Function: Get Selected Screen Info
|
|
1345
|
+
Napi::Value GetSelectedScreenInfo(const Napi::CallbackInfo& info) {
|
|
1346
|
+
Napi::Env env = info.Env();
|
|
1347
|
+
|
|
1348
|
+
@try {
|
|
1349
|
+
NSDictionary *screenInfo = getSelectedScreenInfo();
|
|
1350
|
+
if (!screenInfo) {
|
|
1351
|
+
return env.Null();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1355
|
+
result.Set("id", Napi::Number::New(env, [[screenInfo objectForKey:@"id"] intValue]));
|
|
1356
|
+
result.Set("name", Napi::String::New(env, [[screenInfo objectForKey:@"name"] UTF8String]));
|
|
1357
|
+
result.Set("x", Napi::Number::New(env, [[screenInfo objectForKey:@"x"] intValue]));
|
|
1358
|
+
result.Set("y", Napi::Number::New(env, [[screenInfo objectForKey:@"y"] intValue]));
|
|
1359
|
+
result.Set("width", Napi::Number::New(env, [[screenInfo objectForKey:@"width"] intValue]));
|
|
1360
|
+
result.Set("height", Napi::Number::New(env, [[screenInfo objectForKey:@"height"] intValue]));
|
|
1361
|
+
result.Set("resolution", Napi::String::New(env, [[screenInfo objectForKey:@"resolution"] UTF8String]));
|
|
1362
|
+
result.Set("isPrimary", Napi::Boolean::New(env, [[screenInfo objectForKey:@"isPrimary"] boolValue]));
|
|
1363
|
+
|
|
1364
|
+
NSLog(@"🖥️ SCREEN SELECTED: %@ (%@)",
|
|
1365
|
+
[screenInfo objectForKey:@"name"],
|
|
1366
|
+
[screenInfo objectForKey:@"resolution"]);
|
|
1367
|
+
|
|
1368
|
+
return result;
|
|
1369
|
+
|
|
1370
|
+
} @catch (NSException *exception) {
|
|
1371
|
+
return env.Null();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// NAPI Function: Show Screen Recording Preview
|
|
1376
|
+
Napi::Value ShowScreenRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1377
|
+
Napi::Env env = info.Env();
|
|
1378
|
+
|
|
1379
|
+
if (info.Length() < 1) {
|
|
1380
|
+
Napi::TypeError::New(env, "Screen info object required").ThrowAsJavaScriptException();
|
|
1381
|
+
return env.Null();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (!info[0].IsObject()) {
|
|
1385
|
+
Napi::TypeError::New(env, "Screen info must be an object").ThrowAsJavaScriptException();
|
|
1386
|
+
return env.Null();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
@try {
|
|
1390
|
+
Napi::Object screenInfoObj = info[0].As<Napi::Object>();
|
|
1391
|
+
|
|
1392
|
+
// Convert NAPI object to NSDictionary
|
|
1393
|
+
NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
|
|
1394
|
+
|
|
1395
|
+
if (screenInfoObj.Has("id")) {
|
|
1396
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
|
|
1397
|
+
}
|
|
1398
|
+
if (screenInfoObj.Has("name")) {
|
|
1399
|
+
[screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("name").As<Napi::String>().Utf8Value().c_str()] forKey:@"name"];
|
|
1400
|
+
}
|
|
1401
|
+
if (screenInfoObj.Has("resolution")) {
|
|
1402
|
+
[screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("resolution").As<Napi::String>().Utf8Value().c_str()] forKey:@"resolution"];
|
|
1403
|
+
}
|
|
1404
|
+
if (screenInfoObj.Has("x")) {
|
|
1405
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
|
|
1406
|
+
}
|
|
1407
|
+
if (screenInfoObj.Has("y")) {
|
|
1408
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
|
|
1409
|
+
}
|
|
1410
|
+
if (screenInfoObj.Has("width")) {
|
|
1411
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
|
|
1412
|
+
}
|
|
1413
|
+
if (screenInfoObj.Has("height")) {
|
|
1414
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
bool success = showScreenRecordingPreview(screenInfo);
|
|
1418
|
+
[screenInfo release];
|
|
1419
|
+
|
|
1420
|
+
return Napi::Boolean::New(env, success);
|
|
1421
|
+
|
|
1422
|
+
} @catch (NSException *exception) {
|
|
1423
|
+
return Napi::Boolean::New(env, false);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// NAPI Function: Hide Screen Recording Preview
|
|
1428
|
+
Napi::Value HideScreenRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1429
|
+
Napi::Env env = info.Env();
|
|
1430
|
+
|
|
1431
|
+
@try {
|
|
1432
|
+
bool success = hideScreenRecordingPreview();
|
|
1433
|
+
return Napi::Boolean::New(env, success);
|
|
1434
|
+
|
|
1435
|
+
} @catch (NSException *exception) {
|
|
1436
|
+
return Napi::Boolean::New(env, false);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Export functions
|
|
1441
|
+
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
|
|
1442
|
+
exports.Set("startWindowSelection", Napi::Function::New(env, StartWindowSelection));
|
|
1443
|
+
exports.Set("stopWindowSelection", Napi::Function::New(env, StopWindowSelection));
|
|
1444
|
+
exports.Set("getSelectedWindowInfo", Napi::Function::New(env, GetSelectedWindowInfo));
|
|
1445
|
+
exports.Set("getWindowSelectionStatus", Napi::Function::New(env, GetWindowSelectionStatus));
|
|
1446
|
+
exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
|
|
1447
|
+
exports.Set("setBringToFrontEnabled", Napi::Function::New(env, SetBringToFrontEnabled));
|
|
1448
|
+
exports.Set("showRecordingPreview", Napi::Function::New(env, ShowRecordingPreview));
|
|
1449
|
+
exports.Set("hideRecordingPreview", Napi::Function::New(env, HideRecordingPreview));
|
|
1450
|
+
exports.Set("startScreenSelection", Napi::Function::New(env, StartScreenSelection));
|
|
1451
|
+
exports.Set("stopScreenSelection", Napi::Function::New(env, StopScreenSelection));
|
|
1452
|
+
exports.Set("getSelectedScreenInfo", Napi::Function::New(env, GetSelectedScreenInfo));
|
|
1453
|
+
exports.Set("showScreenRecordingPreview", Napi::Function::New(env, ShowScreenRecordingPreview));
|
|
1454
|
+
exports.Set("hideScreenRecordingPreview", Napi::Function::New(env, HideScreenRecordingPreview));
|
|
1455
|
+
|
|
1456
|
+
return exports;
|
|
1457
|
+
}
|