import AppKit // MARK: - Custom Crosshair View class CrosshairView: NSView { private var crosshairPosition: NSPoint = NSPoint.zero private let crosshairSize: CGFloat = 20 private let lineWidth: CGFloat = 2 override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.wantsLayer = true self.layer?.backgroundColor = NSColor.clear.cgColor } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateCrosshairPosition(_ position: NSPoint) { crosshairPosition = position needsDisplay = true } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) guard let context = NSGraphicsContext.current?.cgContext else { return } // Clear the view context.clear(bounds) // Set crosshair color (white with black outline for visibility) context.setStrokeColor(NSColor.white.cgColor) context.setLineWidth(lineWidth + 1) // Draw black outline context.setStrokeColor(NSColor.black.cgColor) drawCrosshair(in: context, at: crosshairPosition, size: crosshairSize + 2) // Draw white crosshair context.setStrokeColor(NSColor.white.cgColor) context.setLineWidth(lineWidth) drawCrosshair(in: context, at: crosshairPosition, size: crosshairSize) } private func drawCrosshair(in context: CGContext, at position: NSPoint, size: CGFloat) { let halfSize = size / 2 // Horizontal line context.move(to: CGPoint(x: position.x - halfSize, y: position.y)) context.addLine(to: CGPoint(x: position.x + halfSize, y: position.y)) // Vertical line context.move(to: CGPoint(x: position.x, y: position.y - halfSize)) context.addLine(to: CGPoint(x: position.x, y: position.y + halfSize)) context.strokePath() } } class CrosshairCursorView: NSView { private var trackingArea: NSTrackingArea? private var cursorSize: CGFloat = 24.0 private var mouseLocation: NSPoint = .zero // ๐Ÿ”ง NEW: Event-driven approach instead of timer-based private var globalMouseMonitor: Any? private var localMouseMonitor: Any? private var isTrackingActive: Bool = false private var lastUpdateTime: Date = Date() private var healthCheckTimer: Timer? // NIEUW: Enum voor crosshair modus enum CrosshairMode { case normal case allScreensActive } // NIEUW: Property om de huidige modus bij te houden var currentMode: CrosshairMode = .normal { didSet { if oldValue != currentMode { DispatchQueue.main.async { // Zorg dat UI updates op main thread gebeuren self.needsDisplay = true } } } } override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.wantsLayer = true // Make view transparent to mouse events self.isHidden = true // Start hidden } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { // ๐Ÿ”ง CRITICAL: Clean up on deallocation print("๐Ÿงน DEBUG: CrosshairCursorView deinit - cleaning up") stopTracking() } override func updateTrackingAreas() { super.updateTrackingAreas() if let existingTrackingArea = trackingArea { self.removeTrackingArea(existingTrackingArea) } // Track the entire window, not just this view if let window = self.window { let options: NSTrackingArea.Options = [.mouseMoved, .activeAlways, .mouseEnteredAndExited] trackingArea = NSTrackingArea(rect: window.contentView?.bounds ?? self.bounds, options: options, owner: self, userInfo: nil) if let trackingArea = trackingArea { window.contentView?.addTrackingArea(trackingArea) } } } // ๐Ÿ”ง NEW: Event-driven tracking system - much more reliable! func startTracking() { // Stop any existing tracking first stopTracking() print("๐ŸŽฏ DEBUG: Starting event-driven crosshair tracking") // ๐Ÿ”ง CRITICAL FIX: Only start if not already active guard !isTrackingActive else { print("โš ๏ธ DEBUG: Tracking already active, skipping start") return } isTrackingActive = true self.isHidden = false // ๐Ÿ”ง SOLUTION: Use global mouse monitoring for INSTANT response globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] event in guard let self = self, self.isTrackingActive else { return } DispatchQueue.main.async { self.handleMouseEvent(event) } } // ๐Ÿ”ง SOLUTION: Also monitor local events for when mouse is over the app localMouseMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] event in guard let self = self, self.isTrackingActive else { return event } DispatchQueue.main.async { self.handleMouseEvent(event) } return event } // ๐Ÿ”ง SOLUTION: Force initial position update DispatchQueue.main.async { [weak self] in self?.updateToCurrentMousePosition() } // ๐Ÿ”ง NEW: Start health check timer startHealthCheck() print("โœ… DEBUG: Event-driven tracking started successfully") } private func startHealthCheck() { // Stop any existing health check healthCheckTimer?.invalidate() // Start new health check every 0.5 seconds healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in guard let self = self else { return } let timeSinceLastUpdate = Date().timeIntervalSince(self.lastUpdateTime) if timeSinceLastUpdate > 1.0 && self.isTrackingActive { print("โš ๏ธ DEBUG: Crosshair hasn't updated in \(timeSinceLastUpdate)s, forcing position update") DispatchQueue.main.async { self.updateToCurrentMousePosition() } } } } // ๐Ÿ”ง NEW: Simplified fallback position update private func updatePositionFallback(screenLocation: NSPoint) { // Use screen coordinates with basic offset (less precise but always works) let targetLocation = NSPoint(x: screenLocation.x - 100, y: screenLocation.y - 100) updatePosition(location: targetLocation) } func stopTracking() { print("๐Ÿ›‘ DEBUG: Stopping event-driven crosshair tracking") // ๐Ÿ”ง CRITICAL FIX: Only stop if actually tracking guard isTrackingActive else { print("โš ๏ธ DEBUG: Tracking not active, skipping stop") return } isTrackingActive = false // ๐Ÿ”ง SOLUTION: Remove event monitors cleanly if let globalMonitor = globalMouseMonitor { NSEvent.removeMonitor(globalMonitor) globalMouseMonitor = nil print("โœ… DEBUG: Global monitor removed") } if let localMonitor = localMouseMonitor { NSEvent.removeMonitor(localMonitor) localMouseMonitor = nil print("โœ… DEBUG: Local monitor removed") } self.isHidden = true // ๐Ÿ”ง NEW: Stop health check timer healthCheckTimer?.invalidate() healthCheckTimer = nil print("โœ… DEBUG: Event monitors removed successfully") } // ๐Ÿ”ง NEW: Event handler for mouse movements private func handleMouseEvent(_ event: NSEvent) { guard isTrackingActive else { return } // ๐Ÿ”ง ROBUSTNESS: Check if we still have a valid window guard self.window != nil else { print("โš ๏ธ DEBUG: No window available, stopping tracking") stopTracking() return } updateToCurrentMousePosition() } // ๐Ÿ”ง NEW: Direct mouse position update - much more reliable private func updateToCurrentMousePosition() { guard isTrackingActive else { return } // ๐Ÿ”ง ROBUSTNESS: Ensure we're on main thread guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in self?.updateToCurrentMousePosition() } return } // Get current mouse position in screen coordinates let mouseLocationScreen = NSEvent.mouseLocation // Convert to our coordinate system if let targetLocation = convertScreenToViewCoordinates(mouseLocationScreen) { updatePosition(location: targetLocation) } else { print("โš ๏ธ DEBUG: Failed to convert coordinates, using fallback") updatePositionFallback(screenLocation: mouseLocationScreen) } } // ๐Ÿ”ง NEW: Robust coordinate conversion with fallback private func convertScreenToViewCoordinates(_ screenLocation: NSPoint) -> NSPoint? { guard let window = self.window else { print("โš ๏ธ DEBUG: No window available for coordinate conversion") return nil } // ๐Ÿ”ง ROBUSTNESS: Validate window is visible guard window.isVisible else { print("โš ๏ธ DEBUG: Window not visible for coordinate conversion") return nil } let windowLocation = window.convertPoint(fromScreen: screenLocation) if let superview = self.superview { return superview.convert(windowLocation, from: nil) } else { return windowLocation } } func updatePosition(location: NSPoint) { // ๐Ÿ”ง SIMPLIFIED: Basic validation and direct update guard location.x.isFinite && location.y.isFinite else { return } // ๐Ÿ”ง SIMPLIFIED: Check if position changed significantly let tolerance: CGFloat = 1.0 if abs(self.mouseLocation.x - location.x) < tolerance && abs(self.mouseLocation.y - location.y) < tolerance { return } self.mouseLocation = location self.lastUpdateTime = Date() // Update timestamp // ๐Ÿ”ง SIMPLIFIED: Direct frame update (we're already on main thread from event handler) CATransaction.begin() CATransaction.setDisableActions(true) let newFrame = NSRect(x: location.x - cursorSize/2, y: location.y - cursorSize/2, width: cursorSize, height: cursorSize) if newFrame.width > 0 && newFrame.height > 0 { self.frame = newFrame self.needsDisplay = true } CATransaction.commit() } override func mouseMoved(with event: NSEvent) { // ๐Ÿ”ง SIMPLIFIED: Use new event-driven system handleMouseEvent(event) } override func mouseEntered(with event: NSEvent) { // ๐Ÿ”ง SIMPLIFIED: Use new event-driven system handleMouseEvent(event) } override func mouseDragged(with event: NSEvent) { // ๐Ÿ”ง SIMPLIFIED: Use new event-driven system handleMouseEvent(event) } override func draw(_ dirtyRect: NSRect) { // Draw a crosshair cursor guard let context = NSGraphicsContext.current?.cgContext else { return } // Standaard crosshair (wit met zwarte outline) context.setStrokeColor(NSColor.black.cgColor) context.setLineWidth(1.0) // Outline dikte // Horizontale lijn (outline boven) context.move(to: CGPoint(x: 0, y: cursorSize/2 - 1.5)) context.addLine(to: CGPoint(x: cursorSize, y: cursorSize/2 - 1.5)) // Horizontale lijn (outline onder) context.move(to: CGPoint(x: 0, y: cursorSize/2 + 1.5)) context.addLine(to: CGPoint(x: cursorSize, y: cursorSize/2 + 1.5)) // Verticale lijn (outline links) context.move(to: CGPoint(x: cursorSize/2 - 1.5, y: 0)) context.addLine(to: CGPoint(x: cursorSize/2 - 1.5, y: cursorSize)) // Verticale lijn (outline rechts) context.move(to: CGPoint(x: cursorSize/2 + 1.5, y: 0)) context.addLine(to: CGPoint(x: cursorSize/2 + 1.5, y: cursorSize)) context.strokePath() context.setStrokeColor(NSColor.white.cgColor) context.setLineWidth(2.0) // Dikte van witte crosshair // Horizontale lijn (wit) context.move(to: CGPoint(x: 0, y: cursorSize/2)) context.addLine(to: CGPoint(x: cursorSize, y: cursorSize/2)) // Verticale lijn (wit) context.move(to: CGPoint(x: cursorSize/2, y: 0)) context.addLine(to: CGPoint(x: cursorSize/2, y: cursorSize)) context.strokePath() // Teken extra elementen gebaseerd op modus if currentMode == .allScreensActive { context.setStrokeColor(NSColor.systemBlue.withAlphaComponent(0.8).cgColor) // Lichtblauwe kleur context.setLineWidth(2.0) let circleRect = NSRect(x: 1, y: 1, width: cursorSize - 2, height: cursorSize - 2) // Iets kleiner dan de crosshair bounds context.addEllipse(in: circleRect) context.strokePath() } } }