node-mac-recorder 2.16.16 โ†’ 2.16.18

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(FORCE_AVFOUNDATION=1 node -e \"\nconsole.log(''๐Ÿงช Testing CRITICAL AVFoundation scaling fix...'');\nconst MacRecorder = require(''./index.js'');\nconst recorder = new MacRecorder();\n\nrecorder.startRecording(''/tmp/critical-scaling-test.mov'')\n .then(success => {\n console.log(''Start:'', success ? ''โœ… SUCCESS'' : ''โŒ FAILED'');\n if (success) {\n setTimeout(() => {\n recorder.stopRecording().then(() => {\n console.log(''โœ… Test complete'');\n const fs = require(''fs'');\n if (fs.existsSync(''/tmp/critical-scaling-test.mov'')) {\n const size = Math.round(fs.statSync(''/tmp/critical-scaling-test.mov'').size/1024);\n console.log(''๐Ÿ“น File size:'', size + ''KB'');\n }\n });\n }, 3000);\n }\n })\n .catch(console.error);\n\")"
4
+ "Bash(FORCE_AVFOUNDATION=1 node -e \"\nconsole.log(''๐Ÿงช Testing integrated overlay UI elements...'');\nconst WindowSelector = require(''./window-selector.js'');\nconst selector = new WindowSelector();\n\n// Test permission check first\nselector.checkPermissions().then(perms => {\n console.log(''Permissions:'', perms);\n \n // Start window selection to test integrated overlay elements\n return selector.startSelection();\n}).then(() => {\n console.log(''โœ… Window selection started - UI elements should be integrated into overlay'');\n console.log(''โžก๏ธ Move cursor over windows to test integrated button, logo, and title'');\n \n // Auto-stop after 15 seconds for testing\n setTimeout(() => {\n selector.stopSelection().then(() => {\n console.log(''โœ… Window selection stopped - test complete'');\n });\n }, 15000);\n}).catch(console.error);\n\")"
5
5
  ],
6
6
  "deny": [],
7
7
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.16.16",
3
+ "version": "2.16.18",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -16,6 +16,15 @@ static NSTimer *g_trackingTimer = nil;
16
16
  static NSDictionary *g_selectedWindowInfo = nil;
17
17
  static NSMutableArray *g_allWindows = nil;
18
18
 
19
+ // Per-screen overlay system
20
+ static NSMutableArray *g_perScreenOverlays = nil;
21
+ static NSMutableArray *g_perScreenOverlayViews = nil;
22
+ static NSInteger g_activeScreenIndex = -1;
23
+
24
+ // Forward declaration for delegate
25
+ @class WindowSelectorDelegate;
26
+ static WindowSelectorDelegate *g_delegate = nil;
27
+
19
28
  // Functions to hide/show main overlay window during recording
20
29
  void hideAllOverlayWindows() {
21
30
  if (g_overlayWindow && [g_overlayWindow isVisible]) {
@@ -68,6 +77,9 @@ bool showScreenRecordingPreview(NSDictionary *screenInfo);
68
77
  bool hideScreenRecordingPreview();
69
78
  void updateScreenOverlays();
70
79
 
80
+ // Forward declarations
81
+ @class HoverButton;
82
+
71
83
  // Custom overlay view class
72
84
  @interface WindowSelectorOverlayView : NSView
73
85
  @property (nonatomic, strong) NSDictionary *windowInfo;
@@ -75,7 +87,13 @@ void updateScreenOverlays();
75
87
  @property (nonatomic) BOOL isToggled;
76
88
  @property (nonatomic) NSRect highlightFrame;
77
89
  @property (nonatomic, strong) NSValue *globalOriginOffset; // Store global coordinate offset
90
+ @property (nonatomic, strong) HoverButton *recordButton;
91
+ @property (nonatomic, strong) NSImageView *appIconView;
92
+ @property (nonatomic, strong) NSTextField *infoLabel;
78
93
  - (void)setHighlightFrame:(NSRect)frame;
94
+ - (void)setupUIElements;
95
+ - (void)updateUIElements;
96
+ - (void)positionUIElements;
79
97
  @end
80
98
 
81
99
  // Custom button with hover effects
@@ -110,15 +128,14 @@ void updateScreenOverlays();
110
128
  // Disable focus ring completely
111
129
  [self setFocusRingType:NSFocusRingTypeNone];
112
130
 
113
- // Window selector overlay view created
131
+ // Setup UI elements (button, icon, label)
132
+ [self setupUIElements];
133
+
134
+ NSLog(@"๐ŸŽฏ WindowSelectorOverlayView created with UI elements integrated");
114
135
  }
115
136
  return self;
116
137
  }
