node-mac-recorder 2.7.1 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/window_selector.mm +330 -254
- package/src/window_selector.mm.bak +2051 -0
|
@@ -0,0 +1,2051 @@
|
|
|
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
|
+
#import <QuartzCore/QuartzCore.h>
|
|
9
|
+
|
|
10
|
+
// Forward declarations
|
|
11
|
+
@class WindowSelectorOverlayView;
|
|
12
|
+
|
|
13
|
+
// Global state for window selection
|
|
14
|
+
static bool g_isWindowSelecting = false;
|
|
15
|
+
static NSWindow *g_overlayWindow = nil;
|
|
16
|
+
static NSView *g_overlayView = nil;
|
|
17
|
+
static WindowSelectorOverlayView *g_selectedOverlayView = nil; // Track selected overlay
|
|
18
|
+
static NSButton *g_selectButton = nil;
|
|
19
|
+
static NSTimer *g_trackingTimer = nil;
|
|
20
|
+
static NSDictionary *g_selectedWindowInfo = nil;
|
|
21
|
+
static NSMutableArray *g_allWindows = nil;
|
|
22
|
+
static NSDictionary *g_currentWindowUnderCursor = nil;
|
|
23
|
+
static bool g_bringToFrontEnabled = true; // Default enabled
|
|
24
|
+
static id g_windowKeyEventMonitor = nil;
|
|
25
|
+
|
|
26
|
+
// Recording preview overlay state
|
|
27
|
+
static NSWindow *g_recordingPreviewWindow = nil;
|
|
28
|
+
static NSView *g_recordingPreviewView = nil;
|
|
29
|
+
static NSDictionary *g_recordingWindowInfo = nil;
|
|
30
|
+
|
|
31
|
+
// Screen selection overlay state
|
|
32
|
+
static bool g_isScreenSelecting = false;
|
|
33
|
+
static NSMutableArray *g_screenOverlayWindows = nil;
|
|
34
|
+
static NSDictionary *g_selectedScreenInfo = nil;
|
|
35
|
+
static NSArray *g_allScreens = nil;
|
|
36
|
+
static id g_screenKeyEventMonitor = nil;
|
|
37
|
+
static NSTimer *g_screenTrackingTimer = nil;
|
|
38
|
+
static NSInteger g_currentActiveScreenIndex = -1;
|
|
39
|
+
|
|
40
|
+
// Forward declarations
|
|
41
|
+
void cleanupWindowSelector();
|
|
42
|
+
void updateOverlay();
|
|
43
|
+
NSDictionary* getWindowUnderCursor(CGPoint point);
|
|
44
|
+
NSArray* getAllSelectableWindows();
|
|
45
|
+
bool bringWindowToFront(int windowId);
|
|
46
|
+
void cleanupRecordingPreview();
|
|
47
|
+
bool showRecordingPreview(NSDictionary *windowInfo);
|
|
48
|
+
bool hideRecordingPreview();
|
|
49
|
+
void cleanupScreenSelector();
|
|
50
|
+
bool startScreenSelection();
|
|
51
|
+
bool stopScreenSelection();
|
|
52
|
+
NSDictionary* getSelectedScreenInfo();
|
|
53
|
+
bool showScreenRecordingPreview(NSDictionary *screenInfo);
|
|
54
|
+
bool hideScreenRecordingPreview();
|
|
55
|
+
void updateScreenOverlays();
|
|
56
|
+
|
|
57
|
+
// Custom overlay view class
|
|
58
|
+
@interface WindowSelectorOverlayView : NSView
|
|
59
|
+
@property (nonatomic, strong) NSDictionary *windowInfo;
|
|
60
|
+
@property (nonatomic) BOOL isActiveWindow;
|
|
61
|
+
@property (nonatomic) BOOL isSelectedWindow;
|
|
62
|
+
@end
|
|
63
|
+
|
|
64
|
+
@implementation WindowSelectorOverlayView
|
|
65
|
+
|
|
66
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
67
|
+
self = [super initWithFrame:frameRect];
|
|
68
|
+
if (self) {
|
|
69
|
+
// Use layer for background instead of custom drawing
|
|
70
|
+
self.wantsLayer = YES;
|
|
71
|
+
self.isActiveWindow = NO; // Default to inactive (no mouse highlighting)
|
|
72
|
+
self.isSelectedWindow = NO; // Default to not selected
|
|
73
|
+
|
|
74
|
+
// Set initial appearance
|
|
75
|
+
[self updateAppearance];
|
|
76
|
+
|
|
77
|
+
// Window selector overlay view created
|
|
78
|
+
}
|
|
79
|
+
return self;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
- (void)updateAppearance {
|
|
83
|
+
if (self.isSelectedWindow) {
|
|
84
|
+
// Selected window: bright background with thick border
|
|
85
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.6] CGColor];
|
|
86
|
+
self.layer.borderColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:1.0] CGColor];
|
|
87
|
+
self.layer.borderWidth = 3.0; // Thick border for selected window
|
|
88
|
+
// Selected window appearance set
|
|
89
|
+
} else if (self.isActiveWindow) {
|
|
90
|
+
// Active window: brighter background with thin border
|
|
91
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] CGColor];
|
|
92
|
+
self.layer.borderColor = [[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.8] CGColor];
|
|
93
|
+
self.layer.borderWidth = 1.0; // Thin border for active window
|
|
94
|
+
// Active window appearance set
|
|
95
|
+
} else {
|
|
96
|
+
// Inactive window: dimmer background with no border
|
|
97
|
+
self.layer.backgroundColor = [[NSColor colorWithRed:0.4 green:0.2 blue:0.6 alpha:0.25] CGColor];
|
|
98
|
+
self.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
99
|
+
self.layer.borderWidth = 0.0; // No border for inactive window
|
|
100
|
+
// Inactive window appearance set
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Common styling
|
|
104
|
+
self.layer.cornerRadius = 8.0;
|
|
105
|
+
self.layer.masksToBounds = YES;
|
|
106
|
+
self.layer.shadowOpacity = 0.0;
|
|
107
|
+
self.layer.shadowRadius = 0.0;
|
|
108
|
+
self.layer.shadowOffset = NSMakeSize(0, 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
- (void)setIsActiveWindow:(BOOL)isActiveWindow {
|
|
112
|
+
if (_isActiveWindow != isActiveWindow) {
|
|
113
|
+
_isActiveWindow = isActiveWindow;
|
|
114
|
+
[self updateAppearance];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
- (void)setIsSelectedWindow:(BOOL)isSelectedWindow {
|
|
119
|
+
if (_isSelectedWindow != isSelectedWindow) {
|
|
120
|
+
_isSelectedWindow = isSelectedWindow;
|
|
121
|
+
[self updateAppearance];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
- (void)mouseDown:(NSEvent *)event {
|
|
126
|
+
// Handle mouse click to select this window
|
|
127
|
+
if (self.windowInfo) {
|
|
128
|
+
// Set this overlay as selected
|
|
129
|
+
self.isSelectedWindow = YES;
|
|
130
|
+
|
|
131
|
+
// Update global selected window info
|
|
132
|
+
if (g_selectedWindowInfo) {
|
|
133
|
+
[g_selectedWindowInfo release];
|
|
134
|
+
}
|
|
135
|
+
g_selectedWindowInfo = [self.windowInfo retain];
|
|
136
|
+
|
|
137
|
+
// Update global selected overlay reference
|
|
138
|
+
g_selectedOverlayView = self;
|
|
139
|
+
|
|
140
|
+
// Update overlay appearance
|
|
141
|
+
[self updateAppearance];
|
|
142
|
+
|
|
143
|
+
NSLog(@"🎯 WINDOW SELECTED VIA CLICK: %@ - \"%@\"",
|
|
144
|
+
[self.windowInfo objectForKey:@"appName"] ?: @"Unknown",
|
|
145
|
+
[self.windowInfo objectForKey:@"title"] ?: @"Untitled");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Layer-based approach, no custom drawing needed
|
|
150
|
+
|
|
151
|
+
@end
|
|
152
|
+
|
|
153
|
+
// Recording preview overlay view - full screen with cutout
|
|
154
|
+
@interface RecordingPreviewView : NSView
|
|
155
|
+
@property (nonatomic, strong) NSDictionary *recordingWindowInfo;
|
|
156
|
+
@end
|
|
157
|
+
|
|
158
|
+
@implementation RecordingPreviewView
|
|
159
|
+
|
|
160
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
161
|
+
self = [super initWithFrame:frameRect];
|
|
162
|
+
if (self) {
|
|
163
|
+
self.wantsLayer = YES;
|
|
164
|
+
self.layer.backgroundColor = [[NSColor clearColor] CGColor];
|
|
165
|
+
// Ensure no borders or decorations
|
|
166
|
+
self.layer.borderWidth = 0.0;
|
|
167
|
+
self.layer.cornerRadius = 8.0;
|
|
168
|
+
self.layer.masksToBounds = YES;
|
|
169
|
+
self.layer.shadowOpacity = 0.0;
|
|
170
|
+
self.layer.shadowRadius = 0.0;
|
|
171
|
+
self.layer.shadowOffset = NSMakeSize(0, 0);
|
|
172
|
+
}
|
|
173
|
+
return self;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
177
|
+
[super drawRect:dirtyRect];
|
|
178
|
+
|
|
179
|
+
if (!self.recordingWindowInfo) {
|
|
180
|
+
// No window info, fill with semi-transparent black
|
|
181
|
+
[[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
|
|
182
|
+
NSRectFill(dirtyRect);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get window coordinates
|
|
187
|
+
int windowX = [[self.recordingWindowInfo objectForKey:@"x"] intValue];
|
|
188
|
+
int windowY = [[self.recordingWindowInfo objectForKey:@"y"] intValue];
|
|
189
|
+
int windowWidth = [[self.recordingWindowInfo objectForKey:@"width"] intValue];
|
|
190
|
+
int windowHeight = [[self.recordingWindowInfo objectForKey:@"height"] intValue];
|
|
191
|
+
|
|
192
|
+
// Convert from CGWindow coordinates (top-left) to NSView coordinates (bottom-left)
|
|
193
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
194
|
+
CGFloat screenHeight = [mainScreen frame].size.height;
|
|
195
|
+
CGFloat convertedY = screenHeight - windowY - windowHeight;
|
|
196
|
+
|
|
197
|
+
NSRect windowRect = NSMakeRect(windowX, convertedY, windowWidth, windowHeight);
|
|
198
|
+
|
|
199
|
+
// Create a path that covers the entire view but excludes the window area
|
|
200
|
+
NSBezierPath *maskPath = [NSBezierPath bezierPathWithRect:self.bounds];
|
|
201
|
+
NSBezierPath *windowPath = [NSBezierPath bezierPathWithRect:windowRect];
|
|
202
|
+
[maskPath appendBezierPath:windowPath];
|
|
203
|
+
[maskPath setWindingRule:NSWindingRuleEvenOdd]; // Creates hole effect
|
|
204
|
+
|
|
205
|
+
// Fill with semi-transparent black, excluding window area
|
|
206
|
+
[[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
|
|
207
|
+
[maskPath fill];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@end
|
|
211
|
+
|
|
212
|
+
// Screen selection overlay view
|
|
213
|
+
@interface ScreenSelectorOverlayView : NSView
|
|
214
|
+
@property (nonatomic, strong) NSDictionary *screenInfo;
|
|
215
|
+
@property (nonatomic) BOOL isActiveScreen;
|
|
216
|
+
@end
|
|
217
|
+
|
|
218
|
+
@implementation ScreenSelectorOverlayView
|
|
219
|
+
|
|
220
|
+
- (instancetype)initWithFrame:(NSRect)frameRect {
|
|
221
|
+
self = [super initWithFrame:frameRect];
|
|
222
|
+
if (self) {
|
|
223
|
+
self.wantsLayer = YES;
|
|
224
|
+
self.layer.backgroundColor = [[NSColor clearColor] CGColor];
|
|
225
|
+
// Ensure no borders or decorations
|
|
226
|
+
self.layer.borderWidth = 0.0;
|
|
227
|
+
self.layer.cornerRadius = 8.0;
|
|
228
|
+
self.layer.masksToBounds = YES;
|
|
229
|
+
self.layer.shadowOpacity = 0.0;
|
|
230
|
+
self.layer.shadowRadius = 0.0;
|
|
231
|
+
self.layer.shadowOffset = NSMakeSize(0, 0);
|
|
232
|
+
self.isActiveScreen = NO;
|
|
233
|
+
}
|
|
234
|
+
return self;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
- (void)drawRect:(NSRect)dirtyRect {
|
|
238
|
+
[super drawRect:dirtyRect];
|
|
239
|
+
|
|
240
|
+
if (!self.screenInfo) return;
|
|
241
|
+
|
|
242
|
+
// Background with transparency - purple tone varies by active state
|
|
243
|
+
if (self.isActiveScreen) {
|
|
244
|
+
// Active screen: brighter, more opaque
|
|
245
|
+
[[NSColor colorWithRed:0.6 green:0.4 blue:0.9 alpha:0.4] setFill];
|
|
246
|
+
} else {
|
|
247
|
+
// Inactive screen: dimmer, less opaque
|
|
248
|
+
[[NSColor colorWithRed:0.4 green:0.2 blue:0.6 alpha:0.25] setFill];
|
|
249
|
+
}
|
|
250
|
+
NSRectFill(self.bounds);
|
|
251
|
+
|
|
252
|
+
// No border for clean look
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@end
|
|
256
|
+
|
|
257
|
+
// Button action handler and timer target
|
|
258
|
+
@interface WindowSelectorDelegate : NSObject
|
|
259
|
+
- (void)selectButtonClicked:(id)sender;
|
|
260
|
+
- (void)screenSelectButtonClicked:(id)sender;
|
|
261
|
+
- (void)cancelButtonClicked:(id)sender;
|
|
262
|
+
- (void)timerUpdate:(NSTimer *)timer;
|
|
263
|
+
@end
|
|
264
|
+
|
|
265
|
+
@implementation WindowSelectorDelegate
|
|
266
|
+
- (void)selectButtonClicked:(id)sender {
|
|
267
|
+
if (g_selectedWindowInfo) {
|
|
268
|
+
// Use the selected window info (from click) instead of cursor position
|
|
269
|
+
NSLog(@"🎯 START RECORDING: Selected window confirmed - %@ - \"%@\"",
|
|
270
|
+
[g_selectedWindowInfo objectForKey:@"appName"] ?: @"Unknown",
|
|
271
|
+
[g_selectedWindowInfo objectForKey:@"title"] ?: @"Untitled");
|
|
272
|
+
cleanupWindowSelector();
|
|
273
|
+
} else {
|
|
274
|
+
NSLog(@"⚠️ No window selected - cannot start recording");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
- (void)screenSelectButtonClicked:(id)sender {
|
|
279
|
+
NSButton *button = (NSButton *)sender;
|
|
280
|
+
NSInteger screenIndex = [button tag];
|
|
281
|
+
|
|
282
|
+
// Get screen info from global array using button tag
|
|
283
|
+
if (g_allScreens && screenIndex >= 0 && screenIndex < [g_allScreens count]) {
|
|
284
|
+
NSDictionary *screenInfo = [g_allScreens objectAtIndex:screenIndex];
|
|
285
|
+
g_selectedScreenInfo = [screenInfo retain];
|
|
286
|
+
|
|
287
|
+
NSLog(@"🖥️ SCREEN SELECTED: %@ (ID: %@)",
|
|
288
|
+
[screenInfo objectForKey:@"name"],
|
|
289
|
+
[screenInfo objectForKey:@"id"]);
|
|
290
|
+
|
|
291
|
+
cleanupScreenSelector();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
- (void)cancelButtonClicked:(id)sender {
|
|
296
|
+
NSLog(@"🚫 CANCEL BUTTON CLICKED: Selection cancelled");
|
|
297
|
+
// Clean up without selecting anything
|
|
298
|
+
if (g_isScreenSelecting) {
|
|
299
|
+
cleanupScreenSelector();
|
|
300
|
+
} else {
|
|
301
|
+
cleanupWindowSelector();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
- (void)timerUpdate:(NSTimer *)timer {
|
|
306
|
+
updateOverlay();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
- (void)screenTimerUpdate:(NSTimer *)timer {
|
|
310
|
+
updateScreenOverlays();
|
|
311
|
+
}
|
|
312
|
+
@end
|
|
313
|
+
|
|
314
|
+
static WindowSelectorDelegate *g_delegate = nil;
|
|
315
|
+
|
|
316
|
+
// Bring window to front using Accessibility API
|
|
317
|
+
bool bringWindowToFront(int windowId) {
|
|
318
|
+
@autoreleasepool {
|
|
319
|
+
@try {
|
|
320
|
+
// Method 1: Using Accessibility API (most reliable)
|
|
321
|
+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
322
|
+
if (!systemWide) return false;
|
|
323
|
+
|
|
324
|
+
CFArrayRef windowList = NULL;
|
|
325
|
+
AXError error = AXUIElementCopyAttributeValue(systemWide, kAXWindowsAttribute, (CFTypeRef*)&windowList);
|
|
326
|
+
|
|
327
|
+
if (error == kAXErrorSuccess && windowList) {
|
|
328
|
+
CFIndex windowCount = CFArrayGetCount(windowList);
|
|
329
|
+
|
|
330
|
+
for (CFIndex i = 0; i < windowCount; i++) {
|
|
331
|
+
AXUIElementRef windowElement = (AXUIElementRef)CFArrayGetValueAtIndex(windowList, i);
|
|
332
|
+
|
|
333
|
+
// Get window ID by comparing with CGWindowList
|
|
334
|
+
// Since _AXUIElementGetWindow is not available, we'll use app PID approach
|
|
335
|
+
pid_t windowPid;
|
|
336
|
+
error = AXUIElementGetPid(windowElement, &windowPid);
|
|
337
|
+
|
|
338
|
+
if (error == kAXErrorSuccess) {
|
|
339
|
+
// Get window info for this PID from CGWindowList
|
|
340
|
+
CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
|
|
341
|
+
if (cgWindowList) {
|
|
342
|
+
NSArray *windowArray = (__bridge NSArray *)cgWindowList;
|
|
343
|
+
|
|
344
|
+
for (NSDictionary *windowInfo in windowArray) {
|
|
345
|
+
NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
|
|
346
|
+
NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
|
|
347
|
+
|
|
348
|
+
if ([cgWindowId intValue] == windowId && [processId intValue] == windowPid) {
|
|
349
|
+
// Found the window, bring it to front
|
|
350
|
+
NSLog(@"🔝 BRINGING TO FRONT: Window ID %d (PID: %d)", windowId, windowPid);
|
|
351
|
+
|
|
352
|
+
// Method 1: Raise specific window (not the whole app)
|
|
353
|
+
error = AXUIElementPerformAction(windowElement, kAXRaiseAction);
|
|
354
|
+
if (error == kAXErrorSuccess) {
|
|
355
|
+
NSLog(@" ✅ Specific window raised successfully");
|
|
356
|
+
} else {
|
|
357
|
+
NSLog(@" ⚠️ Raise action failed: %d", error);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Method 2: Focus specific window (not main window)
|
|
361
|
+
error = AXUIElementSetAttributeValue(windowElement, kAXFocusedAttribute, kCFBooleanTrue);
|
|
362
|
+
if (error == kAXErrorSuccess) {
|
|
363
|
+
NSLog(@" ✅ Specific window focused");
|
|
364
|
+
} else {
|
|
365
|
+
NSLog(@" ⚠️ Focus failed: %d", error);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
CFRelease(cgWindowList);
|
|
369
|
+
CFRelease(windowList);
|
|
370
|
+
CFRelease(systemWide);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
CFRelease(cgWindowList);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
CFRelease(windowList);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
CFRelease(systemWide);
|
|
382
|
+
|
|
383
|
+
// Method 2: Light activation fallback (minimal app activation)
|
|
384
|
+
NSLog(@" 🔄 Trying minimal activation for window %d", windowId);
|
|
385
|
+
|
|
386
|
+
// Get window info to find the process
|
|
387
|
+
CFArrayRef cgWindowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
|
|
388
|
+
if (cgWindowList) {
|
|
389
|
+
NSArray *windowArray = (__bridge NSArray *)cgWindowList;
|
|
390
|
+
|
|
391
|
+
for (NSDictionary *windowInfo in windowArray) {
|
|
392
|
+
NSNumber *cgWindowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
|
|
393
|
+
if ([cgWindowId intValue] == windowId) {
|
|
394
|
+
// Get process ID
|
|
395
|
+
NSNumber *processId = [windowInfo objectForKey:(NSString *)kCGWindowOwnerPID];
|
|
396
|
+
if (processId) {
|
|
397
|
+
// Light activation - only bring app to front, don't activate all windows
|
|
398
|
+
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:[processId intValue]];
|
|
399
|
+
if (app) {
|
|
400
|
+
// Use NSApplicationActivateIgnoringOtherApps only (no NSApplicationActivateAllWindows)
|
|
401
|
+
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
|
|
402
|
+
NSLog(@" ✅ App minimally activated: PID %d (specific window should be frontmost)", [processId intValue]);
|
|
403
|
+
CFRelease(cgWindowList);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
CFRelease(cgWindowList);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return false;
|
|
414
|
+
|
|
415
|
+
} @catch (NSException *exception) {
|
|
416
|
+
NSLog(@"❌ Error bringing window to front: %@", exception);
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Get all selectable windows
|
|
423
|
+
NSArray* getAllSelectableWindows() {
|
|
424
|
+
@autoreleasepool {
|
|
425
|
+
NSMutableArray *windows = [NSMutableArray array];
|
|
426
|
+
|
|
427
|
+
// Get all windows using CGWindowListCopyWindowInfo
|
|
428
|
+
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
|
|
429
|
+
|
|
430
|
+
if (windowList) {
|
|
431
|
+
NSArray *windowArray = (__bridge NSArray *)windowList;
|
|
432
|
+
|
|
433
|
+
for (NSDictionary *windowInfo in windowArray) {
|
|
434
|
+
NSString *windowOwner = [windowInfo objectForKey:(NSString *)kCGWindowOwnerName];
|
|
435
|
+
NSString *windowName = [windowInfo objectForKey:(NSString *)kCGWindowName];
|
|
436
|
+
NSNumber *windowId = [windowInfo objectForKey:(NSString *)kCGWindowNumber];
|
|
437
|
+
NSNumber *windowLayer = [windowInfo objectForKey:(NSString *)kCGWindowLayer];
|
|
438
|
+
NSDictionary *bounds = [windowInfo objectForKey:(NSString *)kCGWindowBounds];
|
|
439
|
+
|
|
440
|
+
// Skip system windows, dock, menu bar, etc.
|
|
441
|
+
if ([windowLayer intValue] != 0) continue; // Only normal windows
|
|
442
|
+
if (!windowOwner || [windowOwner length] == 0) continue;
|
|
443
|
+
if ([windowOwner isEqualToString:@"WindowServer"]) continue;
|
|
444
|
+
if ([windowOwner isEqualToString:@"Dock"]) continue;
|
|
445
|
+
|
|
446
|
+
// Extract bounds
|
|
447
|
+
int x = [[bounds objectForKey:@"X"] intValue];
|
|
448
|
+
int y = [[bounds objectForKey:@"Y"] intValue];
|
|
449
|
+
int width = [[bounds objectForKey:@"Width"] intValue];
|
|
450
|
+
int height = [[bounds objectForKey:@"Height"] intValue];
|
|
451
|
+
|
|
452
|
+
// Skip too small windows
|
|
453
|
+
if (width < 50 || height < 50) continue;
|
|
454
|
+
|
|
455
|
+
NSDictionary *window = @{
|
|
456
|
+
@"id": windowId ?: @(0),
|
|
457
|
+
@"title": windowName ?: @"Untitled",
|
|
458
|
+
@"appName": windowOwner,
|
|
459
|
+
@"x": @(x),
|
|
460
|
+
@"y": @(y),
|
|
461
|
+
@"width": @(width),
|
|
462
|
+
@"height": @(height)
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
[windows addObject:window];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
CFRelease(windowList);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return [windows copy];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Get window under cursor point
|
|
476
|
+
NSDictionary* getWindowUnderCursor(CGPoint point) {
|
|
477
|
+
@autoreleasepool {
|
|
478
|
+
if (!g_allWindows) return nil;
|
|
479
|
+
|
|
480
|
+
// Find window that contains the cursor point
|
|
481
|
+
for (NSDictionary *window in g_allWindows) {
|
|
482
|
+
int x = [[window objectForKey:@"x"] intValue];
|
|
483
|
+
int y = [[window objectForKey:@"y"] intValue];
|
|
484
|
+
int width = [[window objectForKey:@"width"] intValue];
|
|
485
|
+
int height = [[window objectForKey:@"height"] intValue];
|
|
486
|
+
|
|
487
|
+
if (point.x >= x && point.x <= x + width &&
|
|
488
|
+
point.y >= y && point.y <= y + height) {
|
|
489
|
+
return window;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return nil;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Update overlay to show selected window (no more mouse-based highlighting)
|
|
498
|
+
void updateOverlay() {
|
|
499
|
+
@autoreleasepool {
|
|
500
|
+
if (!g_isWindowSelecting || !g_overlayWindow) return;
|
|
501
|
+
|
|
502
|
+
// Check if we have a selected window or if we should show overlay for selection
|
|
503
|
+
NSDictionary *windowUnderCursor = nil;
|
|
504
|
+
if (!g_selectedWindowInfo) {
|
|
505
|
+
// No window selected yet - show overlay for the first available window
|
|
506
|
+
if (g_allWindows && [g_allWindows count] > 0) {
|
|
507
|
+
windowUnderCursor = [g_allWindows objectAtIndex:0];
|
|
508
|
+
} else {
|
|
509
|
+
return; // No windows available
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
// Use selected window info
|
|
513
|
+
windowUnderCursor = g_selectedWindowInfo;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update overlay position and size
|
|
517
|
+
int x = [[windowUnderCursor objectForKey:@"x"] intValue];
|
|
518
|
+
int y = [[windowUnderCursor objectForKey:@"y"] intValue];
|
|
519
|
+
int width = [[windowUnderCursor objectForKey:@"width"] intValue];
|
|
520
|
+
int height = [[windowUnderCursor objectForKey:@"height"] intValue];
|
|
521
|
+
|
|
522
|
+
// Find which screen contains the window center
|
|
523
|
+
NSArray *screens = [NSScreen screens];
|
|
524
|
+
NSScreen *windowScreen = nil;
|
|
525
|
+
CGFloat windowCenterX = x + width / 2;
|
|
526
|
+
CGFloat windowCenterY = y + height / 2;
|
|
527
|
+
|
|
528
|
+
for (NSScreen *screen in screens) {
|
|
529
|
+
NSRect screenFrame = [screen frame];
|
|
530
|
+
// Convert screen frame to CGWindow coordinates
|
|
531
|
+
CGFloat screenTop = screenFrame.origin.y + screenFrame.size.height;
|
|
532
|
+
CGFloat screenBottom = screenFrame.origin.y;
|
|
533
|
+
CGFloat screenLeft = screenFrame.origin.x;
|
|
534
|
+
CGFloat screenRight = screenFrame.origin.x + screenFrame.size.width;
|
|
535
|
+
|
|
536
|
+
if (windowCenterX >= screenLeft && windowCenterX <= screenRight &&
|
|
537
|
+
windowCenterY >= screenBottom && windowCenterY <= screenTop) {
|
|
538
|
+
windowScreen = screen;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Use main screen if no specific screen found
|
|
544
|
+
if (!windowScreen) windowScreen = [NSScreen mainScreen];
|
|
545
|
+
|
|
546
|
+
// Convert coordinates from CGWindow (top-left) to NSWindow (bottom-left) for the specific screen
|
|
547
|
+
NSRect screenFrame = [windowScreen frame];
|
|
548
|
+
CGFloat screenHeight = screenFrame.size.height;
|
|
549
|
+
CGFloat adjustedY = screenHeight - y - height;
|
|
550
|
+
|
|
551
|
+
// Window coordinates are in global space, overlay frame should be screen-relative
|
|
552
|
+
// Keep X coordinate as-is (already in global space which is what we want)
|
|
553
|
+
// Only convert Y from top-left to bottom-left coordinate system
|
|
554
|
+
NSRect overlayFrame = NSMakeRect(x, adjustedY, width, height);
|
|
555
|
+
|
|
556
|
+
NSString *windowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Untitled";
|
|
557
|
+
NSString *appName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
|
|
558
|
+
|
|
559
|
+
NSLog(@"🎯 WINDOW DETECTED: %@ - \"%@\"", appName, windowTitle);
|
|
560
|
+
NSLog(@" 📍 Position: (%d, %d) 📏 Size: %d × %d", x, y, width, height);
|
|
561
|
+
NSLog(@" 🖥️ NSRect: (%.0f, %.0f, %.0f, %.0f) 🔝 Level: %ld",
|
|
562
|
+
overlayFrame.origin.x, overlayFrame.origin.y,
|
|
563
|
+
overlayFrame.size.width, overlayFrame.size.height,
|
|
564
|
+
[g_overlayWindow level]);
|
|
565
|
+
|
|
566
|
+
// Bring window to front if enabled
|
|
567
|
+
if (g_bringToFrontEnabled) {
|
|
568
|
+
int windowId = [[windowUnderCursor objectForKey:@"id"] intValue];
|
|
569
|
+
if (windowId > 0) {
|
|
570
|
+
bool success = bringWindowToFront(windowId);
|
|
571
|
+
if (!success) {
|
|
572
|
+
NSLog(@" ⚠️ Failed to bring window to front");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Ensure overlay is on the correct screen
|
|
578
|
+
[g_overlayWindow setFrame:overlayFrame display:YES];
|
|
579
|
+
|
|
580
|
+
// Update overlay view window info
|
|
581
|
+
[(WindowSelectorOverlayView *)g_overlayView setWindowInfo:windowUnderCursor];
|
|
582
|
+
|
|
583
|
+
// Don't automatically mark as selected - wait for user click
|
|
584
|
+
[(WindowSelectorOverlayView *)g_overlayView setIsSelectedWindow:NO];
|
|
585
|
+
[(WindowSelectorOverlayView *)g_overlayView setIsActiveWindow:NO];
|
|
586
|
+
|
|
587
|
+
// Add/update info label above button
|
|
588
|
+
NSTextField *infoLabel = nil;
|
|
589
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
590
|
+
if ([subview isKindOfClass:[NSTextField class]]) {
|
|
591
|
+
infoLabel = (NSTextField*)subview;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!infoLabel) {
|
|
597
|
+
infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, width - 40, 60)];
|
|
598
|
+
[infoLabel setEditable:NO];
|
|
599
|
+
[infoLabel setSelectable:NO];
|
|
600
|
+
[infoLabel setBezeled:NO];
|
|
601
|
+
[infoLabel setDrawsBackground:NO];
|
|
602
|
+
[infoLabel setAlignment:NSTextAlignmentCenter];
|
|
603
|
+
[infoLabel setFont:[NSFont systemFontOfSize:18 weight:NSFontWeightMedium]];
|
|
604
|
+
[infoLabel setTextColor:[NSColor whiteColor]];
|
|
605
|
+
|
|
606
|
+
// Force no borders on info label
|
|
607
|
+
[infoLabel setWantsLayer:YES];
|
|
608
|
+
infoLabel.layer.borderWidth = 0.0;
|
|
609
|
+
infoLabel.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
610
|
+
infoLabel.layer.cornerRadius = 0.0;
|
|
611
|
+
infoLabel.layer.masksToBounds = YES;
|
|
612
|
+
|
|
613
|
+
[g_overlayWindow.contentView addSubview:infoLabel];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Add/update app icon
|
|
617
|
+
NSImageView *appIconView = nil;
|
|
618
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
619
|
+
if ([subview isKindOfClass:[NSImageView class]]) {
|
|
620
|
+
appIconView = (NSImageView*)subview;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!appIconView) {
|
|
626
|
+
appIconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 96, 96)];
|
|
627
|
+
[appIconView setImageScaling:NSImageScaleProportionallyUpOrDown];
|
|
628
|
+
[appIconView setWantsLayer:YES];
|
|
629
|
+
[appIconView.layer setCornerRadius:8.0];
|
|
630
|
+
[appIconView.layer setMasksToBounds:YES];
|
|
631
|
+
[appIconView.layer setBackgroundColor:[[NSColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.3] CGColor]]; // Debug background
|
|
632
|
+
|
|
633
|
+
// Force no borders on app icon view
|
|
634
|
+
appIconView.layer.borderWidth = 0.0;
|
|
635
|
+
appIconView.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
636
|
+
appIconView.layer.shadowOpacity = 0.0;
|
|
637
|
+
appIconView.layer.shadowRadius = 0.0;
|
|
638
|
+
appIconView.layer.shadowOffset = NSMakeSize(0, 0);
|
|
639
|
+
|
|
640
|
+
[g_overlayWindow.contentView addSubview:appIconView];
|
|
641
|
+
NSLog(@"🖼️ Created app icon view at frame: (%.0f, %.0f, %.0f, %.0f)",
|
|
642
|
+
appIconView.frame.origin.x, appIconView.frame.origin.y,
|
|
643
|
+
appIconView.frame.size.width, appIconView.frame.size.height);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Get app icon using NSWorkspace
|
|
647
|
+
NSString *iconAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
|
|
648
|
+
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
|
|
649
|
+
NSArray *runningApps = [workspace runningApplications];
|
|
650
|
+
NSImage *appIcon = nil;
|
|
651
|
+
|
|
652
|
+
for (NSRunningApplication *app in runningApps) {
|
|
653
|
+
if ([[app localizedName] isEqualToString:iconAppName] || [[app bundleIdentifier] containsString:iconAppName]) {
|
|
654
|
+
appIcon = [app icon];
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Fallback to generic app icon if not found
|
|
660
|
+
if (!appIcon) {
|
|
661
|
+
appIcon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)];
|
|
662
|
+
NSLog(@"⚠️ Using fallback icon for app: %@", iconAppName);
|
|
663
|
+
} else {
|
|
664
|
+
NSLog(@"✅ Found app icon for: %@", iconAppName);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
[appIconView setImage:appIcon];
|
|
668
|
+
NSLog(@"🖼️ Set icon image, size: %.0fx%.0f", [appIcon size].width, [appIcon size].height);
|
|
669
|
+
|
|
670
|
+
// Update label text
|
|
671
|
+
NSString *labelWindowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Unknown Window";
|
|
672
|
+
NSString *labelAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown App";
|
|
673
|
+
[infoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", labelAppName, labelWindowTitle]];
|
|
674
|
+
|
|
675
|
+
// Position buttons - Start Record in center, Cancel below it
|
|
676
|
+
if (g_selectButton) {
|
|
677
|
+
NSSize buttonSize = [g_selectButton frame].size;
|
|
678
|
+
NSPoint buttonCenter = NSMakePoint(
|
|
679
|
+
(width - buttonSize.width) / 2,
|
|
680
|
+
(height - buttonSize.height) / 2 + 15 // Slightly above center
|
|
681
|
+
);
|
|
682
|
+
[g_selectButton setFrameOrigin:buttonCenter];
|
|
683
|
+
|
|
684
|
+
// Position app icon above text label
|
|
685
|
+
NSPoint iconCenter = NSMakePoint(
|
|
686
|
+
(width - 96) / 2, // Center horizontally (icon is 96px wide)
|
|
687
|
+
buttonCenter.y + buttonSize.height + 60 + 10 // Above label + text height + margin
|
|
688
|
+
);
|
|
689
|
+
[appIconView setFrameOrigin:iconCenter];
|
|
690
|
+
NSLog(@"🎯 Positioning app icon at: (%.0f, %.0f) for window size: (%.0f, %.0f)",
|
|
691
|
+
iconCenter.x, iconCenter.y, (float)width, (float)height);
|
|
692
|
+
|
|
693
|
+
// Add fast horizontal floating animation after positioning
|
|
694
|
+
[appIconView.layer removeAnimationForKey:@"floatAnimationX"];
|
|
695
|
+
[appIconView.layer removeAnimationForKey:@"floatAnimationY"];
|
|
696
|
+
|
|
697
|
+
// Faster horizontal float animation only
|
|
698
|
+
CABasicAnimation *floatAnimationX = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
|
|
699
|
+
floatAnimationX.fromValue = @(-4.0);
|
|
700
|
+
floatAnimationX.toValue = @(4.0);
|
|
701
|
+
floatAnimationX.duration = 1.0; // Much faster animation
|
|
702
|
+
floatAnimationX.repeatCount = HUGE_VALF;
|
|
703
|
+
floatAnimationX.autoreverses = YES;
|
|
704
|
+
floatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
|
705
|
+
[appIconView.layer addAnimation:floatAnimationX forKey:@"floatAnimationX"];
|
|
706
|
+
|
|
707
|
+
// Position info label at overlay center, above button
|
|
708
|
+
NSPoint labelCenter = NSMakePoint(
|
|
709
|
+
(width - [infoLabel frame].size.width) / 2, // Center horizontally
|
|
710
|
+
buttonCenter.y + buttonSize.height + 10 // 10px above button, below icon
|
|
711
|
+
);
|
|
712
|
+
[infoLabel setFrameOrigin:labelCenter];
|
|
713
|
+
|
|
714
|
+
// Position cancel button below the main button
|
|
715
|
+
NSButton *cancelButton = nil;
|
|
716
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
717
|
+
if ([subview isKindOfClass:[NSButton class]] &&
|
|
718
|
+
[[(NSButton*)subview title] isEqualToString:@"Cancel"]) {
|
|
719
|
+
cancelButton = (NSButton*)subview;
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (cancelButton) {
|
|
725
|
+
NSSize cancelButtonSize = [cancelButton frame].size;
|
|
726
|
+
NSPoint cancelButtonCenter = NSMakePoint(
|
|
727
|
+
(width - cancelButtonSize.width) / 2,
|
|
728
|
+
buttonCenter.y - buttonSize.height - 20 // 20px below main button
|
|
729
|
+
);
|
|
730
|
+
[cancelButton setFrameOrigin:cancelButtonCenter];
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Show overlay for the current window (will be selected when clicked)
|
|
735
|
+
[g_overlayWindow orderFront:nil];
|
|
736
|
+
[g_overlayWindow makeKeyAndOrderFront:nil];
|
|
737
|
+
|
|
738
|
+
// Ensure all subviews have no borders after positioning, but preserve corner radius for buttons and icons
|
|
739
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
740
|
+
if ([subview respondsToSelector:@selector(setWantsLayer:)]) {
|
|
741
|
+
[subview setWantsLayer:YES];
|
|
742
|
+
if (subview.layer) {
|
|
743
|
+
subview.layer.borderWidth = 0.0;
|
|
744
|
+
subview.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
745
|
+
subview.layer.masksToBounds = YES;
|
|
746
|
+
subview.layer.shadowOpacity = 0.0;
|
|
747
|
+
subview.layer.shadowRadius = 0.0;
|
|
748
|
+
subview.layer.shadowOffset = NSMakeSize(0, 0);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
NSLog(@" ✅ Overlay Status: Level=%ld, Alpha=%.1f, Visible=%s, Frame Set=YES",
|
|
754
|
+
[g_overlayWindow level], [g_overlayWindow alphaValue],
|
|
755
|
+
[g_overlayWindow isVisible] ? "YES" : "NO");
|
|
756
|
+
|
|
757
|
+
// Update overlay view states - check if this window is selected
|
|
758
|
+
if (g_overlayView && [g_overlayView isKindOfClass:[WindowSelectorOverlayView class]]) {
|
|
759
|
+
WindowSelectorOverlayView *overlayView = (WindowSelectorOverlayView *)g_overlayView;
|
|
760
|
+
|
|
761
|
+
// Check if this is the selected window (true if we have selected window info)
|
|
762
|
+
BOOL isSelected = (g_selectedWindowInfo != nil);
|
|
763
|
+
|
|
764
|
+
// Update states
|
|
765
|
+
overlayView.isActiveWindow = NO; // No more mouse-based highlighting
|
|
766
|
+
overlayView.isSelectedWindow = isSelected;
|
|
767
|
+
|
|
768
|
+
NSLog(@"🎯 Overlay State Updated: Active=NO, Selected=%s", isSelected ? "YES" : "NO");
|
|
769
|
+
}
|
|
770
|
+
} else if (!windowUnderCursor) {
|
|
771
|
+
// No selected window, hide overlay
|
|
772
|
+
[g_overlayWindow orderOut:nil];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Cleanup function
|
|
778
|
+
void cleanupWindowSelector() {
|
|
779
|
+
g_isWindowSelecting = false;
|
|
780
|
+
|
|
781
|
+
// Stop tracking timer
|
|
782
|
+
if (g_trackingTimer) {
|
|
783
|
+
[g_trackingTimer invalidate];
|
|
784
|
+
g_trackingTimer = nil;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Remove key event monitor
|
|
788
|
+
if (g_windowKeyEventMonitor) {
|
|
789
|
+
[NSEvent removeMonitor:g_windowKeyEventMonitor];
|
|
790
|
+
g_windowKeyEventMonitor = nil;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Close overlay window
|
|
794
|
+
if (g_overlayWindow) {
|
|
795
|
+
[g_overlayWindow close];
|
|
796
|
+
g_overlayWindow = nil;
|
|
797
|
+
g_overlayView = nil;
|
|
798
|
+
g_selectButton = nil;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Clean up delegate
|
|
802
|
+
if (g_delegate) {
|
|
803
|
+
[g_delegate release];
|
|
804
|
+
g_delegate = nil;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Clean up data
|
|
808
|
+
if (g_allWindows) {
|
|
809
|
+
[g_allWindows release];
|
|
810
|
+
g_allWindows = nil;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Clean up selected overlay
|
|
814
|
+
if (g_selectedOverlayView) {
|
|
815
|
+
g_selectedOverlayView.isSelectedWindow = NO;
|
|
816
|
+
g_selectedOverlayView = nil;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Recording preview functions
|
|
821
|
+
void cleanupRecordingPreview() {
|
|
822
|
+
if (g_recordingPreviewWindow) {
|
|
823
|
+
[g_recordingPreviewWindow close];
|
|
824
|
+
g_recordingPreviewWindow = nil;
|
|
825
|
+
g_recordingPreviewView = nil;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (g_recordingWindowInfo) {
|
|
829
|
+
[g_recordingWindowInfo release];
|
|
830
|
+
g_recordingWindowInfo = nil;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
bool showRecordingPreview(NSDictionary *windowInfo) {
|
|
835
|
+
@try {
|
|
836
|
+
// Clean up any existing preview
|
|
837
|
+
cleanupRecordingPreview();
|
|
838
|
+
|
|
839
|
+
if (!windowInfo) return false;
|
|
840
|
+
|
|
841
|
+
// Store window info
|
|
842
|
+
g_recordingWindowInfo = [windowInfo retain];
|
|
843
|
+
|
|
844
|
+
// Get main screen bounds for full screen overlay
|
|
845
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
846
|
+
NSRect screenFrame = [mainScreen frame];
|
|
847
|
+
|
|
848
|
+
// Create full-screen overlay window
|
|
849
|
+
g_recordingPreviewWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
850
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
851
|
+
backing:NSBackingStoreBuffered
|
|
852
|
+
defer:NO];
|
|
853
|
+
|
|
854
|
+
[g_recordingPreviewWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)]; // High level but below selection
|
|
855
|
+
[g_recordingPreviewWindow setOpaque:NO];
|
|
856
|
+
[g_recordingPreviewWindow setBackgroundColor:[NSColor clearColor]];
|
|
857
|
+
[g_recordingPreviewWindow setIgnoresMouseEvents:YES]; // Don't interfere with user interaction
|
|
858
|
+
[g_recordingPreviewWindow setAcceptsMouseMovedEvents:NO];
|
|
859
|
+
[g_recordingPreviewWindow setHasShadow:NO];
|
|
860
|
+
[g_recordingPreviewWindow setAlphaValue:1.0];
|
|
861
|
+
[g_recordingPreviewWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
862
|
+
|
|
863
|
+
// Remove any default window decorations and borders
|
|
864
|
+
[g_recordingPreviewWindow setTitlebarAppearsTransparent:YES];
|
|
865
|
+
[g_recordingPreviewWindow setTitleVisibility:NSWindowTitleHidden];
|
|
866
|
+
[g_recordingPreviewWindow setMovable:NO];
|
|
867
|
+
[g_recordingPreviewWindow setMovableByWindowBackground:NO];
|
|
868
|
+
|
|
869
|
+
// Create preview view
|
|
870
|
+
g_recordingPreviewView = [[RecordingPreviewView alloc] initWithFrame:screenFrame];
|
|
871
|
+
[(RecordingPreviewView *)g_recordingPreviewView setRecordingWindowInfo:windowInfo];
|
|
872
|
+
[g_recordingPreviewWindow setContentView:g_recordingPreviewView];
|
|
873
|
+
|
|
874
|
+
// Show the preview
|
|
875
|
+
[g_recordingPreviewWindow orderFront:nil];
|
|
876
|
+
[g_recordingPreviewWindow makeKeyAndOrderFront:nil];
|
|
877
|
+
|
|
878
|
+
NSLog(@"🎬 RECORDING PREVIEW: Showing overlay for %@ - \"%@\"",
|
|
879
|
+
[windowInfo objectForKey:@"appName"],
|
|
880
|
+
[windowInfo objectForKey:@"title"]);
|
|
881
|
+
|
|
882
|
+
return true;
|
|
883
|
+
|
|
884
|
+
} @catch (NSException *exception) {
|
|
885
|
+
NSLog(@"❌ Error showing recording preview: %@", exception);
|
|
886
|
+
cleanupRecordingPreview();
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
bool hideRecordingPreview() {
|
|
892
|
+
@try {
|
|
893
|
+
if (g_recordingPreviewWindow) {
|
|
894
|
+
NSLog(@"🎬 RECORDING PREVIEW: Hiding overlay");
|
|
895
|
+
cleanupRecordingPreview();
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
return false;
|
|
899
|
+
|
|
900
|
+
} @catch (NSException *exception) {
|
|
901
|
+
NSLog(@"❌ Error hiding recording preview: %@", exception);
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Update screen overlays based on mouse position
|
|
907
|
+
void updateScreenOverlays() {
|
|
908
|
+
@autoreleasepool {
|
|
909
|
+
if (!g_isScreenSelecting || !g_screenOverlayWindows || !g_allScreens) return;
|
|
910
|
+
|
|
911
|
+
// Get current mouse position
|
|
912
|
+
NSPoint mouseLocation = [NSEvent mouseLocation];
|
|
913
|
+
// Convert from NSEvent coordinates (bottom-left) to screen coordinates
|
|
914
|
+
NSArray *screens = [NSScreen screens];
|
|
915
|
+
NSScreen *mouseScreen = nil;
|
|
916
|
+
NSInteger mouseScreenIndex = -1;
|
|
917
|
+
|
|
918
|
+
// Find which screen contains the mouse
|
|
919
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
920
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
921
|
+
NSRect screenFrame = [screen frame];
|
|
922
|
+
|
|
923
|
+
if (NSPointInRect(mouseLocation, screenFrame)) {
|
|
924
|
+
mouseScreen = screen;
|
|
925
|
+
mouseScreenIndex = i;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// If mouse screen changed, update overlays
|
|
931
|
+
if (mouseScreenIndex != g_currentActiveScreenIndex) {
|
|
932
|
+
g_currentActiveScreenIndex = mouseScreenIndex;
|
|
933
|
+
|
|
934
|
+
// Update all screen overlays
|
|
935
|
+
for (NSInteger i = 0; i < [g_screenOverlayWindows count] && i < [screens count]; i++) {
|
|
936
|
+
NSWindow *overlayWindow = [g_screenOverlayWindows objectAtIndex:i];
|
|
937
|
+
ScreenSelectorOverlayView *overlayView = (ScreenSelectorOverlayView *)[overlayWindow contentView];
|
|
938
|
+
|
|
939
|
+
// Update overlay appearance based on whether it's the active screen
|
|
940
|
+
bool isActiveScreen = (i == mouseScreenIndex);
|
|
941
|
+
|
|
942
|
+
// Update overlay state and appearance
|
|
943
|
+
[overlayView setIsActiveScreen:isActiveScreen];
|
|
944
|
+
[overlayView setNeedsDisplay:YES];
|
|
945
|
+
|
|
946
|
+
// Update UI elements based on active state
|
|
947
|
+
for (NSView *subview in [overlayView subviews]) {
|
|
948
|
+
if ([subview isKindOfClass:[NSButton class]]) {
|
|
949
|
+
NSButton *button = (NSButton *)subview;
|
|
950
|
+
if ([button.title isEqualToString:@"Start Record"]) {
|
|
951
|
+
if (isActiveScreen) {
|
|
952
|
+
// Active screen: bright, prominent button with new RGB color
|
|
953
|
+
[button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:1.0] CGColor]];
|
|
954
|
+
[button setAlphaValue:1.0];
|
|
955
|
+
} else {
|
|
956
|
+
// Inactive screen: dimmer button with new RGB color
|
|
957
|
+
[button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.6] CGColor]];
|
|
958
|
+
[button setAlphaValue:0.7];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if ([subview isKindOfClass:[NSTextField class]]) {
|
|
963
|
+
NSTextField *label = (NSTextField *)subview;
|
|
964
|
+
if (isActiveScreen) {
|
|
965
|
+
[label setTextColor:[NSColor whiteColor]];
|
|
966
|
+
[label setAlphaValue:1.0];
|
|
967
|
+
} else {
|
|
968
|
+
[label setTextColor:[NSColor colorWithWhite:0.8 alpha:0.8]];
|
|
969
|
+
[label setAlphaValue:0.7];
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if ([subview isKindOfClass:[NSImageView class]]) {
|
|
973
|
+
NSImageView *imageView = (NSImageView *)subview;
|
|
974
|
+
if (isActiveScreen) {
|
|
975
|
+
[imageView setAlphaValue:1.0];
|
|
976
|
+
} else {
|
|
977
|
+
[imageView setAlphaValue:0.6];
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Log active screen changes for debugging (optional)
|
|
983
|
+
if (isActiveScreen) {
|
|
984
|
+
NSLog(@"🖥️ Active screen: Display %ld", (long)(i + 1));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Ensure ALL overlays are visible, but active one is on top
|
|
988
|
+
[overlayWindow orderFront:nil];
|
|
989
|
+
if (isActiveScreen) {
|
|
990
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
991
|
+
} else {
|
|
992
|
+
[overlayWindow orderFront:nil]; // Keep inactive screens visible too
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Screen selection functions
|
|
1000
|
+
void cleanupScreenSelector() {
|
|
1001
|
+
g_isScreenSelecting = false;
|
|
1002
|
+
|
|
1003
|
+
// Stop screen tracking timer
|
|
1004
|
+
if (g_screenTrackingTimer) {
|
|
1005
|
+
[g_screenTrackingTimer invalidate];
|
|
1006
|
+
g_screenTrackingTimer = nil;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Remove key event monitor
|
|
1010
|
+
if (g_screenKeyEventMonitor) {
|
|
1011
|
+
[NSEvent removeMonitor:g_screenKeyEventMonitor];
|
|
1012
|
+
g_screenKeyEventMonitor = nil;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Close all screen overlay windows
|
|
1016
|
+
if (g_screenOverlayWindows) {
|
|
1017
|
+
for (NSWindow *overlayWindow in g_screenOverlayWindows) {
|
|
1018
|
+
[overlayWindow close];
|
|
1019
|
+
}
|
|
1020
|
+
[g_screenOverlayWindows release];
|
|
1021
|
+
g_screenOverlayWindows = nil;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Clean up screen data
|
|
1025
|
+
if (g_allScreens) {
|
|
1026
|
+
[g_allScreens release];
|
|
1027
|
+
g_allScreens = nil;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Reset active screen tracking
|
|
1031
|
+
g_currentActiveScreenIndex = -1;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
bool startScreenSelection() {
|
|
1035
|
+
@try {
|
|
1036
|
+
if (g_isScreenSelecting) return false;
|
|
1037
|
+
|
|
1038
|
+
// Get all available screens
|
|
1039
|
+
NSArray *screens = [NSScreen screens];
|
|
1040
|
+
if (!screens || [screens count] == 0) return false;
|
|
1041
|
+
|
|
1042
|
+
// Create screen info array
|
|
1043
|
+
NSMutableArray *screenInfoArray = [[NSMutableArray alloc] init];
|
|
1044
|
+
g_screenOverlayWindows = [[NSMutableArray alloc] init];
|
|
1045
|
+
|
|
1046
|
+
// Get real display IDs like MacRecorder does
|
|
1047
|
+
CGDirectDisplayID activeDisplays[32];
|
|
1048
|
+
uint32_t displayCount;
|
|
1049
|
+
CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
|
|
1050
|
+
|
|
1051
|
+
if (err != kCGErrorSuccess) {
|
|
1052
|
+
NSLog(@"❌ Failed to get active display list: %d", err);
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
1057
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
1058
|
+
NSRect screenFrame = [screen frame];
|
|
1059
|
+
|
|
1060
|
+
// Get the real CGDirectDisplayID for this screen by matching frame
|
|
1061
|
+
CGDirectDisplayID displayID = 0;
|
|
1062
|
+
|
|
1063
|
+
// Find matching display by comparing bounds
|
|
1064
|
+
for (uint32_t j = 0; j < displayCount; j++) {
|
|
1065
|
+
CGDirectDisplayID candidateID = activeDisplays[j];
|
|
1066
|
+
CGRect displayBounds = CGDisplayBounds(candidateID);
|
|
1067
|
+
|
|
1068
|
+
// Compare screen frame with display bounds
|
|
1069
|
+
if (fabs(screenFrame.origin.x - displayBounds.origin.x) < 1.0 &&
|
|
1070
|
+
fabs(screenFrame.origin.y - displayBounds.origin.y) < 1.0 &&
|
|
1071
|
+
fabs(screenFrame.size.width - displayBounds.size.width) < 1.0 &&
|
|
1072
|
+
fabs(screenFrame.size.height - displayBounds.size.height) < 1.0) {
|
|
1073
|
+
displayID = candidateID;
|
|
1074
|
+
// Screen matched to display ID
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Fallback: use array index if no match found
|
|
1080
|
+
if (displayID == 0 && i < displayCount) {
|
|
1081
|
+
displayID = activeDisplays[i];
|
|
1082
|
+
// Used fallback display ID
|
|
1083
|
+
} else if (displayID == 0) {
|
|
1084
|
+
NSLog(@"❌ Screen %ld could not get Display ID", (long)i);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Create screen info dictionary with real display ID
|
|
1088
|
+
NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
|
|
1089
|
+
[screenInfo setObject:[NSNumber numberWithUnsignedInt:displayID] forKey:@"id"]; // Real display ID
|
|
1090
|
+
[screenInfo setObject:[NSString stringWithFormat:@"Display %ld", (long)(i + 1)] forKey:@"name"];
|
|
1091
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.x] forKey:@"x"];
|
|
1092
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.y] forKey:@"y"];
|
|
1093
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.width] forKey:@"width"];
|
|
1094
|
+
[screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.height] forKey:@"height"];
|
|
1095
|
+
[screenInfo setObject:[NSString stringWithFormat:@"%.0fx%.0f", screenFrame.size.width, screenFrame.size.height] forKey:@"resolution"];
|
|
1096
|
+
[screenInfo setObject:[NSNumber numberWithBool:(displayID == CGMainDisplayID())] forKey:@"isPrimary"]; // Real primary check
|
|
1097
|
+
[screenInfoArray addObject:screenInfo];
|
|
1098
|
+
|
|
1099
|
+
// Create overlay window for this screen (FULL screen including menu bar)
|
|
1100
|
+
// For secondary screens, don't specify screen parameter to avoid issues
|
|
1101
|
+
NSWindow *overlayWindow;
|
|
1102
|
+
if (i == 0) {
|
|
1103
|
+
// Primary screen - use screen parameter
|
|
1104
|
+
overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
1105
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
1106
|
+
backing:NSBackingStoreBuffered
|
|
1107
|
+
defer:NO
|
|
1108
|
+
screen:screen];
|
|
1109
|
+
} else {
|
|
1110
|
+
// Secondary screens - create without screen param, set frame manually
|
|
1111
|
+
overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
1112
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
1113
|
+
backing:NSBackingStoreBuffered
|
|
1114
|
+
defer:NO];
|
|
1115
|
+
// Force specific positioning for secondary screen
|
|
1116
|
+
[overlayWindow setFrameOrigin:screenFrame.origin];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Window created for specific screen
|
|
1120
|
+
|
|
1121
|
+
// Use maximum level to match g_overlayWindow
|
|
1122
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
|
|
1123
|
+
[overlayWindow setOpaque:NO];
|
|
1124
|
+
[overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1125
|
+
[overlayWindow setIgnoresMouseEvents:NO];
|
|
1126
|
+
[overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
1127
|
+
[overlayWindow setHasShadow:NO];
|
|
1128
|
+
[overlayWindow setAlphaValue:1.0];
|
|
1129
|
+
// Ensure window appears on all spaces and stays put - match g_overlayWindow
|
|
1130
|
+
[overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
1131
|
+
|
|
1132
|
+
// Remove any default window decorations and borders
|
|
1133
|
+
[overlayWindow setTitlebarAppearsTransparent:YES];
|
|
1134
|
+
[overlayWindow setTitleVisibility:NSWindowTitleHidden];
|
|
1135
|
+
[overlayWindow setMovable:NO];
|
|
1136
|
+
[overlayWindow setMovableByWindowBackground:NO];
|
|
1137
|
+
|
|
1138
|
+
// Force remove all borders and decorations
|
|
1139
|
+
[overlayWindow setHasShadow:NO];
|
|
1140
|
+
[overlayWindow setOpaque:NO];
|
|
1141
|
+
[overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1142
|
+
|
|
1143
|
+
// Create overlay view
|
|
1144
|
+
ScreenSelectorOverlayView *overlayView = [[ScreenSelectorOverlayView alloc] initWithFrame:screenFrame];
|
|
1145
|
+
[overlayView setScreenInfo:screenInfo];
|
|
1146
|
+
[overlayWindow setContentView:overlayView];
|
|
1147
|
+
|
|
1148
|
+
// Note: NSWindow doesn't have setWantsLayer method, only NSView does
|
|
1149
|
+
|
|
1150
|
+
// Create select button with more padding
|
|
1151
|
+
NSButton *selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 60)];
|
|
1152
|
+
[selectButton setTitle:@"Start Record"];
|
|
1153
|
+
[selectButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
1154
|
+
[selectButton setBordered:NO];
|
|
1155
|
+
[selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
|
|
1156
|
+
[selectButton setTag:i]; // Set screen index as tag
|
|
1157
|
+
|
|
1158
|
+
// Modern button styling with new RGB color
|
|
1159
|
+
[selectButton setWantsLayer:YES];
|
|
1160
|
+
[selectButton.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.95] CGColor]];
|
|
1161
|
+
[selectButton.layer setCornerRadius:8.0];
|
|
1162
|
+
[selectButton.layer setBorderWidth:0.0];
|
|
1163
|
+
|
|
1164
|
+
// Remove all button borders and decorations
|
|
1165
|
+
[selectButton.layer setShadowOpacity:0.0];
|
|
1166
|
+
[selectButton.layer setShadowRadius:0.0];
|
|
1167
|
+
[selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
|
|
1168
|
+
[selectButton.layer setMasksToBounds:YES];
|
|
1169
|
+
|
|
1170
|
+
// Clean white text - normal weight
|
|
1171
|
+
[selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
|
|
1172
|
+
[selectButton setTitle:@"Start Record"];
|
|
1173
|
+
NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
|
|
1174
|
+
initWithString:[selectButton title]];
|
|
1175
|
+
[titleString addAttribute:NSForegroundColorAttributeName
|
|
1176
|
+
value:[NSColor whiteColor]
|
|
1177
|
+
range:NSMakeRange(0, [titleString length])];
|
|
1178
|
+
[selectButton setAttributedTitle:titleString];
|
|
1179
|
+
|
|
1180
|
+
// Clean button - no shadows or highlights
|
|
1181
|
+
|
|
1182
|
+
// Set button target and action (reuse global delegate)
|
|
1183
|
+
if (!g_delegate) {
|
|
1184
|
+
g_delegate = [[WindowSelectorDelegate alloc] init];
|
|
1185
|
+
}
|
|
1186
|
+
[selectButton setTarget:g_delegate];
|
|
1187
|
+
[selectButton setAction:@selector(screenSelectButtonClicked:)];
|
|
1188
|
+
|
|
1189
|
+
// Remove focus ring and other default button behaviors
|
|
1190
|
+
[selectButton setFocusRingType:NSFocusRingTypeNone];
|
|
1191
|
+
[selectButton setShowsBorderOnlyWhileMouseInside:NO];
|
|
1192
|
+
|
|
1193
|
+
// Create cancel button for screen selection
|
|
1194
|
+
NSButton *screenCancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
|
|
1195
|
+
[screenCancelButton setTitle:@"Cancel"];
|
|
1196
|
+
[screenCancelButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
1197
|
+
[screenCancelButton setBordered:NO];
|
|
1198
|
+
[screenCancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightMedium]];
|
|
1199
|
+
|
|
1200
|
+
// Modern cancel button styling - darker gray, clean
|
|
1201
|
+
[screenCancelButton setWantsLayer:YES];
|
|
1202
|
+
[screenCancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.35 green:0.35 blue:0.4 alpha:0.9] CGColor]];
|
|
1203
|
+
[screenCancelButton.layer setCornerRadius:8.0];
|
|
1204
|
+
[screenCancelButton.layer setBorderWidth:0.0];
|
|
1205
|
+
|
|
1206
|
+
// Remove all button borders and decorations
|
|
1207
|
+
[screenCancelButton.layer setShadowOpacity:0.0];
|
|
1208
|
+
[screenCancelButton.layer setShadowRadius:0.0];
|
|
1209
|
+
[screenCancelButton.layer setShadowOffset:NSMakeSize(0, 0)];
|
|
1210
|
+
[screenCancelButton.layer setMasksToBounds:YES];
|
|
1211
|
+
|
|
1212
|
+
// Clean white text for cancel button
|
|
1213
|
+
[screenCancelButton setFont:[NSFont systemFontOfSize:15 weight:NSFontWeightRegular]];
|
|
1214
|
+
NSMutableAttributedString *screenCancelTitleString = [[NSMutableAttributedString alloc]
|
|
1215
|
+
initWithString:[screenCancelButton title]];
|
|
1216
|
+
[screenCancelTitleString addAttribute:NSForegroundColorAttributeName
|
|
1217
|
+
value:[NSColor whiteColor]
|
|
1218
|
+
range:NSMakeRange(0, [screenCancelTitleString length])];
|
|
1219
|
+
[screenCancelButton setAttributedTitle:screenCancelTitleString];
|
|
1220
|
+
|
|
1221
|
+
[screenCancelButton setTarget:g_delegate];
|
|
1222
|
+
[screenCancelButton setAction:@selector(cancelButtonClicked:)];
|
|
1223
|
+
|
|
1224
|
+
// Remove focus ring and other default button behaviors
|
|
1225
|
+
[screenCancelButton setFocusRingType:NSFocusRingTypeNone];
|
|
1226
|
+
[screenCancelButton setShowsBorderOnlyWhileMouseInside:NO];
|
|
1227
|
+
|
|
1228
|
+
// Create info label for screen
|
|
1229
|
+
NSTextField *screenInfoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, screenFrame.size.width - 40, 60)];
|
|
1230
|
+
[screenInfoLabel setEditable:NO];
|
|
1231
|
+
[screenInfoLabel setSelectable:NO];
|
|
1232
|
+
[screenInfoLabel setBezeled:NO];
|
|
1233
|
+
[screenInfoLabel setDrawsBackground:NO];
|
|
1234
|
+
[screenInfoLabel setAlignment:NSTextAlignmentCenter];
|
|
1235
|
+
[screenInfoLabel setFont:[NSFont systemFontOfSize:20 weight:NSFontWeightMedium]];
|
|
1236
|
+
[screenInfoLabel setTextColor:[NSColor whiteColor]];
|
|
1237
|
+
|
|
1238
|
+
// Create screen icon (display icon)
|
|
1239
|
+
NSImageView *screenIconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 96, 96)];
|
|
1240
|
+
[screenIconView setImageScaling:NSImageScaleProportionallyUpOrDown];
|
|
1241
|
+
[screenIconView setWantsLayer:YES];
|
|
1242
|
+
[screenIconView.layer setCornerRadius:8.0];
|
|
1243
|
+
[screenIconView.layer setMasksToBounds:YES];
|
|
1244
|
+
|
|
1245
|
+
// Set display icon
|
|
1246
|
+
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
|
|
1247
|
+
NSImage *displayIcon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kComputerIcon)];
|
|
1248
|
+
[screenIconView setImage:displayIcon];
|
|
1249
|
+
|
|
1250
|
+
// Set screen info text
|
|
1251
|
+
NSString *screenName = [screenInfo objectForKey:@"name"] ?: @"Unknown Screen";
|
|
1252
|
+
NSString *resolution = [screenInfo objectForKey:@"resolution"] ?: @"Unknown Resolution";
|
|
1253
|
+
[screenInfoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", screenName, resolution]];
|
|
1254
|
+
|
|
1255
|
+
// Position buttons - Start Record in center, Cancel below it
|
|
1256
|
+
NSPoint buttonCenter = NSMakePoint(
|
|
1257
|
+
(screenFrame.size.width - [selectButton frame].size.width) / 2,
|
|
1258
|
+
(screenFrame.size.height - [selectButton frame].size.height) / 2 + 15 // Slightly above center
|
|
1259
|
+
);
|
|
1260
|
+
[selectButton setFrameOrigin:buttonCenter];
|
|
1261
|
+
|
|
1262
|
+
// Position screen icon above text label
|
|
1263
|
+
NSPoint iconCenter = NSMakePoint(
|
|
1264
|
+
(screenFrame.size.width - 96) / 2, // Center horizontally (icon is 96px wide)
|
|
1265
|
+
buttonCenter.y + [selectButton frame].size.height + 60 + 10 // Above label + text height + margin
|
|
1266
|
+
);
|
|
1267
|
+
[screenIconView setFrameOrigin:iconCenter];
|
|
1268
|
+
|
|
1269
|
+
// Add fast horizontal floating animation to screen icon
|
|
1270
|
+
CABasicAnimation *screenFloatAnimationX = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
|
|
1271
|
+
screenFloatAnimationX.fromValue = @(-4.0);
|
|
1272
|
+
screenFloatAnimationX.toValue = @(4.0);
|
|
1273
|
+
screenFloatAnimationX.duration = 1.2; // Much faster animation
|
|
1274
|
+
screenFloatAnimationX.repeatCount = HUGE_VALF;
|
|
1275
|
+
screenFloatAnimationX.autoreverses = YES;
|
|
1276
|
+
screenFloatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
|
1277
|
+
[screenIconView.layer addAnimation:screenFloatAnimationX forKey:@"floatAnimationX"];
|
|
1278
|
+
|
|
1279
|
+
// Position info label at screen center, above button
|
|
1280
|
+
NSPoint labelCenter = NSMakePoint(
|
|
1281
|
+
(screenFrame.size.width - [screenInfoLabel frame].size.width) / 2, // Center horizontally
|
|
1282
|
+
buttonCenter.y + [selectButton frame].size.height + 10 // 10px above button, below icon
|
|
1283
|
+
);
|
|
1284
|
+
[screenInfoLabel setFrameOrigin:labelCenter];
|
|
1285
|
+
|
|
1286
|
+
NSPoint cancelButtonCenter = NSMakePoint(
|
|
1287
|
+
(screenFrame.size.width - [screenCancelButton frame].size.width) / 2,
|
|
1288
|
+
buttonCenter.y - [selectButton frame].size.height - 20 // 20px below main button
|
|
1289
|
+
);
|
|
1290
|
+
[screenCancelButton setFrameOrigin:cancelButtonCenter];
|
|
1291
|
+
|
|
1292
|
+
[overlayView addSubview:screenIconView];
|
|
1293
|
+
[overlayView addSubview:screenInfoLabel];
|
|
1294
|
+
[overlayView addSubview:selectButton];
|
|
1295
|
+
[overlayView addSubview:screenCancelButton];
|
|
1296
|
+
|
|
1297
|
+
// Ensure window frame is correct for this screen
|
|
1298
|
+
[overlayWindow setFrame:screenFrame display:YES animate:NO];
|
|
1299
|
+
|
|
1300
|
+
// Show overlay - different strategy for secondary screens
|
|
1301
|
+
if (i == 0) {
|
|
1302
|
+
// Primary screen
|
|
1303
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
1304
|
+
// Primary screen overlay shown
|
|
1305
|
+
} else {
|
|
1306
|
+
// Secondary screens - more aggressive approach
|
|
1307
|
+
[overlayWindow orderFront:nil];
|
|
1308
|
+
[overlayWindow makeKeyAndOrderFront:nil]; // Try makeKey too
|
|
1309
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Match g_overlayWindow level
|
|
1310
|
+
|
|
1311
|
+
// Secondary screen overlay shown
|
|
1312
|
+
|
|
1313
|
+
// Double-check with delayed re-show
|
|
1314
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
1315
|
+
[overlayWindow orderFront:nil];
|
|
1316
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Additional visibility settings
|
|
1321
|
+
[overlayWindow setAlphaValue:1.0];
|
|
1322
|
+
[overlayWindow setIsVisible:YES];
|
|
1323
|
+
|
|
1324
|
+
// Overlay window is now ready and visible
|
|
1325
|
+
|
|
1326
|
+
[g_screenOverlayWindows addObject:overlayWindow];
|
|
1327
|
+
[screenInfo release];
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
g_allScreens = [screenInfoArray retain];
|
|
1331
|
+
[screenInfoArray release];
|
|
1332
|
+
g_isScreenSelecting = true;
|
|
1333
|
+
g_currentActiveScreenIndex = -1;
|
|
1334
|
+
|
|
1335
|
+
// Add ESC key event monitor to cancel selection
|
|
1336
|
+
g_screenKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
|
|
1337
|
+
handler:^(NSEvent *event) {
|
|
1338
|
+
if ([event keyCode] == 53) { // ESC key
|
|
1339
|
+
NSLog(@"🖥️ SCREEN SELECTION: ESC pressed - cancelling selection");
|
|
1340
|
+
cleanupScreenSelector();
|
|
1341
|
+
}
|
|
1342
|
+
}];
|
|
1343
|
+
|
|
1344
|
+
// Start screen tracking timer to update overlays based on mouse position
|
|
1345
|
+
if (!g_delegate) {
|
|
1346
|
+
g_delegate = [[WindowSelectorDelegate alloc] init];
|
|
1347
|
+
}
|
|
1348
|
+
g_screenTrackingTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 // 20 FPS
|
|
1349
|
+
target:g_delegate
|
|
1350
|
+
selector:@selector(screenTimerUpdate:)
|
|
1351
|
+
userInfo:nil
|
|
1352
|
+
repeats:YES];
|
|
1353
|
+
|
|
1354
|
+
// Initial update to set correct highlighting
|
|
1355
|
+
updateScreenOverlays();
|
|
1356
|
+
|
|
1357
|
+
NSLog(@"🖥️ SCREEN SELECTION: Started with %lu screens (ESC to cancel)", (unsigned long)[screens count]);
|
|
1358
|
+
NSLog(@"🖥️ SCREEN TRACKING: Timer started for overlay updates");
|
|
1359
|
+
|
|
1360
|
+
return true;
|
|
1361
|
+
|
|
1362
|
+
} @catch (NSException *exception) {
|
|
1363
|
+
NSLog(@"❌ Error starting screen selection: %@", exception);
|
|
1364
|
+
cleanupScreenSelector();
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
bool stopScreenSelection() {
|
|
1370
|
+
@try {
|
|
1371
|
+
if (!g_isScreenSelecting) return false;
|
|
1372
|
+
|
|
1373
|
+
cleanupScreenSelector();
|
|
1374
|
+
NSLog(@"🖥️ SCREEN SELECTION: Stopped");
|
|
1375
|
+
return true;
|
|
1376
|
+
|
|
1377
|
+
} @catch (NSException *exception) {
|
|
1378
|
+
NSLog(@"❌ Error stopping screen selection: %@", exception);
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
NSDictionary* getSelectedScreenInfo() {
|
|
1384
|
+
if (!g_selectedScreenInfo) return nil;
|
|
1385
|
+
|
|
1386
|
+
NSDictionary *result = [g_selectedScreenInfo retain];
|
|
1387
|
+
[g_selectedScreenInfo release];
|
|
1388
|
+
g_selectedScreenInfo = nil;
|
|
1389
|
+
|
|
1390
|
+
return [result autorelease];
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
bool showScreenRecordingPreview(NSDictionary *screenInfo) {
|
|
1394
|
+
@try {
|
|
1395
|
+
// Clean up any existing preview
|
|
1396
|
+
cleanupRecordingPreview();
|
|
1397
|
+
|
|
1398
|
+
if (!screenInfo) return false;
|
|
1399
|
+
|
|
1400
|
+
// For screen recording preview, we show all OTHER screens as black overlay
|
|
1401
|
+
// and keep the selected screen transparent
|
|
1402
|
+
NSArray *screens = [NSScreen screens];
|
|
1403
|
+
if (!screens || [screens count] == 0) return false;
|
|
1404
|
+
|
|
1405
|
+
int selectedScreenId = [[screenInfo objectForKey:@"id"] intValue];
|
|
1406
|
+
|
|
1407
|
+
// Create overlay for each screen except the selected one
|
|
1408
|
+
for (NSInteger i = 0; i < [screens count]; i++) {
|
|
1409
|
+
if (i == selectedScreenId) continue; // Skip selected screen
|
|
1410
|
+
|
|
1411
|
+
NSScreen *screen = [screens objectAtIndex:i];
|
|
1412
|
+
NSRect screenFrame = [screen frame];
|
|
1413
|
+
|
|
1414
|
+
// Create full-screen black overlay for non-selected screens
|
|
1415
|
+
NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
|
|
1416
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
1417
|
+
backing:NSBackingStoreBuffered
|
|
1418
|
+
defer:NO
|
|
1419
|
+
screen:screen];
|
|
1420
|
+
|
|
1421
|
+
[overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
|
|
1422
|
+
[overlayWindow setOpaque:NO];
|
|
1423
|
+
[overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1424
|
+
[overlayWindow setIgnoresMouseEvents:NO];
|
|
1425
|
+
[overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
1426
|
+
[overlayWindow setHasShadow:NO];
|
|
1427
|
+
// no border
|
|
1428
|
+
[overlayWindow setStyleMask:NSWindowStyleMaskBorderless];
|
|
1429
|
+
[overlayWindow setAlphaValue:1.0];
|
|
1430
|
+
[overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
// Force content view to have no borders
|
|
1434
|
+
overlayWindow.contentView.wantsLayer = YES;
|
|
1435
|
+
overlayWindow.contentView.layer.borderWidth = 0.0;
|
|
1436
|
+
overlayWindow.contentView.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
1437
|
+
overlayWindow.contentView.layer.cornerRadius = 0.0;
|
|
1438
|
+
overlayWindow.contentView.layer.masksToBounds = YES;
|
|
1439
|
+
|
|
1440
|
+
// Remove any default window decorations and borders
|
|
1441
|
+
[overlayWindow setTitlebarAppearsTransparent:YES];
|
|
1442
|
+
[overlayWindow setTitleVisibility:NSWindowTitleHidden];
|
|
1443
|
+
[overlayWindow setMovable:NO];
|
|
1444
|
+
[overlayWindow setMovableByWindowBackground:NO];
|
|
1445
|
+
|
|
1446
|
+
// Force remove all borders and decorations
|
|
1447
|
+
[overlayWindow setHasShadow:NO];
|
|
1448
|
+
[overlayWindow setOpaque:NO];
|
|
1449
|
+
[overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
[overlayWindow orderFront:nil];
|
|
1453
|
+
[overlayWindow makeKeyAndOrderFront:nil];
|
|
1454
|
+
|
|
1455
|
+
// Note: NSWindow doesn't have setWantsLayer method, only NSView does
|
|
1456
|
+
|
|
1457
|
+
// Store for cleanup (reuse recording preview window variable)
|
|
1458
|
+
if (!g_recordingPreviewWindow) {
|
|
1459
|
+
g_recordingPreviewWindow = overlayWindow;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
NSLog(@"🎬 SCREEN RECORDING PREVIEW: Showing overlay for Screen %d", selectedScreenId);
|
|
1464
|
+
|
|
1465
|
+
return true;
|
|
1466
|
+
|
|
1467
|
+
} @catch (NSException *exception) {
|
|
1468
|
+
NSLog(@"❌ Error showing screen recording preview: %@", exception);
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
bool hideScreenRecordingPreview() {
|
|
1474
|
+
return hideRecordingPreview(); // Reuse existing function
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// NAPI Function: Start Window Selection
|
|
1478
|
+
Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
|
|
1479
|
+
Napi::Env env = info.Env();
|
|
1480
|
+
|
|
1481
|
+
if (g_isWindowSelecting) {
|
|
1482
|
+
Napi::TypeError::New(env, "Window selection already in progress").ThrowAsJavaScriptException();
|
|
1483
|
+
return env.Null();
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
@try {
|
|
1487
|
+
// Get all windows
|
|
1488
|
+
g_allWindows = [getAllSelectableWindows() retain];
|
|
1489
|
+
|
|
1490
|
+
if (!g_allWindows || [g_allWindows count] == 0) {
|
|
1491
|
+
Napi::Error::New(env, "No selectable windows found").ThrowAsJavaScriptException();
|
|
1492
|
+
return env.Null();
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Clear any previous selection
|
|
1496
|
+
if (g_selectedWindowInfo) {
|
|
1497
|
+
[g_selectedWindowInfo release];
|
|
1498
|
+
g_selectedWindowInfo = nil;
|
|
1499
|
+
}
|
|
1500
|
+
if (g_selectedOverlayView) {
|
|
1501
|
+
g_selectedOverlayView.isSelectedWindow = NO;
|
|
1502
|
+
g_selectedOverlayView = nil;
|
|
1503
|
+
}
|
|
1504
|
+
if (g_currentWindowUnderCursor) {
|
|
1505
|
+
[g_currentWindowUnderCursor release];
|
|
1506
|
+
g_currentWindowUnderCursor = nil;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Create overlay window (initially hidden)
|
|
1510
|
+
NSRect initialFrame = NSMakeRect(0, 0, 100, 100);
|
|
1511
|
+
g_overlayWindow = [[NSWindow alloc] initWithContentRect:initialFrame
|
|
1512
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
1513
|
+
backing:NSBackingStoreBuffered
|
|
1514
|
+
defer:NO];
|
|
1515
|
+
|
|
1516
|
+
// Force completely borderless appearance
|
|
1517
|
+
[g_overlayWindow setStyleMask:NSWindowStyleMaskBorderless];
|
|
1518
|
+
|
|
1519
|
+
[g_overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)]; // Absolute highest level
|
|
1520
|
+
[g_overlayWindow setOpaque:NO];
|
|
1521
|
+
[g_overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1522
|
+
[g_overlayWindow setIgnoresMouseEvents:NO];
|
|
1523
|
+
[g_overlayWindow setAcceptsMouseMovedEvents:YES];
|
|
1524
|
+
[g_overlayWindow setHasShadow:NO];
|
|
1525
|
+
[g_overlayWindow setAlphaValue:1.0];
|
|
1526
|
+
[g_overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
|
|
1527
|
+
|
|
1528
|
+
// Remove any default window decorations and borders
|
|
1529
|
+
[g_overlayWindow setTitlebarAppearsTransparent:YES];
|
|
1530
|
+
[g_overlayWindow setTitleVisibility:NSWindowTitleHidden];
|
|
1531
|
+
[g_overlayWindow setMovable:NO];
|
|
1532
|
+
[g_overlayWindow setMovableByWindowBackground:NO];
|
|
1533
|
+
|
|
1534
|
+
// Force remove all borders and decorations
|
|
1535
|
+
[g_overlayWindow setHasShadow:NO];
|
|
1536
|
+
[g_overlayWindow setOpaque:NO];
|
|
1537
|
+
[g_overlayWindow setBackgroundColor:[NSColor clearColor]];
|
|
1538
|
+
|
|
1539
|
+
// Create overlay view
|
|
1540
|
+
g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:initialFrame];
|
|
1541
|
+
[g_overlayWindow setContentView:g_overlayView];
|
|
1542
|
+
|
|
1543
|
+
// Initialize overlay view properties
|
|
1544
|
+
[(WindowSelectorOverlayView *)g_overlayView setIsActiveWindow:NO];
|
|
1545
|
+
[(WindowSelectorOverlayView *)g_overlayView setIsSelectedWindow:NO];
|
|
1546
|
+
|
|
1547
|
+
// Note: NSWindow doesn't have setWantsLayer method, only NSView does
|
|
1548
|
+
|
|
1549
|
+
// Force content view to have no borders
|
|
1550
|
+
g_overlayWindow.contentView.wantsLayer = YES;
|
|
1551
|
+
g_overlayWindow.contentView.layer.borderWidth = 0.0;
|
|
1552
|
+
g_overlayWindow.contentView.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
1553
|
+
g_overlayWindow.contentView.layer.cornerRadius = 0.0;
|
|
1554
|
+
g_overlayWindow.contentView.layer.masksToBounds = YES;
|
|
1555
|
+
|
|
1556
|
+
// Additional window styling to ensure no borders or decorations
|
|
1557
|
+
[g_overlayWindow setMovable:NO];
|
|
1558
|
+
[g_overlayWindow setMovableByWindowBackground:NO];
|
|
1559
|
+
|
|
1560
|
+
// Create select button with purple theme
|
|
1561
|
+
g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 60)];
|
|
1562
|
+
[g_selectButton setTitle:@"Start Record"];
|
|
1563
|
+
[g_selectButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
1564
|
+
[g_selectButton setBordered:NO];
|
|
1565
|
+
[g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
|
|
1566
|
+
|
|
1567
|
+
// Modern button styling with new RGB color
|
|
1568
|
+
[g_selectButton setWantsLayer:YES];
|
|
1569
|
+
[g_selectButton.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.95] CGColor]];
|
|
1570
|
+
[g_selectButton.layer setCornerRadius:8.0];
|
|
1571
|
+
[g_selectButton.layer setBorderWidth:0.0];
|
|
1572
|
+
|
|
1573
|
+
// Remove all button borders and decorations
|
|
1574
|
+
[g_selectButton.layer setShadowOpacity:0.0];
|
|
1575
|
+
[g_selectButton.layer setShadowRadius:0.0];
|
|
1576
|
+
[g_selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
|
|
1577
|
+
[g_selectButton.layer setMasksToBounds:YES];
|
|
1578
|
+
[g_selectButton.layer setBorderWidth:0.0];
|
|
1579
|
+
[g_selectButton.layer setBorderColor:[[NSColor clearColor] CGColor]];
|
|
1580
|
+
|
|
1581
|
+
// Clean white text - normal weight
|
|
1582
|
+
NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
|
|
1583
|
+
initWithString:[g_selectButton title]];
|
|
1584
|
+
[titleString addAttribute:NSForegroundColorAttributeName
|
|
1585
|
+
value:[NSColor whiteColor]
|
|
1586
|
+
range:NSMakeRange(0, [titleString length])];
|
|
1587
|
+
[g_selectButton setAttributedTitle:titleString];
|
|
1588
|
+
|
|
1589
|
+
// Create delegate for button action and timer
|
|
1590
|
+
g_delegate = [[WindowSelectorDelegate alloc] init];
|
|
1591
|
+
[g_selectButton setTarget:g_delegate];
|
|
1592
|
+
[g_selectButton setAction:@selector(selectButtonClicked:)];
|
|
1593
|
+
|
|
1594
|
+
// Remove focus ring and other default button behaviors
|
|
1595
|
+
[g_selectButton setFocusRingType:NSFocusRingTypeNone];
|
|
1596
|
+
[g_selectButton setShowsBorderOnlyWhileMouseInside:NO];
|
|
1597
|
+
|
|
1598
|
+
// Add select button directly to window (not view) for proper layering
|
|
1599
|
+
[g_overlayWindow.contentView addSubview:g_selectButton];
|
|
1600
|
+
|
|
1601
|
+
// Create cancel button
|
|
1602
|
+
NSButton *cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 120, 40)];
|
|
1603
|
+
[cancelButton setTitle:@"Cancel"];
|
|
1604
|
+
[cancelButton setButtonType:NSButtonTypeMomentaryPushIn];
|
|
1605
|
+
[cancelButton setBordered:NO];
|
|
1606
|
+
[cancelButton setFont:[NSFont systemFontOfSize:14 weight:NSFontWeightRegular]];
|
|
1607
|
+
|
|
1608
|
+
// Modern cancel button styling - darker gray, clean
|
|
1609
|
+
[cancelButton setWantsLayer:YES];
|
|
1610
|
+
[cancelButton.layer setBackgroundColor:[[NSColor colorWithRed:0.35 green:0.35 blue:0.4 alpha:0.9] CGColor]];
|
|
1611
|
+
[cancelButton.layer setCornerRadius:8.0];
|
|
1612
|
+
[cancelButton.layer setBorderWidth:0.0];
|
|
1613
|
+
|
|
1614
|
+
// Remove all button borders and decorations
|
|
1615
|
+
[cancelButton.layer setShadowOpacity:0.0];
|
|
1616
|
+
[cancelButton.layer setShadowRadius:0.0];
|
|
1617
|
+
[cancelButton.layer setShadowOffset:NSMakeSize(0, 0)];
|
|
1618
|
+
[cancelButton.layer setMasksToBounds:YES];
|
|
1619
|
+
[cancelButton.layer setBorderWidth:0.0];
|
|
1620
|
+
[cancelButton.layer setBorderColor:[[NSColor clearColor] CGColor]];
|
|
1621
|
+
|
|
1622
|
+
// Clean white text for cancel button
|
|
1623
|
+
NSMutableAttributedString *cancelTitleString = [[NSMutableAttributedString alloc]
|
|
1624
|
+
initWithString:[cancelButton title]];
|
|
1625
|
+
[cancelTitleString addAttribute:NSForegroundColorAttributeName
|
|
1626
|
+
value:[NSColor whiteColor]
|
|
1627
|
+
range:NSMakeRange(0, [cancelTitleString length])];
|
|
1628
|
+
[cancelButton setAttributedTitle:cancelTitleString];
|
|
1629
|
+
|
|
1630
|
+
[cancelButton setTarget:g_delegate];
|
|
1631
|
+
[cancelButton setAction:@selector(cancelButtonClicked:)];
|
|
1632
|
+
|
|
1633
|
+
// Remove focus ring and other default button behaviors
|
|
1634
|
+
[cancelButton setFocusRingType:NSFocusRingTypeNone];
|
|
1635
|
+
[cancelButton setShowsBorderOnlyWhileMouseInside:NO];
|
|
1636
|
+
|
|
1637
|
+
// Add cancel button to window
|
|
1638
|
+
[g_overlayWindow.contentView addSubview:cancelButton];
|
|
1639
|
+
|
|
1640
|
+
// Force all subviews to have no borders, but preserve corner radius for buttons and icons
|
|
1641
|
+
for (NSView *subview in [g_overlayWindow.contentView subviews]) {
|
|
1642
|
+
if ([subview respondsToSelector:@selector(setWantsLayer:)]) {
|
|
1643
|
+
[subview setWantsLayer:YES];
|
|
1644
|
+
if (subview.layer) {
|
|
1645
|
+
subview.layer.borderWidth = 0.0;
|
|
1646
|
+
subview.layer.borderColor = [[NSColor clearColor] CGColor];
|
|
1647
|
+
subview.layer.masksToBounds = YES;
|
|
1648
|
+
subview.layer.shadowOpacity = 0.0;
|
|
1649
|
+
subview.layer.shadowRadius = 0.0;
|
|
1650
|
+
subview.layer.shadowOffset = NSMakeSize(0, 0);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Cancel button reference will be found dynamically in positioning code
|
|
1656
|
+
|
|
1657
|
+
// Timer approach doesn't work well with Node.js
|
|
1658
|
+
// Instead, we'll use JavaScript polling via getWindowSelectionStatus
|
|
1659
|
+
// The JS side will call this function repeatedly to trigger overlay updates
|
|
1660
|
+
g_trackingTimer = nil; // No timer for now
|
|
1661
|
+
|
|
1662
|
+
// Add ESC key event monitor to cancel selection
|
|
1663
|
+
g_windowKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
|
|
1664
|
+
handler:^(NSEvent *event) {
|
|
1665
|
+
if ([event keyCode] == 53) { // ESC key
|
|
1666
|
+
NSLog(@"🪟 WINDOW SELECTION: ESC pressed - cancelling selection");
|
|
1667
|
+
cleanupWindowSelector();
|
|
1668
|
+
}
|
|
1669
|
+
}];
|
|
1670
|
+
|
|
1671
|
+
g_isWindowSelecting = true;
|
|
1672
|
+
g_selectedWindowInfo = nil;
|
|
1673
|
+
|
|
1674
|
+
return Napi::Boolean::New(env, true);
|
|
1675
|
+
|
|
1676
|
+
} @catch (NSException *exception) {
|
|
1677
|
+
cleanupWindowSelector();
|
|
1678
|
+
Napi::Error::New(env, [[exception reason] UTF8String]).ThrowAsJavaScriptException();
|
|
1679
|
+
return env.Null();
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// NAPI Function: Stop Window Selection
|
|
1684
|
+
Napi::Value StopWindowSelection(const Napi::CallbackInfo& info) {
|
|
1685
|
+
Napi::Env env = info.Env();
|
|
1686
|
+
|
|
1687
|
+
if (!g_isWindowSelecting) {
|
|
1688
|
+
return Napi::Boolean::New(env, false);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
cleanupWindowSelector();
|
|
1692
|
+
return Napi::Boolean::New(env, true);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// NAPI Function: Get Selected Window Info
|
|
1696
|
+
Napi::Value GetSelectedWindowInfo(const Napi::CallbackInfo& info) {
|
|
1697
|
+
Napi::Env env = info.Env();
|
|
1698
|
+
|
|
1699
|
+
if (!g_selectedWindowInfo) {
|
|
1700
|
+
return env.Null();
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
@try {
|
|
1704
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1705
|
+
result.Set("id", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"id"] intValue]));
|
|
1706
|
+
result.Set("title", Napi::String::New(env, [[g_selectedWindowInfo objectForKey:@"title"] UTF8String]));
|
|
1707
|
+
result.Set("appName", Napi::String::New(env, [[g_selectedWindowInfo objectForKey:@"appName"] UTF8String]));
|
|
1708
|
+
// Original CGWindow coordinates
|
|
1709
|
+
result.Set("x", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"x"] intValue]));
|
|
1710
|
+
result.Set("y", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"y"] intValue]));
|
|
1711
|
+
result.Set("width", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"width"] intValue]));
|
|
1712
|
+
result.Set("height", Napi::Number::New(env, [[g_selectedWindowInfo objectForKey:@"height"] intValue]));
|
|
1713
|
+
|
|
1714
|
+
// Add overlay coordinates for direct use in recording
|
|
1715
|
+
// These are the exact coordinates used by the recording preview overlay
|
|
1716
|
+
int windowX = [[g_selectedWindowInfo objectForKey:@"x"] intValue];
|
|
1717
|
+
int windowY = [[g_selectedWindowInfo objectForKey:@"y"] intValue];
|
|
1718
|
+
int windowWidth = [[g_selectedWindowInfo objectForKey:@"width"] intValue];
|
|
1719
|
+
int windowHeight = [[g_selectedWindowInfo objectForKey:@"height"] intValue];
|
|
1720
|
+
|
|
1721
|
+
result.Set("overlayX", Napi::Number::New(env, windowX));
|
|
1722
|
+
result.Set("overlayY", Napi::Number::New(env, windowY));
|
|
1723
|
+
result.Set("overlayWidth", Napi::Number::New(env, windowWidth));
|
|
1724
|
+
result.Set("overlayHeight", Napi::Number::New(env, windowHeight));
|
|
1725
|
+
|
|
1726
|
+
// Determine which screen this window is on
|
|
1727
|
+
int x = [[g_selectedWindowInfo objectForKey:@"x"] intValue];
|
|
1728
|
+
int y = [[g_selectedWindowInfo objectForKey:@"y"] intValue];
|
|
1729
|
+
int width = [[g_selectedWindowInfo objectForKey:@"width"] intValue];
|
|
1730
|
+
int height = [[g_selectedWindowInfo objectForKey:@"height"] intValue];
|
|
1731
|
+
|
|
1732
|
+
NSLog(@"🎯 WINDOW SELECTED: %@ - \"%@\"",
|
|
1733
|
+
[g_selectedWindowInfo objectForKey:@"appName"],
|
|
1734
|
+
[g_selectedWindowInfo objectForKey:@"title"]);
|
|
1735
|
+
NSLog(@" 📊 Details: ID=%@, Pos=(%d,%d), Size=%dx%d",
|
|
1736
|
+
[g_selectedWindowInfo objectForKey:@"id"], x, y, width, height);
|
|
1737
|
+
|
|
1738
|
+
// Get all screens
|
|
1739
|
+
NSArray *screens = [NSScreen screens];
|
|
1740
|
+
NSScreen *windowScreen = nil;
|
|
1741
|
+
NSScreen *mainScreen = [NSScreen mainScreen];
|
|
1742
|
+
|
|
1743
|
+
for (NSScreen *screen in screens) {
|
|
1744
|
+
NSRect screenFrame = [screen frame];
|
|
1745
|
+
|
|
1746
|
+
// Convert window coordinates to screen-relative
|
|
1747
|
+
if (x >= screenFrame.origin.x &&
|
|
1748
|
+
x < screenFrame.origin.x + screenFrame.size.width &&
|
|
1749
|
+
y >= screenFrame.origin.y &&
|
|
1750
|
+
y < screenFrame.origin.y + screenFrame.size.height) {
|
|
1751
|
+
windowScreen = screen;
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (!windowScreen) {
|
|
1757
|
+
windowScreen = mainScreen;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Add screen information
|
|
1761
|
+
NSRect screenFrame = [windowScreen frame];
|
|
1762
|
+
result.Set("screenId", Napi::Number::New(env, [[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] ?
|
|
1763
|
+
[[[windowScreen deviceDescription] objectForKey:@"NSScreenNumber"] intValue] : 0));
|
|
1764
|
+
result.Set("screenX", Napi::Number::New(env, (int)screenFrame.origin.x));
|
|
1765
|
+
result.Set("screenY", Napi::Number::New(env, (int)screenFrame.origin.y));
|
|
1766
|
+
result.Set("screenWidth", Napi::Number::New(env, (int)screenFrame.size.width));
|
|
1767
|
+
result.Set("screenHeight", Napi::Number::New(env, (int)screenFrame.size.height));
|
|
1768
|
+
|
|
1769
|
+
// Clear selected window info after reading
|
|
1770
|
+
[g_selectedWindowInfo release];
|
|
1771
|
+
g_selectedWindowInfo = nil;
|
|
1772
|
+
|
|
1773
|
+
return result;
|
|
1774
|
+
|
|
1775
|
+
} @catch (NSException *exception) {
|
|
1776
|
+
return env.Null();
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// NAPI Function: Bring Window To Front
|
|
1781
|
+
Napi::Value BringWindowToFront(const Napi::CallbackInfo& info) {
|
|
1782
|
+
Napi::Env env = info.Env();
|
|
1783
|
+
|
|
1784
|
+
if (info.Length() < 1) {
|
|
1785
|
+
Napi::TypeError::New(env, "Window ID required").ThrowAsJavaScriptException();
|
|
1786
|
+
return env.Null();
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
int windowId = info[0].As<Napi::Number>().Int32Value();
|
|
1790
|
+
|
|
1791
|
+
@try {
|
|
1792
|
+
bool success = bringWindowToFront(windowId);
|
|
1793
|
+
return Napi::Boolean::New(env, success);
|
|
1794
|
+
|
|
1795
|
+
} @catch (NSException *exception) {
|
|
1796
|
+
return Napi::Boolean::New(env, false);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// NAPI Function: Enable/Disable Auto Bring To Front
|
|
1801
|
+
Napi::Value SetBringToFrontEnabled(const Napi::CallbackInfo& info) {
|
|
1802
|
+
Napi::Env env = info.Env();
|
|
1803
|
+
|
|
1804
|
+
if (info.Length() < 1) {
|
|
1805
|
+
Napi::TypeError::New(env, "Boolean value required").ThrowAsJavaScriptException();
|
|
1806
|
+
return env.Null();
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
bool enabled = info[0].As<Napi::Boolean>();
|
|
1810
|
+
g_bringToFrontEnabled = enabled;
|
|
1811
|
+
|
|
1812
|
+
NSLog(@"🔄 Auto bring-to-front: %s", enabled ? "ENABLED" : "DISABLED");
|
|
1813
|
+
|
|
1814
|
+
return Napi::Boolean::New(env, true);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// NAPI Function: Get Window Selection Status
|
|
1818
|
+
Napi::Value GetWindowSelectionStatus(const Napi::CallbackInfo& info) {
|
|
1819
|
+
Napi::Env env = info.Env();
|
|
1820
|
+
|
|
1821
|
+
// Update overlay each time status is requested (JavaScript polling approach)
|
|
1822
|
+
if (g_isWindowSelecting) {
|
|
1823
|
+
updateOverlay();
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1827
|
+
result.Set("isSelecting", Napi::Boolean::New(env, g_isWindowSelecting));
|
|
1828
|
+
result.Set("hasSelectedWindow", Napi::Boolean::New(env, g_selectedWindowInfo != nil));
|
|
1829
|
+
result.Set("windowCount", Napi::Number::New(env, g_allWindows ? [g_allWindows count] : 0));
|
|
1830
|
+
result.Set("hasOverlay", Napi::Boolean::New(env, g_overlayWindow != nil));
|
|
1831
|
+
|
|
1832
|
+
if (g_currentWindowUnderCursor) {
|
|
1833
|
+
Napi::Object currentWindow = Napi::Object::New(env);
|
|
1834
|
+
currentWindow.Set("id", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"id"] intValue]));
|
|
1835
|
+
currentWindow.Set("title", Napi::String::New(env, [[g_currentWindowUnderCursor objectForKey:@"title"] UTF8String]));
|
|
1836
|
+
currentWindow.Set("appName", Napi::String::New(env, [[g_currentWindowUnderCursor objectForKey:@"appName"] UTF8String]));
|
|
1837
|
+
currentWindow.Set("x", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"x"] intValue]));
|
|
1838
|
+
currentWindow.Set("y", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"y"] intValue]));
|
|
1839
|
+
currentWindow.Set("width", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"width"] intValue]));
|
|
1840
|
+
currentWindow.Set("height", Napi::Number::New(env, [[g_currentWindowUnderCursor objectForKey:@"height"] intValue]));
|
|
1841
|
+
result.Set("currentWindow", currentWindow);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
return result;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// NAPI Function: Show Recording Preview
|
|
1848
|
+
Napi::Value ShowRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1849
|
+
Napi::Env env = info.Env();
|
|
1850
|
+
|
|
1851
|
+
if (info.Length() < 1) {
|
|
1852
|
+
Napi::TypeError::New(env, "Window info object required").ThrowAsJavaScriptException();
|
|
1853
|
+
return env.Null();
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (!info[0].IsObject()) {
|
|
1857
|
+
Napi::TypeError::New(env, "Window info must be an object").ThrowAsJavaScriptException();
|
|
1858
|
+
return env.Null();
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
@try {
|
|
1862
|
+
Napi::Object windowInfoObj = info[0].As<Napi::Object>();
|
|
1863
|
+
|
|
1864
|
+
// Convert NAPI object to NSDictionary
|
|
1865
|
+
NSMutableDictionary *windowInfo = [[NSMutableDictionary alloc] init];
|
|
1866
|
+
|
|
1867
|
+
if (windowInfoObj.Has("id")) {
|
|
1868
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
|
|
1869
|
+
}
|
|
1870
|
+
if (windowInfoObj.Has("title")) {
|
|
1871
|
+
[windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("title").As<Napi::String>().Utf8Value().c_str()] forKey:@"title"];
|
|
1872
|
+
}
|
|
1873
|
+
if (windowInfoObj.Has("appName")) {
|
|
1874
|
+
[windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("appName").As<Napi::String>().Utf8Value().c_str()] forKey:@"appName"];
|
|
1875
|
+
}
|
|
1876
|
+
if (windowInfoObj.Has("x")) {
|
|
1877
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
|
|
1878
|
+
}
|
|
1879
|
+
if (windowInfoObj.Has("y")) {
|
|
1880
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
|
|
1881
|
+
}
|
|
1882
|
+
if (windowInfoObj.Has("width")) {
|
|
1883
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
|
|
1884
|
+
}
|
|
1885
|
+
if (windowInfoObj.Has("height")) {
|
|
1886
|
+
[windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
bool success = showRecordingPreview(windowInfo);
|
|
1890
|
+
[windowInfo release];
|
|
1891
|
+
|
|
1892
|
+
return Napi::Boolean::New(env, success);
|
|
1893
|
+
|
|
1894
|
+
} @catch (NSException *exception) {
|
|
1895
|
+
return Napi::Boolean::New(env, false);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// NAPI Function: Hide Recording Preview
|
|
1900
|
+
Napi::Value HideRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1901
|
+
Napi::Env env = info.Env();
|
|
1902
|
+
|
|
1903
|
+
@try {
|
|
1904
|
+
bool success = hideRecordingPreview();
|
|
1905
|
+
return Napi::Boolean::New(env, success);
|
|
1906
|
+
|
|
1907
|
+
} @catch (NSException *exception) {
|
|
1908
|
+
return Napi::Boolean::New(env, false);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// NAPI Function: Start Screen Selection
|
|
1913
|
+
Napi::Value StartScreenSelection(const Napi::CallbackInfo& info) {
|
|
1914
|
+
Napi::Env env = info.Env();
|
|
1915
|
+
|
|
1916
|
+
@try {
|
|
1917
|
+
bool success = startScreenSelection();
|
|
1918
|
+
return Napi::Boolean::New(env, success);
|
|
1919
|
+
|
|
1920
|
+
} @catch (NSException *exception) {
|
|
1921
|
+
return Napi::Boolean::New(env, false);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// NAPI Function: Stop Screen Selection
|
|
1926
|
+
Napi::Value StopScreenSelection(const Napi::CallbackInfo& info) {
|
|
1927
|
+
Napi::Env env = info.Env();
|
|
1928
|
+
|
|
1929
|
+
@try {
|
|
1930
|
+
bool success = stopScreenSelection();
|
|
1931
|
+
return Napi::Boolean::New(env, success);
|
|
1932
|
+
|
|
1933
|
+
} @catch (NSException *exception) {
|
|
1934
|
+
return Napi::Boolean::New(env, false);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// NAPI Function: Get Selected Screen Info
|
|
1939
|
+
Napi::Value GetSelectedScreenInfo(const Napi::CallbackInfo& info) {
|
|
1940
|
+
Napi::Env env = info.Env();
|
|
1941
|
+
|
|
1942
|
+
@try {
|
|
1943
|
+
NSDictionary *screenInfo = getSelectedScreenInfo();
|
|
1944
|
+
if (!screenInfo) {
|
|
1945
|
+
return env.Null();
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1949
|
+
result.Set("id", Napi::Number::New(env, [[screenInfo objectForKey:@"id"] intValue]));
|
|
1950
|
+
result.Set("name", Napi::String::New(env, [[screenInfo objectForKey:@"name"] UTF8String]));
|
|
1951
|
+
result.Set("x", Napi::Number::New(env, [[screenInfo objectForKey:@"x"] intValue]));
|
|
1952
|
+
result.Set("y", Napi::Number::New(env, [[screenInfo objectForKey:@"y"] intValue]));
|
|
1953
|
+
result.Set("width", Napi::Number::New(env, [[screenInfo objectForKey:@"width"] intValue]));
|
|
1954
|
+
result.Set("height", Napi::Number::New(env, [[screenInfo objectForKey:@"height"] intValue]));
|
|
1955
|
+
result.Set("resolution", Napi::String::New(env, [[screenInfo objectForKey:@"resolution"] UTF8String]));
|
|
1956
|
+
result.Set("isPrimary", Napi::Boolean::New(env, [[screenInfo objectForKey:@"isPrimary"] boolValue]));
|
|
1957
|
+
|
|
1958
|
+
NSLog(@"🖥️ SCREEN SELECTED: %@ (%@)",
|
|
1959
|
+
[screenInfo objectForKey:@"name"],
|
|
1960
|
+
[screenInfo objectForKey:@"resolution"]);
|
|
1961
|
+
|
|
1962
|
+
return result;
|
|
1963
|
+
|
|
1964
|
+
} @catch (NSException *exception) {
|
|
1965
|
+
return env.Null();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// NAPI Function: Show Screen Recording Preview
|
|
1970
|
+
Napi::Value ShowScreenRecordingPreview(const Napi::CallbackInfo& info) {
|
|
1971
|
+
Napi::Env env = info.Env();
|
|
1972
|
+
|
|
1973
|
+
if (info.Length() < 1) {
|
|
1974
|
+
Napi::TypeError::New(env, "Screen info object required").ThrowAsJavaScriptException();
|
|
1975
|
+
return env.Null();
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
if (!info[0].IsObject()) {
|
|
1979
|
+
Napi::TypeError::New(env, "Screen info must be an object").ThrowAsJavaScriptException();
|
|
1980
|
+
return env.Null();
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
@try {
|
|
1984
|
+
Napi::Object screenInfoObj = info[0].As<Napi::Object>();
|
|
1985
|
+
|
|
1986
|
+
// Convert NAPI object to NSDictionary
|
|
1987
|
+
NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
|
|
1988
|
+
|
|
1989
|
+
if (screenInfoObj.Has("id")) {
|
|
1990
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
|
|
1991
|
+
}
|
|
1992
|
+
if (screenInfoObj.Has("name")) {
|
|
1993
|
+
[screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("name").As<Napi::String>().Utf8Value().c_str()] forKey:@"name"];
|
|
1994
|
+
}
|
|
1995
|
+
if (screenInfoObj.Has("resolution")) {
|
|
1996
|
+
[screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("resolution").As<Napi::String>().Utf8Value().c_str()] forKey:@"resolution"];
|
|
1997
|
+
}
|
|
1998
|
+
if (screenInfoObj.Has("x")) {
|
|
1999
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
|
|
2000
|
+
}
|
|
2001
|
+
if (screenInfoObj.Has("y")) {
|
|
2002
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
|
|
2003
|
+
}
|
|
2004
|
+
if (screenInfoObj.Has("width")) {
|
|
2005
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
|
|
2006
|
+
}
|
|
2007
|
+
if (screenInfoObj.Has("height")) {
|
|
2008
|
+
[screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
bool success = showScreenRecordingPreview(screenInfo);
|
|
2012
|
+
[screenInfo release];
|
|
2013
|
+
|
|
2014
|
+
return Napi::Boolean::New(env, success);
|
|
2015
|
+
|
|
2016
|
+
} @catch (NSException *exception) {
|
|
2017
|
+
return Napi::Boolean::New(env, false);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// NAPI Function: Hide Screen Recording Preview
|
|
2022
|
+
Napi::Value HideScreenRecordingPreview(const Napi::CallbackInfo& info) {
|
|
2023
|
+
Napi::Env env = info.Env();
|
|
2024
|
+
|
|
2025
|
+
@try {
|
|
2026
|
+
bool success = hideScreenRecordingPreview();
|
|
2027
|
+
return Napi::Boolean::New(env, success);
|
|
2028
|
+
|
|
2029
|
+
} @catch (NSException *exception) {
|
|
2030
|
+
return Napi::Boolean::New(env, false);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Export functions
|
|
2035
|
+
Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
|
|
2036
|
+
exports.Set("startWindowSelection", Napi::Function::New(env, StartWindowSelection));
|
|
2037
|
+
exports.Set("stopWindowSelection", Napi::Function::New(env, StopWindowSelection));
|
|
2038
|
+
exports.Set("getSelectedWindowInfo", Napi::Function::New(env, GetSelectedWindowInfo));
|
|
2039
|
+
exports.Set("getWindowSelectionStatus", Napi::Function::New(env, GetWindowSelectionStatus));
|
|
2040
|
+
exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
|
|
2041
|
+
exports.Set("setBringToFrontEnabled", Napi::Function::New(env, SetBringToFrontEnabled));
|
|
2042
|
+
exports.Set("showRecordingPreview", Napi::Function::New(env, ShowRecordingPreview));
|
|
2043
|
+
exports.Set("hideRecordingPreview", Napi::Function::New(env, HideRecordingPreview));
|
|
2044
|
+
exports.Set("startScreenSelection", Napi::Function::New(env, StartScreenSelection));
|
|
2045
|
+
exports.Set("stopScreenSelection", Napi::Function::New(env, StopScreenSelection));
|
|
2046
|
+
exports.Set("getSelectedScreenInfo", Napi::Function::New(env, GetSelectedScreenInfo));
|
|
2047
|
+
exports.Set("showScreenRecordingPreview", Napi::Function::New(env, ShowScreenRecordingPreview));
|
|
2048
|
+
exports.Set("hideScreenRecordingPreview", Napi::Function::New(env, HideScreenRecordingPreview));
|
|
2049
|
+
|
|
2050
|
+
return exports;
|
|
2051
|
+
}
|