Files
shotscreen/ShotScreen/Sources/CrosshairViews.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

379 lines
14 KiB
Swift

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()
}
}
}