117
138
 
118
- - (void)setHighlightFrame:(NSRect)frame {
119
- _highlightFrame = frame;
120
- [self setNeedsDisplay:YES]; // Trigger redraw
121
- }
122
139
 
123
140
  - (void)drawRect:(NSRect)dirtyRect {
124
141
  [super drawRect:dirtyRect];
@@ -213,6 +230,147 @@ void updateScreenOverlays();
213
230
  NSLog(@"๐ŸŽฏ Global toggle state: %s", g_hasToggledWindow ? "HAS_TOGGLED" : "NO_TOGGLE");
214
231
  }
215
232
 
233
+ - (void)setupUIElements {
234
+ NSLog(@"๐Ÿ”ง Setting up integrated UI elements");
235
+
236
+ // Create record button
237
+ self.recordButton = [[HoverButton alloc] initWithFrame:NSMakeRect(0, 0, 200, 60)];
238
+ [self.recordButton setTitle:@"Start Recording"];
239
+ [self.recordButton setFont:[NSFont systemFontOfSize:18.0 weight:NSFontWeightMedium]];
240
+ [self.recordButton setBezelStyle:NSBezelStyleRounded];
241
+ [self.recordButton setTarget:g_delegate];
242
+ [self.recordButton setAction:@selector(selectButtonClicked:)];
243
+
244
+ // Style the button
245
+ self.recordButton.wantsLayer = YES;
246
+ self.recordButton.layer.backgroundColor = [[NSColor colorWithRed:0.2 green:0.6 blue:1.0 alpha:0.9] CGColor];
247
+ self.recordButton.layer.cornerRadius = 8.0;
248
+ self.recordButton.layer.borderWidth = 0.0;
249
+
250
+ [self addSubview:self.recordButton];
251
+
252
+ // Create app icon view
253
+ self.appIconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 96, 96)];
254
+ [self.appIconView setImageScaling:NSImageScaleProportionallyUpOrDown];
255
+ [self.appIconView setWantsLayer:YES];
256
+ [self.appIconView.layer setCornerRadius:8.0];
257
+ [self.appIconView.layer setMasksToBounds:YES];
258
+ [self addSubview:self.appIconView];
259
+
260
+ // Create info label
261
+ self.infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 400, 60)];
262
+ [self.infoLabel setEditable:NO];
263
+ [self.infoLabel setSelectable:NO];
264
+ [self.infoLabel setBezeled:NO];
265
+ [self.infoLabel setDrawsBackground:NO];
266
+ [self.infoLabel setAlignment:NSTextAlignmentCenter];
267
+ [self.infoLabel setFont:[NSFont systemFontOfSize:23.4 weight:NSFontWeightMedium]];
268
+ [self.infoLabel setTextColor:[NSColor whiteColor]];
269
+ [self addSubview:self.infoLabel];
270
+
271
+ NSLog(@"โœ… UI elements created and added to overlay view");
272
+ }
273
+
274
+ - (void)updateUIElements {
275
+ if (!self.windowInfo) return;
276
+
277
+ NSLog(@"๐Ÿ”„ Updating UI elements for window: %@", [self.windowInfo objectForKey:@"title"]);
278
+
279
+ // Update info label
280
+ NSString *windowTitle = [self.windowInfo objectForKey:@"title"] ?: @"Unknown Window";
281
+ NSString *appName = [self.windowInfo objectForKey:@"appName"] ?: @"Unknown App";
282
+ [self.infoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", appName, windowTitle]];
283
+
284
+ // Update app icon
285
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
286
+ NSArray *runningApps = [workspace runningApplications];
287
+ NSImage *appIcon = nil;
288
+
289
+ for (NSRunningApplication *app in runningApps) {
290
+ if ([[app localizedName] isEqualToString:appName] || [[app bundleIdentifier] containsString:appName]) {
291
+ appIcon = [app icon];
292
+ break;
293
+ }
294
+ }
295
+
296
+ if (!appIcon) {
297
+ appIcon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)];
298
+ }
299
+
300
+ [self.appIconView setImage:appIcon];
301
+
302
+ // Position elements
303
+ [self positionUIElements];
304
+ }
305
+
306
+ - (void)positionUIElements {
307
+ if (NSEqualRects(self.highlightFrame, NSZeroRect)) {
308
+ // Hide elements when no window is highlighted
309
+ [self.recordButton setHidden:YES];
310
+ [self.appIconView setHidden:YES];
311
+ [self.infoLabel setHidden:YES];
312
+ return;
313
+ }
314
+
315
+ // Show elements
316
+ [self.recordButton setHidden:NO];
317
+ [self.appIconView setHidden:NO];
318
+ [self.infoLabel setHidden:NO];
319
+
320
+ // Calculate window center
321
+ CGFloat windowCenterX = self.highlightFrame.origin.x + (self.highlightFrame.size.width / 2);
322
+ CGFloat windowCenterY = self.highlightFrame.origin.y + (self.highlightFrame.size.height / 2);
323
+
324
+ // Position record button at window center
325
+ NSSize buttonSize = self.recordButton.frame.size;
326
+ [self.recordButton setFrameOrigin:NSMakePoint(
327
+ windowCenterX - (buttonSize.width / 2),
328
+ windowCenterY - (buttonSize.height / 2)
329
+ )];
330
+
331
+ // Position app icon above window center
332
+ [self.appIconView setFrameOrigin:NSMakePoint(
333
+ windowCenterX - 48, // Center horizontally (96/2 = 48)
334
+ windowCenterY + 120 // 120px above window center
335
+ )];
336
+
337
+ // Add floating animation to app icon
338
+ [self.appIconView.layer removeAnimationForKey:@"floatAnimationX"];
339
+ CABasicAnimation *floatAnimationX = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
340
+ floatAnimationX.fromValue = @(-4.0);
341
+ floatAnimationX.toValue = @(4.0);
342
+ floatAnimationX.duration = 1.0;
343
+ floatAnimationX.repeatCount = HUGE_VALF;
344
+ floatAnimationX.autoreverses = YES;
345
+ floatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
346
+ [self.appIconView.layer addAnimation:floatAnimationX forKey:@"floatAnimationX"];
347
+
348
+ // Position info label between icon and button
349
+ NSSize labelSize = self.infoLabel.frame.size;
350
+ [self.infoLabel setFrameOrigin:NSMakePoint(
351
+ windowCenterX - (labelSize.width / 2),
352
+ windowCenterY + 50 // 50px above window center
353
+ )];
354
+
355
+ NSLog(@"๐Ÿ“ UI elements positioned - Button:(%.0f,%.0f) Icon:(%.0f,%.0f) Label:(%.0f,%.0f)",
356
+ self.recordButton.frame.origin.x, self.recordButton.frame.origin.y,
357
+ self.appIconView.frame.origin.x, self.appIconView.frame.origin.y,
358
+ self.infoLabel.frame.origin.x, self.infoLabel.frame.origin.y);
359
+ }
360
+
361
+ // Override setHighlightFrame to trigger UI updates
362
+ - (void)setHighlightFrame:(NSRect)frame {
363
+ _highlightFrame = frame;
364
+ [self positionUIElements]; // Position UI elements when highlight changes
365
+ [self setNeedsDisplay:YES]; // Trigger redraw
366
+ }
367
+
368
+ // Override setWindowInfo to trigger UI updates
369
+ - (void)setWindowInfo:(NSDictionary *)windowInfo {
370
+ _windowInfo = windowInfo;
371
+ [self updateUIElements]; // Update UI elements when window info changes
372
+ }
373
+
216
374
  // Layer-based approach, no custom drawing needed
