🚀 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.
1263 lines
54 KiB
Swift
1263 lines
54 KiB
Swift
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: "+")
|
||
}
|
||
} |