🚀 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.
253 lines
8.3 KiB
Swift
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)
|
|
}
|
|
} |