Files
shotscreen/ShotScreen/Sources/MultiMonitorSystem.swift
Nick Roodenrijs 0dabed11d2 🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set:

 Core Features:
- Advanced screenshot capture system
- Multi-monitor support
- Professional UI/UX design
- Automated update system with Sparkle
- Apple notarized & code signed

🛠 Technical Excellence:
- Native Swift macOS application
- Professional build & deployment pipeline
- Comprehensive error handling
- Memory optimized performance

📦 Distribution Ready:
- Professional DMG packaging
- Apple notarization complete
- No security warnings for users
- Ready for public distribution

This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
2025-06-28 16:15:15 +02:00

1263 lines
54 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<NSScreen> = []
// 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: "+")
}
}