🚀 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.
280 lines
11 KiB
Swift
280 lines
11 KiB
Swift
import AppKit
|
|
|
|
// MARK: - Event Capture Window
|
|
class EventCaptureWindow: NSWindow {
|
|
override var canBecomeKey: Bool {
|
|
return true
|
|
}
|
|
|
|
override var canBecomeMain: Bool {
|
|
return true
|
|
}
|
|
|
|
override func sendEvent(_ event: NSEvent) {
|
|
// During screenshot selection, we want to capture ALL events
|
|
// and prevent them from reaching other windows
|
|
switch event.type {
|
|
case .leftMouseDown, .leftMouseUp, .leftMouseDragged,
|
|
.rightMouseDown, .rightMouseUp, .rightMouseDragged,
|
|
.otherMouseDown, .otherMouseUp, .otherMouseDragged,
|
|
.mouseMoved, .mouseEntered, .mouseExited,
|
|
.scrollWheel, .keyDown, .keyUp:
|
|
// Process the event normally through our view hierarchy
|
|
super.sendEvent(event)
|
|
// DO NOT pass the event to other windows - this prevents
|
|
// other windows from being dragged or interacted with
|
|
return
|
|
default:
|
|
// For other event types, allow normal processing
|
|
super.sendEvent(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Event Capture View
|
|
class EventCaptureView: NSView {
|
|
weak var screenshotApp: ScreenshotApp?
|
|
var shouldDisplayAllScreensActiveText: Bool = false // Nieuwe property
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
setupView()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setupView()
|
|
}
|
|
|
|
private func setupView() {
|
|
// This view will handle mouse events - no special setup needed
|
|
self.wantsLayer = true // Zorg ervoor dat de view een layer heeft voor efficiënt tekenen
|
|
}
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
super.draw(dirtyRect) // Belangrijk voor standaard teken gedrag
|
|
|
|
if shouldDisplayAllScreensActiveText {
|
|
// Teken "Alle Schermen Actief" tekst-overlay bovenaan het scherm van deze view
|
|
let instructionText = "All Screens Mode, Click to Capture, ESC to Cancel"
|
|
let attributes: [NSAttributedString.Key: Any] = [
|
|
.font: NSFont.systemFont(ofSize: 18, weight: .medium),
|
|
.foregroundColor: NSColor.white.withAlphaComponent(0.9),
|
|
.strokeColor: NSColor.black.withAlphaComponent(0.5), // Tekst outline
|
|
.strokeWidth: -2.0, // Negatief voor outline binnen de letters
|
|
.paragraphStyle: {
|
|
let style = NSMutableParagraphStyle()
|
|
style.alignment = .center
|
|
return style
|
|
}()
|
|
]
|
|
let attributedString = NSAttributedString(string: instructionText, attributes: attributes)
|
|
let textSize = attributedString.size()
|
|
|
|
// self.bounds is de grootte van deze EventCaptureView, die het hele scherm beslaat.
|
|
let viewBounds = self.bounds
|
|
|
|
let textRect = NSRect(x: (viewBounds.width - textSize.width) / 2,
|
|
y: viewBounds.height - textSize.height - 30, // 30px van de top van het scherm
|
|
width: textSize.width,
|
|
height: textSize.height)
|
|
|
|
let backgroundPadding: CGFloat = 10
|
|
let backgroundRect = NSRect(
|
|
x: textRect.origin.x - backgroundPadding,
|
|
y: textRect.origin.y - backgroundPadding,
|
|
width: textRect.width + (2 * backgroundPadding),
|
|
height: textRect.height + (2 * backgroundPadding)
|
|
)
|
|
let BORDER_RADIUS: CGFloat = 10
|
|
let textBackgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: BORDER_RADIUS, yRadius: BORDER_RADIUS)
|
|
NSColor.black.withAlphaComponent(0.4).setFill()
|
|
textBackgroundPath.fill()
|
|
|
|
attributedString.draw(in: textRect)
|
|
// NSLog("🎨 EventCaptureView: Drew 'Alle Schermen Actief' text.")
|
|
}
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
guard let app = screenshotApp else {
|
|
// Even if app is nil, consume the event to prevent it from reaching other windows
|
|
return
|
|
}
|
|
|
|
if app.isMultiMonitorSelectionActive && !app.isDragging {
|
|
let globalLocation = NSEvent.mouseLocation
|
|
print("🎯 Event capture - starting selection at: \(globalLocation)")
|
|
|
|
// SET MOUSE TRACKING VARIABLES FOR SINGLE CLICK DETECTION
|
|
let allScreenModifier: UInt = (1 << 0) // Command key
|
|
app.isAllScreenModifierPressed = app.isModifierPressed(event.modifierFlags, modifier: allScreenModifier)
|
|
app.mouseDownLocation = globalLocation
|
|
app.mouseDownTime = CACurrentMediaTime()
|
|
app.hasMouseMoved = false
|
|
|
|
|
|
|
|
// Hide crosshairs during drag to prevent stuck crosshair issue
|
|
hideCrosshairs()
|
|
|
|
app.startDragSelection(at: globalLocation)
|
|
}
|
|
// DO NOT call super.mouseDown - this prevents the event from propagating
|
|
}
|
|
|
|
override func mouseDragged(with event: NSEvent) {
|
|
guard let app = screenshotApp else {
|
|
// Even if app is nil, consume the event to prevent it from reaching other windows
|
|
return
|
|
}
|
|
|
|
if app.isMultiMonitorSelectionActive && app.isDragging {
|
|
let globalLocation = NSEvent.mouseLocation
|
|
app.hasMouseMoved = true // MARK THAT MOUSE HAS MOVED
|
|
app.updateDragSelection(to: globalLocation)
|
|
}
|
|
// DO NOT call super.mouseDragged - this prevents the event from propagating
|
|
}
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
guard let app = screenshotApp else {
|
|
// Even if app is nil, consume the event to prevent it from reaching other windows
|
|
return
|
|
}
|
|
|
|
if app.isMultiMonitorSelectionActive && app.isDragging {
|
|
let globalLocation = NSEvent.mouseLocation
|
|
|
|
// CHECK FOR SINGLE CLICK FIRST
|
|
let timeSinceMouseDown = CACurrentMediaTime() - app.mouseDownTime
|
|
let distanceMoved = sqrt(pow(globalLocation.x - app.mouseDownLocation.x, 2) + pow(globalLocation.y - app.mouseDownLocation.y, 2))
|
|
|
|
print("🎯 EventCaptureView - mouse up at: \(globalLocation)")
|
|
print("🎯 Time since down: \(timeSinceMouseDown)s, distance: \(distanceMoved)px, moved: \(app.hasMouseMoved), allScreenModifierPressed: \(app.isAllScreenModifierPressed), allScreensToggled: \(app.isAllScreensCaptureToggledOn)")
|
|
|
|
|
|
|
|
// 1. NIEUW: Als "alle schermen" modus is GETOGGLED AAN, en het is een klik (geen drag)
|
|
if app.isAllScreensCaptureToggledOn && !app.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 {
|
|
print("🎯 Click detected (toggle ON) - capturing all screens")
|
|
app.deactivateMultiMonitorSelection()
|
|
app.captureAllScreens() // captureAllScreens reset zelf de toggle
|
|
app.resetTrackingVariables()
|
|
return
|
|
}
|
|
|
|
// 2. Prioriteit: Single click voor huidige scherm (als toggle NIET aanstaat)
|
|
if !app.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 && !app.isAllScreenModifierPressed {
|
|
print("🎯 Single click detected (toggle OFF) - capturing current screen")
|
|
app.deactivateMultiMonitorSelection()
|
|
app.captureCurrentScreen(at: globalLocation)
|
|
app.resetTrackingVariables() // RESET for clean state
|
|
return
|
|
}
|
|
|
|
// 4. Anders, normale selectie beëindigen
|
|
print("🎯 Ending drag selection normally")
|
|
app.endDragSelection(at: globalLocation)
|
|
|
|
// Show crosshairs again after drag ends
|
|
showCrosshairs()
|
|
}
|
|
// DO NOT call super.mouseUp - this prevents the event from propagating
|
|
}
|
|
|
|
override func rightMouseDown(with event: NSEvent) {
|
|
// Handle right-click to cancel selection
|
|
guard let app = screenshotApp else { return }
|
|
|
|
if app.isMultiMonitorSelectionActive {
|
|
print("🎯 Event capture - right-click, canceling selection")
|
|
app.cancelMultiMonitorSelection()
|
|
}
|
|
// DO NOT call super.rightMouseDown - this prevents the event from propagating
|
|
}
|
|
|
|
override func rightMouseUp(with event: NSEvent) {
|
|
// Consume right mouse up events during selection
|
|
// DO NOT call super.rightMouseUp - this prevents the event from propagating
|
|
}
|
|
|
|
override func otherMouseDown(with event: NSEvent) {
|
|
// Consume other mouse button events during selection
|
|
// DO NOT call super.otherMouseDown - this prevents the event from propagating
|
|
}
|
|
|
|
override func otherMouseUp(with event: NSEvent) {
|
|
// Consume other mouse button events during selection
|
|
// DO NOT call super.otherMouseUp - this prevents the event from propagating
|
|
}
|
|
|
|
override func scrollWheel(with event: NSEvent) {
|
|
// Consume scroll wheel events during selection
|
|
// DO NOT call super.scrollWheel - this prevents the event from propagating
|
|
}
|
|
|
|
override func keyDown(with event: NSEvent) {
|
|
// Only handle ESC key if multi-monitor selection is active
|
|
if event.keyCode == 53 && screenshotApp?.isMultiMonitorSelectionActive == true { // ESC key
|
|
screenshotApp?.cancelMultiMonitorSelection()
|
|
return // Consume event
|
|
}
|
|
|
|
// The Command-key handling has been moved to the flagsChanged handler
|
|
// in MultiMonitorSystem.swift to implement the toggle correctly.
|
|
|
|
super.keyDown(with: event)
|
|
}
|
|
|
|
override func keyUp(with event: NSEvent) {
|
|
// ... existing code ...
|
|
super.keyUp(with: event)
|
|
}
|
|
|
|
override var acceptsFirstResponder: Bool {
|
|
return true
|
|
}
|
|
|
|
override func becomeFirstResponder() -> Bool {
|
|
print("⌨️ EventCaptureView becoming first responder")
|
|
return super.becomeFirstResponder()
|
|
}
|
|
|
|
private func hideCrosshairs() {
|
|
// Find all crosshair views in the window hierarchy and hide them
|
|
if let window = self.window {
|
|
findAndHideCrosshairs(in: window.contentView)
|
|
}
|
|
}
|
|
|
|
private func showCrosshairs() {
|
|
// Find all crosshair views in the window hierarchy and show them
|
|
if let window = self.window {
|
|
findAndShowCrosshairs(in: window.contentView)
|
|
}
|
|
}
|
|
|
|
private func findAndHideCrosshairs(in view: NSView?) {
|
|
guard let view = view else { return }
|
|
|
|
for subview in view.subviews {
|
|
if let crosshair = subview as? CrosshairCursorView {
|
|
crosshair.stopTracking()
|
|
}
|
|
findAndHideCrosshairs(in: subview)
|
|
}
|
|
}
|
|
|
|
private func findAndShowCrosshairs(in view: NSView?) {
|
|
guard let view = view else { return }
|
|
|
|
for subview in view.subviews {
|
|
if let crosshair = subview as? CrosshairCursorView {
|
|
crosshair.isHidden = false
|
|
crosshair.startTracking()
|
|
}
|
|
findAndShowCrosshairs(in: subview)
|
|
}
|
|
}
|
|
} |