217
375
 
218
376
  @end
@@ -508,8 +666,6 @@ void updateScreenOverlays();
508
666
  }
509
667
  @end
510
668
 
511
- static WindowSelectorDelegate *g_delegate = nil;
512
-
513
669
  // Bring window to front using Accessibility API
514
670
  bool bringWindowToFront(int windowId) {
515
671
  @autoreleasepool {
@@ -866,242 +1022,74 @@ void updateOverlay() {
866
1022
  }
867
1023
  }
868
1024
 
869
- // Convert global window coordinates to local view coordinates
870
- // Account for multi-display offset from global origin
871
- WindowSelectorOverlayView *overlayView = (WindowSelectorOverlayView *)g_overlayView;
872
- NSPoint globalOffset = NSZeroPoint;
873
- if (overlayView.globalOriginOffset) {
874
- globalOffset = [overlayView.globalOriginOffset pointValue];
875
- }
876
-
877
- // Determine if this is a primary display window
1025
+ // NEW PER-SCREEN SYSTEM: Find which screen this window belongs to
878
1026
  NSArray *allScreens = [NSScreen screens];
879
- NSScreen *primaryScreen = [allScreens objectAtIndex:0]; // Primary screen
880
- NSRect primaryFrame = [primaryScreen frame];
881
- BOOL isPrimaryDisplayWindow = (x >= primaryFrame.origin.x &&
882
- x <= primaryFrame.origin.x + primaryFrame.size.width &&
883
- y >= primaryFrame.origin.y &&
884
- y <= primaryFrame.origin.y + primaryFrame.size.height);
885
-
886
- CGFloat localX, localY;
887
- if (isPrimaryDisplayWindow) {
888
- // Primary display windows: Calculate dynamic offset from combined frame
889
- NSRect combinedFrame = [g_overlayWindow frame];
890
-
891
- // Calculate primary screen offset within combined frame
892
- CGFloat primaryOffsetX = primaryFrame.origin.x - combinedFrame.origin.x;
893
- CGFloat primaryOffsetY = primaryFrame.origin.y - combinedFrame.origin.y;
894
-
895
- NSLog(@"๐Ÿ”ง PRIMARY DEBUG:");
896
- NSLog(@" Primary frame: (%.0f,%.0f) %.0fx%.0f", primaryFrame.origin.x, primaryFrame.origin.y, primaryFrame.size.width, primaryFrame.size.height);
897
- NSLog(@" Combined frame: (%.0f,%.0f) %.0fx%.0f", combinedFrame.origin.x, combinedFrame.origin.y, combinedFrame.size.width, combinedFrame.size.height);
898
- NSLog(@" Primary offset: (%.0f,%.0f)", primaryOffsetX, primaryOffsetY);
899
- NSLog(@" Window coords: (%.0f,%.0f) โ†’ Local: (%.0f,%.0f)", (CGFloat)x, (CGFloat)y, x + primaryOffsetX, ([g_overlayView frame].size.height - (y + primaryOffsetY)) - height);
900
-
901
- localX = x + primaryOffsetX;
902
- localY = ([g_overlayView frame].size.height - (y + primaryOffsetY)) - height;
1027
+ NSInteger targetScreenIndex = -1;
1028
+ NSScreen *targetScreen = nil;
1029
+
1030
+ // Find screen containing this window
1031
+ for (NSInteger i = 0; i < [allScreens count]; i++) {
1032
+ NSScreen *screen = [allScreens objectAtIndex:i];
1033
+ NSRect screenFrame = [screen frame];
903
1034
 
904
- } else {
905
- // Secondary display windows: Apply standard coordinate transformation
906
- NSLog(@"๐Ÿ”ง SECONDARY DEBUG:");
907
- NSLog(@" GlobalOffset: (%.0f,%.0f)", globalOffset.x, globalOffset.y);
908
- NSLog(@" Window coords: (%.0f,%.0f) โ†’ Local: (%.0f,%.0f)", (CGFloat)x, (CGFloat)y, x - globalOffset.x, ([g_overlayView frame].size.height - (y - globalOffset.y)) - height);
1035
+ // Check if window center is within this screen bounds
1036
+ CGFloat windowCenterX = x + width/2;
1037
+ CGFloat windowCenterY = y + height/2;
909
1038
 
910
- localX = x - globalOffset.x;
911
- localY = ([g_overlayView frame].size.height - (y - globalOffset.y)) - height;
912
- }
913
-
914
- // Update overlay view window info for highlighting
915
- [overlayView setWindowInfo:targetWindow];
916
- [overlayView setHighlightFrame:NSMakeRect(localX, localY, width, height)];
917
-
918
- // Only reset toggle state when switching to different window (not for position updates)
919
- if (!g_hasToggledWindow) {
920
- [(WindowSelectorOverlayView *)g_overlayView setIsToggled:NO];
921
- } else {
922
- // Keep toggle state for locked window
923
- [(WindowSelectorOverlayView *)g_overlayView setIsToggled:YES];
924
- }
925
-
926
- // Add/update info label above button
927
- NSTextField *infoLabel = nil;
928
- for (NSView *subview in [g_overlayWindow.contentView subviews]) {
929
- if ([subview isKindOfClass:[NSTextField class]]) {
930
- infoLabel = (NSTextField*)subview;
1039
+ if (windowCenterX >= screenFrame.origin.x &&
1040
+ windowCenterX <= screenFrame.origin.x + screenFrame.size.width &&
1041
+ windowCenterY >= screenFrame.origin.y &&
1042
+ windowCenterY <= screenFrame.origin.y + screenFrame.size.height) {
1043
+ targetScreenIndex = i;
1044
+ targetScreen = screen;
931
1045
  break;
932
1046
  }
933
1047
  }
934
1048
 
935
- if (!infoLabel) {
936
- infoLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, width - 40, 60)];
937
- [infoLabel setEditable:NO];
938
- [infoLabel setSelectable:NO];
939
- [infoLabel setBezeled:NO];
940
- [infoLabel setDrawsBackground:NO];
941
- [infoLabel setAlignment:NSTextAlignmentCenter];
942
- [infoLabel setFont:[NSFont systemFontOfSize:23.4 weight:NSFontWeightMedium]]; // 18 * 1.3 = 23.4
943
- [infoLabel setTextColor:[NSColor whiteColor]];
944
-
945
- // Force no borders on info label
946
- [infoLabel setWantsLayer:YES];
947
- infoLabel.layer.borderWidth = 1.0;
948
- infoLabel.layer.borderColor = [[NSColor clearColor] CGColor];
949
- infoLabel.layer.cornerRadius = 0.0;
950
- infoLabel.layer.masksToBounds = YES;
951
-
952
- [g_overlayWindow.contentView addSubview:infoLabel];
1049
+ if (targetScreenIndex == -1) {
1050
+ NSLog(@"โš ๏ธ Window not found on any screen: (%.0f,%.0f)", (CGFloat)x, (CGFloat)y);
1051
+ return;
953
1052
  }
954
1053
 
955
- // Add/update app icon
956
- NSImageView *appIconView = nil;
957
- for (NSView *subview in [g_overlayWindow.contentView subviews]) {
958
- if ([subview isKindOfClass:[NSImageView class]]) {
959
- appIconView = (NSImageView*)subview;
960
- break;
961
- }
962
- }
1054
+ // Get the overlay for target screen
1055
+ NSWindow *targetOverlay = [g_perScreenOverlays objectAtIndex:targetScreenIndex];
1056
+ WindowSelectorOverlayView *targetOverlayView = [g_perScreenOverlayViews objectAtIndex:targetScreenIndex];
1057
+ NSRect targetScreenFrame = [targetScreen frame];
963
1058
 
964
- if (!appIconView) {
965
- appIconView = [[NSImageView alloc] initWithFrame:NSMakeRect(0, 0, 96, 96)];
966
- [appIconView setImageScaling:NSImageScaleProportionallyUpOrDown];
967
- [appIconView setWantsLayer:YES];
968
- [appIconView.layer setCornerRadius:8.0];
969
- [appIconView.layer setMasksToBounds:YES];
970
- [appIconView.layer setBackgroundColor:[[NSColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.3] CGColor]]; // Debug background
971
-
972
- // Force no borders on app icon view
973
- appIconView.layer.borderWidth = 1.0;
974
- appIconView.layer.borderColor = [[NSColor clearColor] CGColor];
975
- appIconView.layer.shadowOpacity = 0.0;
976
- appIconView.layer.shadowRadius = 0.0;
977
- appIconView.layer.shadowOffset = NSMakeSize(0, 0);
978
-
979
- [g_overlayWindow.contentView addSubview:appIconView];
980
- NSLog(@"๐Ÿ–ผ๏ธ Created app icon view at frame: (%.0f, %.0f, %.0f, %.0f)",
981
- appIconView.frame.origin.x, appIconView.frame.origin.y,
982
- appIconView.frame.size.width, appIconView.frame.size.height);
983
- }
1059
+ // Calculate LOCAL coordinates within target screen (much simpler!)
1060
+ CGFloat localX = x - targetScreenFrame.origin.x;
1061
+ CGFloat localY = (targetScreenFrame.size.height - (y - targetScreenFrame.origin.y)) - height;
984
1062
 
985
- // Get app icon using NSWorkspace
986
- NSString *iconAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
987
- NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
988
- NSArray *runningApps = [workspace runningApplications];
989
- NSImage *appIcon = nil;
1063
+ NSLog(@"๐ŸŽฏ Window on Screen %ld: Global(%.0f,%.0f) โ†’ Local(%.0f,%.0f)",
1064
+ targetScreenIndex, (CGFloat)x, (CGFloat)y, localX, localY);
990
1065
 
991
- for (NSRunningApplication *app in runningApps) {
992
- if ([[app localizedName] isEqualToString:iconAppName] || [[app bundleIdentifier] containsString:iconAppName]) {
993
- appIcon = [app icon];
994
- break;
995
- }
1066
+ // Update active screen if changed
1067
+ if (g_activeScreenIndex != targetScreenIndex) {
1068
+ NSLog(@"๐Ÿ”„ Switching to Screen %ld overlay", targetScreenIndex);
1069
+ g_activeScreenIndex = targetScreenIndex;
1070
+ g_overlayWindow = targetOverlay;
1071
+ g_overlayView = targetOverlayView;
996
1072
  }
997
1073
 
998
- // Fallback to generic app icon if not found
999
- if (!appIcon) {
1000
- appIcon = [workspace iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)];
1001
- NSLog(@"โš ๏ธ Using fallback icon for app: %@", iconAppName);
1002
- } else {
1003
- NSLog(@"โœ… Found app icon for: %@", iconAppName);
1004
- }
1074
+ // Update TARGET overlay view window info for highlighting
1075
+ [targetOverlayView setWindowInfo:targetWindow];
1076
+ [targetOverlayView setHighlightFrame:NSMakeRect(localX, localY, width, height)];
1005
1077
 
1006
- [appIconView setImage:appIcon];
1007
- NSLog(@"๐Ÿ–ผ๏ธ Set icon image, size: %.0fx%.0f", [appIcon size].width, [appIcon size].height);
1008
-
1009
- // Update label text
1010
- NSString *labelWindowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Unknown Window";
1011
- NSString *labelAppName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown App";
1012
- [infoLabel setStringValue:[NSString stringWithFormat:@"%@\n%@", labelAppName, labelWindowTitle]];
1013
-
1014
- // Position buttons - Start Record in center of selected window
1015
- if (g_selectButton) {
1016
- NSSize buttonSize = [g_selectButton frame].size;
1017
- // Use local window center for positioning
1018
- CGFloat localWindowCenterX = localX + (width / 2);
1019
- CGFloat localWindowCenterY = localY + (height / 2);
1020
- NSPoint buttonCenter = NSMakePoint(
1021
- localWindowCenterX - (buttonSize.width / 2),
1022
- localWindowCenterY - (buttonSize.height / 2) // Perfect center of window
1023
- );
1024
-
1025
- NSLog(@" ButtonCalc: WindowCenter(%.0f, %.0f) -> Button(%.0f, %.0f)",
1026
- localWindowCenterX, localWindowCenterY, buttonCenter.x, buttonCenter.y);
1027
-
1028
- [g_selectButton setFrameOrigin:buttonCenter];
1029
-
1030
- // Position app icon above window center
1031
- NSPoint iconCenter = NSMakePoint(
1032
- localWindowCenterX - (96 / 2), // Center horizontally on window
1033
- localWindowCenterY + 120 // 120px above window center
1034
- );
1035
- [appIconView setFrameOrigin:iconCenter];
1036
- NSLog(@"๐ŸŽฏ Positioning app icon at: (%.0f, %.0f) for window size: (%.0f, %.0f)",
1037
- iconCenter.x, iconCenter.y, (float)width, (float)height);
1038
-
1039
- // Add fast horizontal floating animation after positioning
1040
- [appIconView.layer removeAnimationForKey:@"floatAnimationX"];
1041
- [appIconView.layer removeAnimationForKey:@"floatAnimationY"];
1042
-
1043
- // Faster horizontal float animation only
1044
- CABasicAnimation *floatAnimationX = [CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
1045
- floatAnimationX.fromValue = @(-4.0);
1046
- floatAnimationX.toValue = @(4.0);
1047
- floatAnimationX.duration = 1.0; // Much faster animation
1048
- floatAnimationX.repeatCount = HUGE_VALF;
1049
- floatAnimationX.autoreverses = YES;
1050
- floatAnimationX.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
1051
- [appIconView.layer addAnimation:floatAnimationX forKey:@"floatAnimationX"];
1052
-
1053
- // Position info label between icon and button relative to window center
1054
- NSPoint labelCenter = NSMakePoint(
1055
- localWindowCenterX - ([infoLabel frame].size.width / 2), // Center horizontally on window
1056
- localWindowCenterY + 50 // 50px above window center, below icon
1057
- );
1058
- [infoLabel setFrameOrigin:labelCenter];
1059
-
1060
- // Position cancel button below the main button
1061
- NSButton *cancelButton = nil;
1062
- for (NSView *subview in [g_overlayWindow.contentView subviews]) {
1063
- if ([subview isKindOfClass:[NSButton class]] &&
1064
- [[(NSButton*)subview title] isEqualToString:@"Cancel"]) {
1065
- cancelButton = (NSButton*)subview;
1066
- break;
1067
- }
1068
- }
1069
-
1070
- if (cancelButton) {
1071
- NSSize cancelButtonSize = [cancelButton frame].size;
1072
- NSPoint cancelButtonCenter = NSMakePoint(
1073
- localWindowCenterX - (cancelButtonSize.width / 2), // Center horizontally on window
1074
- localWindowCenterY - 80 // 80px below window center
1075
- );
1076
- [cancelButton setFrameOrigin:cancelButtonCenter];
1077
- }
1078
+ // Only reset toggle state when switching to different window (not for position updates)
1079
+ if (!g_hasToggledWindow) {
1080
+ [targetOverlayView setIsToggled:NO];
1081
+ } else {
1082
+ // Keep toggle state for locked window
1083
+ [targetOverlayView setIsToggled:YES];
1078
1084
  }
1079
1085
 
1080
- [g_overlayWindow orderFront:nil];
1081
- // DON'T make key - prevents focus ring
1082
- // [g_overlayWindow makeKeyAndOrderFront:nil];
1086
+ NSLog(@"โœ… Updated overlay view with integrated UI elements");
1083
1087
 
1084
- // Ensure subviews (except overlay view itself) have no borders after positioning
1085
- for (NSView *subview in [g_overlayWindow.contentView subviews]) {
1086
- // Skip the main overlay view - it handles its own borders
1087
- if ([subview isKindOfClass:[WindowSelectorOverlayView class]]) continue;
1088
-
1089
- if ([subview respondsToSelector:@selector(setWantsLayer:)]) {
1090
- [subview setWantsLayer:YES];
1091
- if (subview.layer) {
1092
- subview.layer.borderWidth = 1.0;
1093
- subview.layer.borderColor = [[NSColor clearColor] CGColor];
1094
- subview.layer.masksToBounds = YES;
1095
- subview.layer.shadowOpacity = 0.0;
1096
- subview.layer.shadowRadius = 0.0;
1097
- subview.layer.shadowOffset = NSMakeSize(0, 0);
1098
- }
1099
- }
1100
- }
1088
+ [targetOverlay orderFront:nil];
1101
1089
 
1102
- NSLog(@" โœ… Overlay Status: Level=%ld, Alpha=%.1f, Visible=%s, Frame Set=YES",
1103
- [g_overlayWindow level], [g_overlayWindow alphaValue],
1104
- [g_overlayWindow isVisible] ? "YES" : "NO");
1090
+ NSLog(@" โœ… Overlay Status: Level=%ld, Alpha=%.1f, Visible=%s",
1091
+ [targetOverlay level], [targetOverlay alphaValue],
1092
+ [targetOverlay isVisible] ? "YES" : "NO");
1105
1093
  } else if (!windowUnderCursor && g_currentWindowUnderCursor) {
1106
1094
  // No window under cursor and no toggle active, hide overlay
1107
1095
  NSString *leftWindowTitle = [g_currentWindowUnderCursor objectForKey:@"title"] ?: @"Untitled";
@@ -1133,7 +1121,27 @@ void cleanupWindowSelector() {
1133
1121
  g_windowKeyEventMonitor = nil;
1134
1122
  }
1135
1123
 
1136
- // Close overlay window
1124
+ // NEW PER-SCREEN CLEANUP: Close all per-screen overlays
1125
+ if (g_perScreenOverlays) {
1126
+ NSLog(@"๐Ÿงน Cleaning up %lu per-screen overlays", [g_perScreenOverlays count]);
1127
+ for (NSWindow *overlay in g_perScreenOverlays) {
1128
+ if (overlay) {
1129
+ [overlay close];
1130
+ }
1131
+ }
1132
+ [g_perScreenOverlays release];
1133
+ g_perScreenOverlays = nil;
1134
+ }
1135
+
1136
+ if (g_perScreenOverlayViews) {
1137
+ [g_perScreenOverlayViews release];
1138
+ g_perScreenOverlayViews = nil;
1139
+ }
1140
+
1141
+ // Reset per-screen state
1142
+ g_activeScreenIndex = -1;
1143
+
1144
+ // Close legacy overlay window (fallback cleanup)
1137
1145
  if (g_overlayWindow) {
1138
1146
  [g_overlayWindow close];
1139
1147
  g_overlayWindow = nil;
@@ -1846,32 +1854,55 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
1846
1854
  }
1847
1855
 
1848
1856
  // Get all windows
1849
- g_allWindows = [getAllSelectableWindows() retain];
1857
+ g_allWindows = [[NSMutableArray alloc] initWithArray:getAllSelectableWindows()];
1850
1858
 
1851
1859
  if (!g_allWindows || [g_allWindows count] == 0) {
1852
1860
  Napi::Error::New(env, "No selectable windows found").ThrowAsJavaScriptException();
1853
1861
  return env.Null();
1854
1862
  }
1855
1863
 
1856
- // Create multi-display overlay window to prevent window dragging
1864
+ // Create PER-SCREEN overlay windows (much simpler than combined overlay)
1857
1865
  NSArray *allScreens = [NSScreen screens];
1858
- NSRect combinedFrame = NSZeroRect;
1866
+ g_perScreenOverlays = [[NSMutableArray alloc] init];
1867
+ g_perScreenOverlayViews = [[NSMutableArray alloc] init];
1868
+
1869
+ NSLog(@"๐Ÿ–ฅ๏ธ Creating per-screen overlays for %lu displays", [allScreens count]);
1859
1870
 
1860
- // Calculate combined frame that covers all displays
1861
- for (NSScreen *screen in allScreens) {
1871
+ // Create overlay for each screen
1872
+ for (NSInteger i = 0; i < [allScreens count]; i++) {
1873
+ NSScreen *screen = [allScreens objectAtIndex:i];
1862
1874
  NSRect screenFrame = [screen frame];
1863
- if (NSEqualRects(combinedFrame, NSZeroRect)) {
1864
- combinedFrame = screenFrame;
1865
- } else {
1866
- combinedFrame = NSUnionRect(combinedFrame, screenFrame);
1867
- }
1875
+
1876
+ // Create per-screen overlay window
1877
+ NSWindow *screenOverlay = [[NoFocusWindow alloc] initWithContentRect:screenFrame
1878
+ styleMask:NSWindowStyleMaskBorderless
1879
+ backing:NSBackingStoreBuffered
1880
+ defer:NO
1881
+ screen:screen];
1882
+
1883
+ // Create per-screen overlay view
1884
+ WindowSelectorOverlayView *overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:NSMakeRect(0, 0, screenFrame.size.width, screenFrame.size.height)];
1885
+ [screenOverlay setContentView:overlayView];
1886
+
1887
+ // Configure overlay window
1888
+ [screenOverlay setStyleMask:NSWindowStyleMaskBorderless];
1889
+ [screenOverlay setBackgroundColor:[NSColor clearColor]];
1890
+ [screenOverlay setOpaque:NO];
1891
+ [screenOverlay setIgnoresMouseEvents:NO];
1892
+ [screenOverlay setAcceptsMouseMovedEvents:YES];
1893
+ [screenOverlay setLevel:NSScreenSaverWindowLevel];
1894
+ [screenOverlay setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary];
1895
+
1896
+ NSLog(@" Screen %ld: (%.0f,%.0f) %.0fx%.0f โ†’ Overlay created", i, screenFrame.origin.x, screenFrame.origin.y, screenFrame.size.width, screenFrame.size.height);
1897
+
1898
+ [g_perScreenOverlays addObject:screenOverlay];
1899
+ [g_perScreenOverlayViews addObject:overlayView];
1868
1900
  }
1869
1901
 
1870
- NSRect fullScreenFrame = combinedFrame;
1871
- g_overlayWindow = [[NoFocusWindow alloc] initWithContentRect:fullScreenFrame
1872
- styleMask:NSWindowStyleMaskBorderless
1873
- backing:NSBackingStoreBuffered
1874
- defer:NO];
1902
+ // For compatibility, keep first screen as main overlay
1903
+ g_overlayWindow = [g_perScreenOverlays objectAtIndex:0];
1904
+ g_overlayView = [g_perScreenOverlayViews objectAtIndex:0];
1905
+ g_activeScreenIndex = 0;
1875
1906
 
1876
1907
  // Force completely borderless appearance
1877
1908
  [g_overlayWindow setStyleMask:NSWindowStyleMaskBorderless];
@@ -1896,17 +1927,7 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
1896
1927
  [g_overlayWindow setOpaque:NO];
1897
1928
  [g_overlayWindow setBackgroundColor:[NSColor clearColor]];
1898
1929
 
1899
- // Create overlay view covering all displays
1900
- // Convert global coordinates to local view coordinates (offset by origin)
1901
- NSRect localViewFrame = NSMakeRect(0, 0, fullScreenFrame.size.width, fullScreenFrame.size.height);
1902
- g_overlayView = [[WindowSelectorOverlayView alloc] initWithFrame:localViewFrame];
1903
-
1904
- // Store the global origin offset for coordinate conversion
1905
- WindowSelectorOverlayView *overlayView = (WindowSelectorOverlayView *)g_overlayView;
1906
- overlayView.globalOriginOffset = [NSValue valueWithPoint:fullScreenFrame.origin];
1907
-
1908
- NSLog(@"๐Ÿ”ง SET GlobalOriginOffset: (%.0f, %.0f)", fullScreenFrame.origin.x, fullScreenFrame.origin.y);
1909
- [g_overlayWindow setContentView:g_overlayView];
1930
+ NSLog(@"โœ… Per-screen overlay system initialized with %lu screens", [allScreens count]);
1910
1931
 
1911
1932
  // Note: NSWindow doesn't have setWantsLayer method, only NSView does
1912
1933