🎉 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.
This commit is contained in:
253
ShotScreen/Sources/OverlayComponents.swift
Normal file
253
ShotScreen/Sources/OverlayComponents.swift
Normal file
@@ -0,0 +1,253 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user