import AppKit import SwiftUI // MARK: - Multi-Monitor Screenshot System extension ScreenshotApp { // MARK: - Global Mouse Tracking Setup func setupGlobalMouseTracking() { // Global monitors (for events outside our app) globalMouseDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in self?.handleGlobalMouseDown(event) } globalMouseDragMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDragged]) { [weak self] event in self?.handleGlobalMouseDragged(event) } globalMouseUpMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseUp]) { [weak self] event in self?.handleGlobalMouseUp(event) } // Local monitors (for events within our app) - these can consume events localMouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in guard let self = self else { return event } if self.isMultiMonitorSelectionActive && !self.isDragging { let globalLocation = NSEvent.mouseLocation // SET MOUSE TRACKING VARIABLES FOR SINGLE CLICK DETECTION let allScreenModifier: UInt = (1 << 0) // Command key self.isAllScreenModifierPressed = self.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) self.mouseDownLocation = globalLocation self.mouseDownTime = CACurrentMediaTime() self.hasMouseMoved = false let modifierName = self.getModifierName(allScreenModifier) print("๐ŸŽฏ Local mouse down - starting selection at: \(globalLocation), \(modifierName): \(self.isAllScreenModifierPressed)") self.startDragSelection(at: globalLocation) return nil // Consume the event to prevent other apps from processing it } return event // Let other apps handle it normally } localMouseDragMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDragged]) { [weak self] event in guard let self = self else { return event } if self.isMultiMonitorSelectionActive && self.isDragging { let globalLocation = NSEvent.mouseLocation self.hasMouseMoved = true // MARK THAT MOUSE HAS MOVED self.updateDragSelection(to: globalLocation) return nil // LOCAL monitors return NSEvent? } return event } localMouseUpMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseUp]) { [weak self] event in guard let self = self else { return event } if self.isMultiMonitorSelectionActive && self.isDragging { let globalLocation = NSEvent.mouseLocation // CHECK FOR SINGLE CLICK FIRST let timeSinceMouseDown = CACurrentMediaTime() - self.mouseDownTime let distanceMoved = sqrt(pow(globalLocation.x - self.mouseDownLocation.x, 2) + pow(globalLocation.y - self.mouseDownLocation.y, 2)) print("๐ŸŽฏ Local mouse up at: \(globalLocation)") print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, Distance: \(distanceMoved)px, HasMouseMoved: \(self.hasMouseMoved)") print("๐ŸŽฏ isAllScreensCaptureToggledOn: \(self.isAllScreensCaptureToggledOn), isAllScreenModifierPressed (at mouseDown): \(self.isAllScreenModifierPressed), isWindowCaptureMode: \(self.isWindowCaptureMode)") // Was it a click (not a drag)? let isClick = !self.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 print("๐ŸŽฏ Evaluated isClick: \(isClick)") if isClick { // 1. If "all screens" mode is GETOGGLED ON if self.isAllScreensCaptureToggledOn { print("๐ŸŽฏ Condition MET: isAllScreensCaptureToggledOn is TRUE.") print("๐ŸŽฏ Local Click detected (toggle ON) - capturing all screens") self.deactivateMultiMonitorSelection() self.captureAllScreens() // captureAllScreens resets de toggle self.resetTrackingVariables() return event // Return event since this is a local monitor } // 2. If it was a single click (no toggle, no CMD modifier pressed) else if !self.isAllScreenModifierPressed { // IMPORTANT: Don't capture screen if we're in window capture mode if self.isWindowCaptureMode { print("๐ŸŽฏ Condition SKIPPED (isAllScreensCaptureToggledOn=false): Single click in window capture mode - ignoring screen capture") return event // Let event continue in window capture mode } print("๐ŸŽฏ Condition MET (isAllScreensCaptureToggledOn=false, !isAllScreenModifierPressed): Single click detected (toggle OFF) - capturing current screen") self.deactivateMultiMonitorSelection() self.captureCurrentScreen(at: globalLocation) self.resetTrackingVariables() return nil // Consume event } // 3. If it was CMD+click (modifier held, setting enabled) else if self.isAllScreenModifierPressed { let allScreenModifier: UInt = (1 << 0) // Command key let modifierName = self.getModifierName(allScreenModifier) print("๐ŸŽฏ Condition MET (isAllScreensCaptureToggledOn=false, isAllScreenModifierPressed): \(modifierName)+click detected - capturing all screens") self.deactivateMultiMonitorSelection() self.captureAllScreens() self.resetTrackingVariables() return nil // Consume event } else { print("๐ŸŽฏ Condition NOT MET for any click type when isClick=true. isAllScreensCaptureToggledOn=\(self.isAllScreensCaptureToggledOn), isAllScreenModifierPressed=\(self.isAllScreenModifierPressed)") } } else { print("๐ŸŽฏ Evaluated isClick as FALSE. Proceeding with drag logic or unhandled mouse up.") } // If it wasn't a click handled above, or it was a drag completion print("๐ŸŽฏ Local mouse up - ending drag selection normally or unhandled click.") self.endDragSelection(at: globalLocation) // For drag completion, endDragSelection handles deactivation and capture. // We can return nil here as well, as endDragSelection is the final action for a drag. // Or return event if subsequent handlers (like EventCaptureView.mouseUp) are desired for drag completion. // Let's return nil to make localMouseUpMonitor authoritative for drags too. return nil } return event } // ESC key monitor for canceling selection NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { [weak self] event in if event.keyCode == 53 { // ESC key if self?.isMultiMonitorSelectionActive == true { print("โŒจ๏ธ ESC pressed - canceling multi-monitor selection") self?.cancelMultiMonitorSelection() return nil // LOCAL monitors return NSEvent? } } return event } print("๐ŸŽฏ Global mouse tracking setup complete") } // MARK: - Global Mouse Event Handlers func handleGlobalMouseDown(_ event: NSEvent) { // Only start selection if multi-monitor selection mode is active and we're not already dragging guard isMultiMonitorSelectionActive && !isDragging else { return } let globalLocation = NSEvent.mouseLocation print("โœ… Starting selection at: \(globalLocation)") // Start drag selection at click location startDragSelection(at: globalLocation) } func handleGlobalMouseDragged(_ event: NSEvent) { guard isMultiMonitorSelectionActive && isDragging else { return } let globalLocation = NSEvent.mouseLocation updateDragSelection(to: globalLocation) } func handleGlobalMouseUp(_ event: NSEvent) { guard isMultiMonitorSelectionActive && isDragging else { return } let globalLocation = NSEvent.mouseLocation endDragSelection(at: globalLocation) } // MARK: - Global Event Monitors func setupGlobalKeyMonitor() { // Remove existing monitor if any removeGlobalKeyMonitor() // Add global key monitor for ESC key and custom modifier keys globalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown, .keyUp, .flagsChanged]) { [weak self] event in guard let self = self else { return } // Handle ESC key only during multi-monitor selection if event.type == .keyDown && event.keyCode == 53 { // ESC key if self.isMultiMonitorSelectionActive { print("โŒจ๏ธ Global ESC key detected, canceling selection") self.cancelMultiMonitorSelection() } return } // Handle custom modifier keys for window capture mode toggle - ALWAYS active during screenshot mode if event.type == .flagsChanged && self.isMultiMonitorSelectionActive { let currentFlags = event.modifierFlags let previousFlags = self.lastKnownModifierFlags // --- Command Toggle Logic (Uses lastKnownModifierFlags) --- let commandFlag = NSEvent.ModifierFlags.command if currentFlags.contains(commandFlag) && !previousFlags.contains(commandFlag) { print("โŒจ๏ธ Command key JUST PRESSED (via flagsChanged) - Toggling All Screens Mode.") self.toggleAllScreensCaptureMode() } // BELANGRIJK: De Command-toggle is nu onafhankelijk en zou de Option-logica niet moeten blokkeren. // --- Window Capture Logic removed (simplification) --- // Window capture functionality is now handled through spacebar during drag selection self.lastKnownModifierFlags = currentFlags // Update voor de volgende Command-toggle check } } print("โŒจ๏ธ Global key monitor setup for ESC and custom modifier keys") // Also setup global mouse monitor to intercept ALL mouse events setupGlobalMouseMonitor() } func setupGlobalMouseMonitor() { // Remove existing monitor if any removeGlobalMouseMonitor() // IMPORTANT: Don't setup mouse monitors if we're in window capture mode // Let the WindowCaptureManager handle mouse events instead if isWindowCaptureMode { print("๐ŸŽฏ Skipping global mouse monitor setup - in window capture mode") return } // Add BOTH global and local monitors for maximum coverage let globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [ .leftMouseDown, .leftMouseUp, .leftMouseDragged, .rightMouseDown, .rightMouseUp, .rightMouseDragged, .otherMouseDown, .otherMouseUp, .otherMouseDragged, .mouseMoved, .mouseEntered, .mouseExited, .scrollWheel ]) { [weak self] event in guard let self = self, self.isMultiMonitorSelectionActive else { return } // IMPORTANT: Don't handle mouse events if in window capture mode if self.isWindowCaptureMode { return } print("๐ŸŽฏ Global monitor intercepted: \(event.type.rawValue)") // During selection, intercept and handle all mouse events ourselves let globalLocation = NSEvent.mouseLocation switch event.type { case .leftMouseDown: if !self.isDragging { // Check if Command key is pressed for all-screen capture let allScreenModifier: UInt = (1 << 0) // Command key self.isAllScreenModifierPressed = self.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) self.mouseDownLocation = globalLocation self.mouseDownTime = CACurrentMediaTime() self.hasMouseMoved = false let modifierName = self.getModifierName(allScreenModifier) print("๐ŸŽฏ Global mouse monitor - mouse down at: \(globalLocation), \(modifierName): \(self.isAllScreenModifierPressed)") // If all-screen modifier+click and setting is enabled, capture all screens immediately if self.isAllScreenModifierPressed { print("๐ŸŽฏ \(modifierName)+click detected - capturing all screens") self.deactivateMultiMonitorSelection() self.captureAllScreens() self.resetTrackingVariables() // RESET for clean state return } // Otherwise start potential drag selection self.startDragSelection(at: globalLocation) } case .leftMouseDragged: if self.isDragging { self.hasMouseMoved = true self.updateDragSelection(to: globalLocation) } case .leftMouseUp: if self.isDragging { let timeSinceMouseDown = CACurrentMediaTime() - self.mouseDownTime let distanceMoved = sqrt(pow(globalLocation.x - self.mouseDownLocation.x, 2) + pow(globalLocation.y - self.mouseDownLocation.y, 2)) print("๐ŸŽฏ Global mouse monitor - mouse up at: \(globalLocation)") print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, distance: \(distanceMoved)px, moved: \(self.hasMouseMoved)") // If it was a single click (no significant movement and quick) if !self.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 && !self.isAllScreenModifierPressed { // IMPORTANT: Don't capture screen if we're in window capture mode if self.isWindowCaptureMode { print("๐ŸŽฏ Single click in window capture mode - ignoring screen capture") return // Global monitors return Void } print("๐ŸŽฏ Single click detected - capturing current screen") self.deactivateMultiMonitorSelection() self.captureCurrentScreen(at: globalLocation) self.resetTrackingVariables() // RESET for clean state return // Global monitors return Void } // Otherwise end drag selection normally print("๐ŸŽฏ Global mouse monitor - ending drag selection at: \(globalLocation)") self.endDragSelection(at: globalLocation) } // Global monitors return Void case .rightMouseDown, .rightMouseUp: print("๐ŸŽฏ Global mouse monitor - right-click, canceling selection") self.cancelMultiMonitorSelection() default: // For all other mouse events during selection, just consume them break } } // Add LOCAL monitor to intercept events within our app and BLOCK them from propagating let localMonitor = NSEvent.addLocalMonitorForEvents(matching: [ .leftMouseDown, .leftMouseUp, .leftMouseDragged, .rightMouseDown, .rightMouseUp, .rightMouseDragged, .otherMouseDown, .otherMouseUp, .otherMouseDragged, .mouseMoved, .mouseEntered, .mouseExited, .scrollWheel ]) { [weak self] event in guard let self = self, self.isMultiMonitorSelectionActive else { return event } // Only print for non-mouseMoved events to reduce spam if event.type != .mouseMoved { print("๐ŸŽฏ Local monitor intercepted: \(event.type.rawValue)") } // During selection, handle the event ourselves and RETURN NIL to block propagation let globalLocation = NSEvent.mouseLocation switch event.type { case .leftMouseDown: if !self.isDragging { // Check if Command key is pressed for all-screen capture let allScreenModifier: UInt = (1 << 0) // Command key self.isAllScreenModifierPressed = self.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) self.mouseDownLocation = globalLocation self.mouseDownTime = CACurrentMediaTime() self.hasMouseMoved = false let modifierName = self.getModifierName(allScreenModifier) print("๐ŸŽฏ Local mouse monitor - mouse down at: \(globalLocation), \(modifierName): \(self.isAllScreenModifierPressed)") // If all-screen modifier+click and setting is enabled, capture all screens immediately if self.isAllScreenModifierPressed { print("๐ŸŽฏ \(modifierName)+click detected - capturing all screens") self.deactivateMultiMonitorSelection() self.captureAllScreens() self.resetTrackingVariables() // RESET for clean state return nil } // Otherwise start potential drag selection self.startDragSelection(at: globalLocation) } return nil // BLOCK the event from propagating case .leftMouseDragged: if self.isDragging { self.hasMouseMoved = true self.updateDragSelection(to: globalLocation) } return nil // BLOCK the event from propagating case .leftMouseUp: if self.isDragging { let timeSinceMouseDown = CACurrentMediaTime() - self.mouseDownTime let distanceMoved = sqrt(pow(globalLocation.x - self.mouseDownLocation.x, 2) + pow(globalLocation.y - self.mouseDownLocation.y, 2)) print("๐ŸŽฏ Local mouse monitor - mouse up at: \(globalLocation)") print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, distance: \(distanceMoved)px, moved: \(self.hasMouseMoved)") // If it was a single click (no significant movement and quick) if !self.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 && !self.isAllScreenModifierPressed { // IMPORTANT: Don't capture screen if we're in window capture mode if self.isWindowCaptureMode { print("๐ŸŽฏ Single click in window capture mode - ignoring screen capture") return event // Let event continue in window capture mode } print("๐ŸŽฏ Single click detected - capturing current screen") self.deactivateMultiMonitorSelection() self.captureCurrentScreen(at: globalLocation) self.resetTrackingVariables() // RESET for clean state return event // Let event pass through after reset } // Otherwise end drag selection normally print("๐ŸŽฏ Local mouse monitor - ending drag selection at: \(globalLocation)") self.endDragSelection(at: globalLocation) } return nil // BLOCK the event from propagating case .rightMouseDown, .rightMouseUp: print("๐ŸŽฏ Local mouse monitor - right-click, canceling selection") self.cancelMultiMonitorSelection() return nil // BLOCK the event from propagating case .mouseMoved: // ๐Ÿ”ง FIX: Let mouseMoved events pass through for crosshair tracking return event default: // For all other mouse events during selection, BLOCK them return nil } } // Store both monitors globalMouseMonitor = [globalMonitor as Any, localMonitor as Any] print("๐ŸŽฏ Both global and local mouse monitors setup to intercept all mouse events") } func removeGlobalKeyMonitor() { if let monitor = globalKeyMonitor { NSEvent.removeMonitor(monitor) globalKeyMonitor = nil print("โŒจ๏ธ Global key monitor removed") } removeGlobalMouseMonitor() } func removeGlobalMouseMonitor() { if let monitors = globalMouseMonitor { if let monitorArray = monitors as? [Any] { // Multiple monitors stored as array for monitor in monitorArray { NSEvent.removeMonitor(monitor) } print("๐ŸŽฏ Both global and local mouse monitors removed") } else { // Single monitor (fallback) NSEvent.removeMonitor(monitors) print("๐ŸŽฏ Single mouse monitor removed") } globalMouseMonitor = nil } } // MARK: - Custom Crosshair Overlay System func hideCursor() { // Try to hide cursor using CGDisplayHideCursor (may require private API) // Alternative: use invisible cursor let invisibleCursor = NSCursor() invisibleCursor.set() print("๐Ÿ™ˆ System cursor hidden") } func showCursor() { // Restore normal cursor NSCursor.arrow.set() print("๐Ÿ‘๏ธ System cursor restored") } func setupCustomCrosshairOverlay() { // Remove existing crosshair windows removeCustomCrosshairOverlay() // Create crosshair windows for each screen for screen in NSScreen.screens { let crosshairWindow = createCrosshairWindow(for: screen) crosshairWindows.append(crosshairWindow) } // Setup mouse tracking to update crosshair position crosshairTrackingMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak self] event in self?.updateCrosshairPosition() } // Also add local monitor for events within our app let localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak self] event in self?.updateCrosshairPosition() return event } if let localMonitor = localMonitor { // Store both monitors (we'll need to clean up both) crosshairTrackingMonitor = [crosshairTrackingMonitor as Any, localMonitor] } // IMPORTANT: Set initial crosshair position to current mouse location updateCrosshairPosition() print("๐ŸŽฏ Custom crosshair overlay setup for all screens") } func createCrosshairWindow(for screen: NSScreen) -> NSWindow { let window = NSWindow( contentRect: screen.frame, styleMask: [.borderless], backing: .buffered, defer: false ) window.backgroundColor = NSColor.clear window.isOpaque = false window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.screenSaverWindow)) + 2) window.ignoresMouseEvents = true window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle] // Create crosshair view let crosshairView = CrosshairView(frame: window.contentView!.bounds) window.contentView = crosshairView window.orderFront(nil) return window } func updateCrosshairPosition() { guard isMultiMonitorSelectionActive else { return } let mouseLocation = NSEvent.mouseLocation for window in crosshairWindows { if let crosshairView = window.contentView as? CrosshairView { // Convert global mouse location to window coordinates let windowLocation = window.convertPoint(fromScreen: mouseLocation) crosshairView.updateCrosshairPosition(windowLocation) } } } func removeCustomCrosshairOverlay() { // Remove all crosshair windows for window in crosshairWindows { window.orderOut(nil) } crosshairWindows.removeAll() // Remove mouse tracking monitor if let monitor = crosshairTrackingMonitor { if let monitorArray = monitor as? [Any] { // Multiple monitors stored as array for mon in monitorArray { NSEvent.removeMonitor(mon) } } else { // Single monitor NSEvent.removeMonitor(monitor) } crosshairTrackingMonitor = nil } print("๐ŸŽฏ Custom crosshair overlay removed") } // MARK: - Multi-Monitor Screenshot Selection Logic func startDragSelection(at point: NSPoint) { isDragging = true startPoint = point currentEndPoint = point print("๐Ÿ–ฑ๏ธ Start multi-monitor drag selection at: \(point)") // Create initial overlay windows createOverlayWindows(from: startPoint, to: currentEndPoint) } func updateDragSelection(to point: NSPoint) { guard isDragging else { return } // Throttle updates for performance let currentTime = CACurrentMediaTime() guard currentTime - lastUpdateTime >= updateThrottle else { return } lastUpdateTime = currentTime currentEndPoint = point // Update overlay windows to show current selection updateOverlayWindows(from: startPoint, to: currentEndPoint) } func endDragSelection(at point: NSPoint) { guard isDragging else { return } isDragging = false currentEndPoint = point print("๐Ÿ–ฑ๏ธ End multi-monitor drag selection at: \(point)") // Calculate final selection rectangle let selectionRect = calculateSelectionRect(from: startPoint, to: currentEndPoint) // Hide overlay windows hideMultiMonitorOverlayWindows() // Deactivate selection mode deactivateMultiMonitorSelection() // Only capture if selection is meaningful (at least 2x2 pixels) if selectionRect.width >= 2 && selectionRect.height >= 2 { print("๐Ÿ“ธ Capturing multi-monitor screenshot of area: \(selectionRect)") captureMultiMonitorScreenshot(in: selectionRect) } else { print("โŒ Selection too small, canceling multi-monitor screenshot") } } func cancelMultiMonitorSelection() { isDragging = false isMultiMonitorSelectionActive = false isWindowCaptureMode = false // Reset window capture mode // Hide overlay windows hideMultiMonitorOverlayWindows() // Remove event capture window removeEventCaptureWindow() // Remove global key monitor removeGlobalKeyMonitor() // Remove custom crosshair overlay removeCustomCrosshairOverlay() // Show cursor again showCursor() // Reset tracking variables for clean state resetTrackingVariables() // Deactivate window capture if active if #available(macOS 12.3, *) { windowCaptureManager?.deactivateWindowSelectionMode() } print("โŒ Multi-monitor selection canceled") } func deactivateMultiMonitorSelection() { isMultiMonitorSelectionActive = false isWindowCaptureMode = false // Reset window capture mode // Remove event capture window removeEventCaptureWindow() // Remove global key monitor removeGlobalKeyMonitor() // Remove custom crosshair overlay removeCustomCrosshairOverlay() // Show cursor again showCursor() // Reset tracking variables for clean state resetTrackingVariables() // Deactivate window capture if active if #available(macOS 12.3, *) { windowCaptureManager?.deactivateWindowSelectionMode() } print("๐Ÿ”„ Multi-monitor selection mode deactivated") } func showSelectionModeNotification() { // Create a temporary notification window let notificationWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 300, height: 80), styleMask: [.borderless], backing: .buffered, defer: false ) notificationWindow.backgroundColor = NSColor.clear notificationWindow.isOpaque = false notificationWindow.level = .floating notificationWindow.ignoresMouseEvents = true // Create notification view let notificationView = NSView(frame: notificationWindow.frame) notificationView.wantsLayer = true // Background with blur effect let blurView = NSVisualEffectView(frame: notificationView.bounds) blurView.blendingMode = NSVisualEffectView.BlendingMode.behindWindow blurView.material = NSVisualEffectView.Material.hudWindow blurView.state = NSVisualEffectView.State.active blurView.layer?.cornerRadius = 12 blurView.layer?.masksToBounds = true notificationView.addSubview(blurView) // Text label let label = NSTextField(labelWithString: "๐Ÿ“ธ Multi-Monitor Screenshot Mode\nClick on empty desktop space to start") label.font = NSFont.systemFont(ofSize: 13, weight: .medium) label.textColor = .white label.alignment = .center label.frame = NSRect(x: 20, y: 20, width: 260, height: 40) notificationView.addSubview(label) notificationWindow.contentView = notificationView // Position at top center of main screen if let mainScreen = NSScreen.main { let screenFrame = mainScreen.frame let x = screenFrame.midX - notificationWindow.frame.width / 2 let y = screenFrame.maxY - 100 notificationWindow.setFrameOrigin(NSPoint(x: x, y: y)) } // Show with animation notificationWindow.alphaValue = 0 notificationWindow.orderFront(nil) NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 notificationWindow.animator().alphaValue = 1.0 } // Auto-hide after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 notificationWindow.animator().alphaValue = 0 }) { notificationWindow.orderOut(nil as Any?) } } } // MARK: - Multi-Window Overlay System func createOverlayWindows(from start: NSPoint, to end: NSPoint) { let selectionRect = calculateSelectionRect(from: start, to: end) // Clear any existing windows hideMultiMonitorOverlayWindows() // Get all screens that the selection intersects let intersectingScreens = NSScreen.screens.filter { screen in selectionRect.intersects(screen.frame) } // Create a separate window for each intersecting screen for screen in intersectingScreens { let intersection = selectionRect.intersection(screen.frame) // Only create window if intersection is meaningful if intersection.width > 1 && intersection.height > 1 { createSingleOverlayWindow(for: intersection, on: screen) } } } func createSingleOverlayWindow(for rect: NSRect, on screen: NSScreen) { let window = NSPanel( contentRect: rect, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) // Configure window properties for multi-monitor support window.level = .screenSaver // Below event capture window but above everything else window.isOpaque = false window.backgroundColor = .clear window.hasShadow = false window.ignoresMouseEvents = true // Don't interfere with mouse tracking window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle, .stationary] // Disable automatic positioning window.setFrameAutosaveName("") window.isMovableByWindowBackground = false window.isMovable = false window.animationBehavior = .none window.displaysWhenScreenProfileChanges = true // Panel-specific settings let panel = window panel.isFloatingPanel = true panel.becomesKeyOnlyIfNeeded = false panel.hidesOnDeactivate = false // Create and set content view with multi-monitor selection overlay let contentView = MultiMonitorSelectionOverlayView() let hostingView = NSHostingView(rootView: contentView) // Create a container view that will hold both the SwiftUI view and the crosshair let containerView = NSView(frame: rect) containerView.addSubview(hostingView) hostingView.frame = containerView.bounds hostingView.autoresizingMask = [.width, .height] // Add crosshair view on top let crosshairView = CrosshairCursorView(frame: NSRect(x: 0, y: 0, width: 24, height: 24)) containerView.addSubview(crosshairView) crosshairView.startTracking() window.contentView = containerView // Set frame and show window.setFrame(rect, display: false, animate: false) window.makeKeyAndOrderFront(nil as Any?) // Add to our collection overlayWindows.append(window) windowScreenMap[window] = screen print("๐ŸชŸ Created multi-monitor overlay window on \(screen.localizedName) at \(rect)") } func updateOverlayWindows(from start: NSPoint, to end: NSPoint) { let selectionRect = calculateSelectionRect(from: start, to: end) // Get all screens that the selection intersects let intersectingScreens = NSScreen.screens.filter { screen in selectionRect.intersects(screen.frame) } // Smart update: reuse existing windows when possible updateExistingWindows(for: intersectingScreens, selectionRect: selectionRect) } func updateExistingWindows(for screens: [NSScreen], selectionRect: NSRect) { // Track which screens we've updated var updatedScreens: Set = [] // Update existing windows that are still relevant overlayWindows = overlayWindows.compactMap { window in guard let associatedScreen = windowScreenMap[window] else { window.close() return nil } // Check if this screen still intersects with the selection let intersection = selectionRect.intersection(associatedScreen.frame) if intersection.width > 1 && intersection.height > 1 { // Update the existing window smoothly updateWindowFrame(window, to: intersection) updatedScreens.insert(associatedScreen) return window } else { // This screen no longer intersects, remove the window window.close() windowScreenMap.removeValue(forKey: window) return nil } } // Create new windows for screens that don't have one yet for screen in screens { if !updatedScreens.contains(screen) { let intersection = selectionRect.intersection(screen.frame) if intersection.width > 1 && intersection.height > 1 { createSingleOverlayWindow(for: intersection, on: screen) } } } } func updateWindowFrame(_ window: NSWindow, to rect: NSRect) { // Fast frame update without recreation window.setFrame(rect, display: true, animate: false) } func hideMultiMonitorOverlayWindows() { overlayWindows.forEach { $0.close() } overlayWindows.removeAll() windowScreenMap.removeAll() print("๐ŸชŸ All multi-monitor overlay windows hidden") } // MARK: - Screen Mapping and Calculation func calculateSelectionRect(from start: NSPoint, to end: NSPoint) -> NSRect { let minX = min(start.x, end.x) let maxX = max(start.x, end.x) let minY = min(start.y, end.y) let maxY = max(start.y, end.y) return NSRect( x: minX, y: minY, width: maxX - minX, height: maxY - minY ) } func findWindowForScreen(_ screen: NSScreen) -> NSWindow? { return overlayWindows.first { window in windowScreenMap[window] == screen } } // MARK: - Multi-Monitor Screenshot Capture func captureMultiMonitorScreenshot(in rect: NSRect) { print("๐Ÿ“ธ Capturing multi-monitor screenshot...") print("๐Ÿ“ Multi-monitor capture area: x=\(rect.origin.x), y=\(rect.origin.y), width=\(rect.width), height=\(rect.height)") // Convert from global screen coordinates to proper capture coordinates let convertedRect = convertGlobalToScreenCoordinates(rect) print("๐ŸŽฏ Converted capture area: x=\(convertedRect.origin.x), y=\(convertedRect.origin.y), width=\(convertedRect.width), height=\(convertedRect.height)") // Use the existing capture method with converted coordinates and multi-monitor flag capture(rect: convertedRect, isMultiMonitor: true) } func convertGlobalToScreenCoordinates(_ globalRect: NSRect) -> NSRect { // NSEvent.mouseLocation and CGWindowListCreateImage both use the same coordinate system: // - (0,0) is at the bottom-left of the main screen // - Y increases upward // - No conversion needed! print("๐Ÿ”„ Coordinate conversion (SIMPLIFIED):") print(" Input rect: \(globalRect)") print(" Output rect: \(globalRect) (NO CONVERSION)") // Diagnostic: Show which screens this rect intersects let intersectingScreens = NSScreen.screens.filter { screen in globalRect.intersects(screen.frame) } print(" ๐Ÿ“บ Intersecting screens:") for screen in intersectingScreens { let intersection = globalRect.intersection(screen.frame) print(" - \(screen.localizedName): intersection \(intersection)") } return globalRect } // MARK: - Screen Information func listAvailableScreens() { print("๐Ÿ“บ Available screens for multi-monitor support:") let screens = NSScreen.screens let mainScreen = NSScreen.main for (index, screen) in screens.enumerated() { let frame = screen.frame let isMain = screen == mainScreen let _ = screen.deviceDescription let displayName = screen.localizedName print(" \(index): \(displayName)") print(" Frame: x=\(frame.origin.x), y=\(frame.origin.y), w=\(frame.width), h=\(frame.height)") print(" Scale: \(screen.backingScaleFactor)x") if isMain { print(" โญ Main screen (primary display)") } // Check for potential issues if frame.origin.x != 0 && frame.origin.y != 0 { print(" ๐Ÿ“ Positioned screen (not at origin)") } // Show relative position if frame.origin.y > 0 { print(" โฌ†๏ธ Screen positioned ABOVE main screen") } else if frame.origin.y < 0 { print(" โฌ‡๏ธ Screen positioned BELOW main screen") } if frame.origin.x > 0 { print(" โžก๏ธ Screen positioned RIGHT of main screen") } else if frame.origin.x < 0 { print(" โฌ…๏ธ Screen positioned LEFT of main screen") } } // Calculate and display total desktop bounds let totalBounds = getAllScreensBounds() print("๐ŸŒ Total desktop bounds: x=\(totalBounds.origin.x) to \(totalBounds.maxX), y=\(totalBounds.origin.y) to \(totalBounds.maxY)") print("๐Ÿ“ Total desktop size: \(totalBounds.width) ร— \(totalBounds.height)") // Show coordinate system info print("๐Ÿงญ macOS Coordinate System Info:") print(" - (0,0) is at bottom-left of main screen") print(" - Y increases upward") print(" - Screens above main have positive Y") print(" - Screens below main have negative Y") // App Store compatibility check if screens.count > 2 { print("๐Ÿ” Multi-screen setup detected (\(screens.count) screens) - testing compatibility") } // Check for unusual configurations let hasNegativeCoordinates = screens.contains { $0.frame.origin.x < 0 || $0.frame.origin.y < 0 } if hasNegativeCoordinates { print("โš ๏ธ Negative coordinates detected - using advanced coordinate conversion") } } // MARK: - Cleanup func cleanupMultiMonitorResources() { // Remove all event monitors if let monitor = globalMouseDownMonitor { NSEvent.removeMonitor(monitor) globalMouseDownMonitor = nil } if let monitor = globalMouseDragMonitor { NSEvent.removeMonitor(monitor) globalMouseDragMonitor = nil } if let monitor = globalMouseUpMonitor { NSEvent.removeMonitor(monitor) globalMouseUpMonitor = nil } if let monitor = localMouseDownMonitor { NSEvent.removeMonitor(monitor) localMouseDownMonitor = nil } if let monitor = localMouseDragMonitor { NSEvent.removeMonitor(monitor) localMouseDragMonitor = nil } if let monitor = localMouseUpMonitor { NSEvent.removeMonitor(monitor) localMouseUpMonitor = nil } // Clean up windows hideMultiMonitorOverlayWindows() // Remove global key monitor removeGlobalKeyMonitor() // Remove custom crosshair overlay removeCustomCrosshairOverlay() print("๐Ÿงน Multi-monitor resources cleaned up") } // MARK: - Multi-Monitor Event Capture Window Management func createEventCaptureWindow() { // Remove any existing capture windows removeEventCaptureWindow() // Create a separate event capture window for each screen for (index, screen) in NSScreen.screens.enumerated() { let captureWindow = EventCaptureWindow( contentRect: screen.frame, styleMask: [.borderless], backing: .buffered, defer: false ) // Configure the window to be invisible but capture ALL events captureWindow.backgroundColor = NSColor.clear captureWindow.isOpaque = false captureWindow.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.assistiveTechHighWindow)) + 10) // Even higher level captureWindow.ignoresMouseEvents = false captureWindow.acceptsMouseMovedEvents = true captureWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle, .stationary] // CRITICAL: Prevent other windows from receiving events captureWindow.hidesOnDeactivate = false captureWindow.isMovableByWindowBackground = false // Force the window to stay on top and capture all events captureWindow.sharingType = .none captureWindow.isExcludedFromWindowsMenu = true // Create a container view for both event capture and crosshair let containerView = NSView(frame: NSRect(origin: .zero, size: screen.frame.size)) // Create a custom view that handles mouse events let captureView = EventCaptureView() captureView.screenshotApp = self captureView.frame = containerView.bounds containerView.addSubview(captureView) // Add crosshair view on top let crosshairView = CrosshairCursorView(frame: NSRect(x: 0, y: 0, width: 24, height: 24)) containerView.addSubview(crosshairView) crosshairView.startTracking() captureWindow.contentView = containerView // Position the window exactly on this screen captureWindow.setFrame(screen.frame, display: false) // Make sure this window is always on top captureWindow.orderFrontRegardless() // Store the window eventCaptureWindows.append(captureWindow) print("๐ŸŽฏ Event capture window \(index) created for screen: \(screen.localizedName) at \(screen.frame)") } // Make the first window key to receive keyboard events if let firstWindow = eventCaptureWindows.first { firstWindow.makeKey() // CRITICAL: Force our app to become active and prevent other apps from receiving events NSApp.activate(ignoringOtherApps: true) firstWindow.makeKeyAndOrderFront(nil as Any?) // Make the capture view the first responder for keyboard events DispatchQueue.main.async { if let captureView = firstWindow.contentView?.subviews.first(where: { $0 is EventCaptureView }) { firstWindow.makeFirstResponder(captureView) print("โŒจ๏ธ Made EventCaptureView first responder on primary capture window") } // Additional activation to ensure we're really on top NSApp.activate(ignoringOtherApps: true) } } print("๐Ÿ”’ Created \(eventCaptureWindows.count) event capture windows for multi-monitor setup") print("๐Ÿ”’ App activated to capture all events") } func removeEventCaptureWindow() { for window in eventCaptureWindows { window.orderOut(nil as Any?) } eventCaptureWindows.removeAll() // Also remove the legacy single window if it exists if let captureWindow = eventCaptureWindow { captureWindow.orderOut(nil as Any?) eventCaptureWindow = nil } print("๐Ÿ—‘๏ธ All event capture windows removed") } func getAllScreensBounds() -> NSRect { guard !NSScreen.screens.isEmpty else { return NSRect(x: 0, y: 0, width: 1920, height: 1080) // Fallback } print("๐Ÿ–ฅ๏ธ Detecting screens for event capture:") for (index, screen) in NSScreen.screens.enumerated() { print(" Screen \(index): \(screen.localizedName) - Frame: \(screen.frame)") } var minX: CGFloat = CGFloat.greatestFiniteMagnitude var minY: CGFloat = CGFloat.greatestFiniteMagnitude var maxX: CGFloat = -CGFloat.greatestFiniteMagnitude var maxY: CGFloat = -CGFloat.greatestFiniteMagnitude for screen in NSScreen.screens { let frame = screen.frame minX = min(minX, frame.minX) minY = min(minY, frame.minY) maxX = max(maxX, frame.maxX) maxY = max(maxY, frame.maxY) } let combinedBounds = NSRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) print("๐ŸŽฏ Combined screen bounds for event capture: \(combinedBounds)") return combinedBounds } // MARK: - Window Capture Mode State var isWindowCaptureMode: Bool { get { return objc_getAssociatedObject(self, &ScreenshotApp.isWindowCaptureModeKey) as? Bool ?? false } set { objc_setAssociatedObject(self, &ScreenshotApp.isWindowCaptureModeKey, newValue, .OBJC_ASSOCIATION_ASSIGN) } } private static var isWindowCaptureModeKey: UInt8 = 0 // MARK: - Window Capture Mode Switching func switchToWindowCaptureMode() { guard !isWindowCaptureMode else { return } print("๐ŸชŸ Switching to window capture mode") isWindowCaptureMode = true // IMPORTANT: Remove normal screenshot event capture windows removeEventCaptureWindow() removeCustomCrosshairOverlay() // CRITICAL: Also remove all mouse and key monitors that interfere with window capture removeGlobalKeyMonitor() // This also calls removeGlobalMouseMonitor() // CRITICAL: Also clean up the local mouse tracking from setupGlobalMouseTracking() if let monitor = localMouseDownMonitor { NSEvent.removeMonitor(monitor) localMouseDownMonitor = nil } if let monitor = localMouseDragMonitor { NSEvent.removeMonitor(monitor) localMouseDragMonitor = nil } if let monitor = localMouseUpMonitor { NSEvent.removeMonitor(monitor) localMouseUpMonitor = nil } if let monitor = globalMouseDownMonitor { NSEvent.removeMonitor(monitor) globalMouseDownMonitor = nil } if let monitor = globalMouseDragMonitor { NSEvent.removeMonitor(monitor) globalMouseDragMonitor = nil } if let monitor = globalMouseUpMonitor { NSEvent.removeMonitor(monitor) globalMouseUpMonitor = nil } print("๐ŸŽฏ All multi-monitor mouse tracking disabled for window capture mode") // Activate window capture functionality if #available(macOS 12.3, *) { windowCaptureManager?.activateWindowSelectionMode() } else { print("โš ๏ธ Window capture requires macOS 12.3 or later") isWindowCaptureMode = false // Restore normal screenshot mode if window capture fails createEventCaptureWindow() setupCustomCrosshairOverlay() setupGlobalKeyMonitor() // Restore monitors setupGlobalMouseTracking() // Restore local mouse tracking } } func switchToNormalScreenshotMode() { guard isWindowCaptureMode else { return } print("๐Ÿ“ธ Switching back to normal screenshot mode") isWindowCaptureMode = false // Deactivate window capture functionality if #available(macOS 12.3, *) { windowCaptureManager?.deactivateWindowSelectionMode() } // IMPORTANT: Restore normal screenshot event capture and crosshair createEventCaptureWindow() setupCustomCrosshairOverlay() // CRITICAL: Restore the global key monitor for normal screenshot mode setupGlobalKeyMonitor() // CRITICAL: Restore the local mouse tracking for normal screenshot mode setupGlobalMouseTracking() print("๐ŸŽฏ All multi-monitor mouse tracking restored for normal screenshot mode") } // MARK: - Helper Functions for Custom Modifiers func isModifierPressed(_ flags: NSEvent.ModifierFlags, modifier: UInt) -> Bool { if modifier & (1 << 0) != 0 && !flags.contains(.command) { return false } if modifier & (1 << 1) != 0 && !flags.contains(.shift) { return false } if modifier & (1 << 2) != 0 && !flags.contains(.option) { return false } if modifier & (1 << 3) != 0 && !flags.contains(.control) { return false } // Check that only the required modifiers are pressed let requiredCommand = modifier & (1 << 0) != 0 let requiredShift = modifier & (1 << 1) != 0 let requiredOption = modifier & (1 << 2) != 0 let requiredControl = modifier & (1 << 3) != 0 return flags.contains(.command) == requiredCommand && flags.contains(.shift) == requiredShift && flags.contains(.option) == requiredOption && flags.contains(.control) == requiredControl } func getModifierName(_ modifier: UInt) -> String { var parts: [String] = [] if modifier & (1 << 3) != 0 { parts.append("Control") } if modifier & (1 << 2) != 0 { parts.append("Option") } if modifier & (1 << 1) != 0 { parts.append("Shift") } if modifier & (1 << 0) != 0 { parts.append("Command") } return parts.joined(separator: "+") } }