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

253 lines
8.3 KiB
Swift

import AppKit
// MARK: - Overlay Window for Screenshot Selection
class OverlayWindow: NSWindow {
var crosshairView: CrosshairCursorView?
var onCancelRequested: (() -> Void)?
init(screen: NSScreen) {
super.init(contentRect: screen.frame,
styleMask: [.borderless],
backing: .buffered,
defer: false)
self.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.mainMenuWindow)) + 1)
self.isOpaque = false
self.backgroundColor = NSColor.clear
self.ignoresMouseEvents = false
self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
// Try to hide the system cursor
CGDisplayHideCursor(CGMainDisplayID())
// Create an empty content view first
let contentContainer = NSView(frame: screen.frame)
contentContainer.wantsLayer = true
self.contentView = contentContainer
// Make this window visible
self.makeKeyAndOrderFront(nil as Any?)
self.orderFrontRegardless()
// Add our custom crosshair view at the top level
crosshairView = CrosshairCursorView(frame: NSRect(x: 0, y: 0, width: 24, height: 24))
if let crosshairView = crosshairView {
contentContainer.addSubview(crosshairView)
// Get current mouse position and update crosshair initial position
let mouseLoc = NSEvent.mouseLocation
let windowLoc = self.convertPoint(fromScreen: mouseLoc)
crosshairView.frame = NSRect(x: windowLoc.x - 12, y: windowLoc.y - 12, width: 24, height: 24)
// Start tracking timer
crosshairView.startTracking()
}
}
deinit {
// Show the cursor again when window is destroyed
CGDisplayShowCursor(CGMainDisplayID())
}
// Forceer dat het window key events kan ontvangen, zelfs zonder title bar
override var canBecomeKey: Bool {
return true
}
override func keyDown(with event: NSEvent) {
if event.keyCode == 53 { // 53 is Escape
print("ESC key pressed in OverlayWindow - keyDown")
// Roep alleen de callback aan
onCancelRequested?()
// Sluit het venster NIET HIER
} else {
// Voor andere toetsen, stuur door naar super
super.keyDown(with: event)
}
}
override func rightMouseDown(with event: NSEvent) {
print("🖱 Right mouse down in OverlayWindow")
// Roep alleen de callback aan
onCancelRequested?()
// Roep super niet aan, we handelen dit af.
}
override func close() {
// Show the cursor before closing
CGDisplayShowCursor(CGMainDisplayID())
super.close()
}
}
// MARK: - Selection View for Drawing Selection Rectangle
class SelectionView: NSView {
var startPoint: NSPoint?
var endPoint: NSPoint?
var selectionLayer = CAShapeLayer()
var onSelectionComplete: ((NSRect) -> Void)?
weak var parentWindow: OverlayWindow?
private var isSelecting = false
private var hasStartedSelection = false
override var acceptsFirstResponder: Bool { true }
// Add mouse event monitoring
private var eventMonitor: Any?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
// Listen to mouse events at the application level
setupEventMonitor()
}
private func setupEventMonitor() {
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]) { [weak self] event in
guard let self = self else { return event }
// Get the location in window coordinates
let locationInWindow = event.locationInWindow
switch event.type {
case .leftMouseDown:
self.handleMouseDown(at: locationInWindow)
case .leftMouseDragged:
self.handleMouseDragged(at: locationInWindow)
case .leftMouseUp:
self.handleMouseUp(at: locationInWindow)
default:
break
}
return event
}
}
// Maak internal ipv private
func removeEventMonitor() {
if let eventMonitor = eventMonitor {
NSEvent.removeMonitor(eventMonitor)
self.eventMonitor = nil
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
parentWindow = window as? OverlayWindow
}
override func removeFromSuperview() {
// Remove event monitor
removeEventMonitor()
// Show cursor when removed
CGDisplayShowCursor(CGMainDisplayID())
super.removeFromSuperview()
}
func handleMouseDown(at location: NSPoint) {
isSelecting = true
hasStartedSelection = true
startPoint = location
endPoint = location
updateSelectionLayer()
print("🖱 Mouse down at: \(location)")
// Update crosshair position
if let parentWindow = parentWindow,
let crosshairView = parentWindow.crosshairView {
crosshairView.updatePosition(location: location)
}
}
func handleMouseDragged(at location: NSPoint) {
guard isSelecting && hasStartedSelection else { return }
endPoint = location
updateSelectionLayer()
// Update crosshair position
if let parentWindow = parentWindow,
let crosshairView = parentWindow.crosshairView {
crosshairView.updatePosition(location: location)
}
}
func handleMouseUp(at location: NSPoint) {
guard isSelecting && hasStartedSelection else { return }
isSelecting = false
// Set the end point
endPoint = location
guard let start = startPoint, let end = endPoint else { return }
// Calculate the selection rectangle in screen coordinates
let rect: NSRect
// If start and end are very close (within 5 pixels), assume user wants the full screen
if abs(start.x - end.x) < 5 && abs(start.y - end.y) < 5 {
rect = self.bounds
print("📺 Full screen capture requested")
} else {
rect = NSRect(x: min(start.x, end.x),
y: min(start.y, end.y),
width: abs(start.x - end.x),
height: abs(start.y - end.y))
print("✂️ Selection completed: \(rect)")
}
// Show cursor before completing
CGDisplayShowCursor(CGMainDisplayID())
// Remove the event monitor
removeEventMonitor()
onSelectionComplete?(rect)
removeFromSuperview()
window?.orderOut(nil as Any?)
}
// Keep these for direct interactions with the view
override func mouseDown(with event: NSEvent) {
handleMouseDown(at: event.locationInWindow)
}
override func mouseDragged(with event: NSEvent) {
handleMouseDragged(at: event.locationInWindow)
}
override func mouseUp(with event: NSEvent) {
handleMouseUp(at: event.locationInWindow)
}
func updateSelectionLayer() {
selectionLayer.removeFromSuperlayer()
// Only create selection if we have valid start and end points
guard let start = startPoint, let end = endPoint,
abs(start.x - end.x) >= 1 || abs(start.y - end.y) >= 1 else {
return
}
let rect = NSRect(x: min(start.x, end.x),
y: min(start.y, end.y),
width: max(1, abs(start.x - end.x)),
height: max(1, abs(start.y - end.y)))
let path = CGPath(rect: rect, transform: nil)
selectionLayer.path = path
selectionLayer.fillColor = NSColor(calibratedWhite: 1, alpha: 0.3).cgColor
selectionLayer.strokeColor = NSColor.white.cgColor
selectionLayer.lineWidth = 1.0
self.layer?.addSublayer(selectionLayer)
}
}