🚀 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.
213 lines
8.4 KiB
Swift
213 lines
8.4 KiB
Swift
import AppKit
|
|
|
|
// MARK: - DraggableImageView Protocol
|
|
protocol DraggableImageViewClickHandler: AnyObject {
|
|
func thumbnailWasClicked(image: NSImage)
|
|
}
|
|
|
|
// MARK: - DraggableImageView
|
|
class DraggableImageView: NSImageView {
|
|
var onDragStart: (() -> Void)?
|
|
weak var appDelegate: ScreenshotApp?
|
|
private var mouseDownEvent: NSEvent?
|
|
private let dragThreshold: CGFloat = 3.0
|
|
private var isPerformingDrag: Bool = false
|
|
|
|
// 🎨 NEW: Track the file URL for the current image (for BGR mode)
|
|
var imageURL: URL?
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
self.imageScaling = .scaleProportionallyUpOrDown
|
|
self.imageAlignment = .alignCenter
|
|
self.animates = true
|
|
self.imageFrameStyle = .none
|
|
self.registerForDraggedTypes([.fileURL, .URL, .tiff, .png])
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
self.imageScaling = .scaleProportionallyUpOrDown
|
|
self.imageAlignment = .alignCenter
|
|
self.animates = true
|
|
self.imageFrameStyle = .none
|
|
self.registerForDraggedTypes([.fileURL, .URL, .tiff, .png])
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
self.mouseDownEvent = event
|
|
self.isPerformingDrag = false
|
|
}
|
|
|
|
override func mouseDragged(with event: NSEvent) {
|
|
guard let mouseDownEvent = self.mouseDownEvent else {
|
|
super.mouseDragged(with: event)
|
|
return
|
|
}
|
|
|
|
if !isPerformingDrag {
|
|
let dragThreshold: CGFloat = 3.0
|
|
let deltaX = abs(event.locationInWindow.x - mouseDownEvent.locationInWindow.x)
|
|
let deltaY = abs(event.locationInWindow.y - mouseDownEvent.locationInWindow.y)
|
|
|
|
if deltaX > dragThreshold || deltaY > dragThreshold {
|
|
isPerformingDrag = true
|
|
self.mouseDownEvent = nil
|
|
|
|
guard let unwrappedAppDelegate = appDelegate else {
|
|
isPerformingDrag = false
|
|
return
|
|
}
|
|
|
|
// 🎨 FIXED: Check for available URL (BGR mode or normal mode) before proceeding
|
|
let sourceURLForDrag = self.imageURL ?? unwrappedAppDelegate.tempURL
|
|
|
|
guard let finalSourceURL = sourceURLForDrag else {
|
|
print("❌ DraggableImageView: No valid URL available for dragging (imageURL: \(imageURL?.path ?? "nil"), tempURL: \(unwrappedAppDelegate.tempURL?.path ?? "nil"))")
|
|
isPerformingDrag = false
|
|
return
|
|
}
|
|
|
|
print("🎯 DraggableImageView: Starting drag with URL: \(finalSourceURL.path)")
|
|
|
|
if let preview = unwrappedAppDelegate.activePreviewWindow, preview.isVisible {
|
|
preview.orderOut(nil as Any?)
|
|
}
|
|
unwrappedAppDelegate.gridViewManager?.showGrid(previewFrame: self.window?.frame)
|
|
|
|
// NIEUW: Start drag session voor proximity monitoring
|
|
unwrappedAppDelegate.gridViewManager?.startDragSession()
|
|
|
|
let fileItem = NSDraggingItem(pasteboardWriter: finalSourceURL as NSURL)
|
|
|
|
if let imageToDrag = self.image {
|
|
let fullFrame = convert(bounds, to: nil)
|
|
|
|
let scale: CGFloat = 0.05
|
|
let yOffset: CGFloat = 30
|
|
|
|
let scaledFrame = NSRect(
|
|
x: fullFrame.midX - fullFrame.width * scale / 2,
|
|
y: fullFrame.midY - fullFrame.height * scale / 2 - yOffset,
|
|
width: fullFrame.width * scale,
|
|
height: fullFrame.height * scale
|
|
)
|
|
fileItem.setDraggingFrame(scaledFrame, contents: imageToDrag)
|
|
}
|
|
let items: [NSDraggingItem] = [fileItem]
|
|
beginDraggingSession(with: items, event: event, source: self)
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
if !isPerformingDrag {
|
|
if let image = self.image, let appDelegate = self.appDelegate {
|
|
appDelegate.thumbnailWasClicked(image: image)
|
|
}
|
|
super.mouseUp(with: event)
|
|
}
|
|
self.mouseDownEvent = nil
|
|
self.isPerformingDrag = false
|
|
}
|
|
}
|
|
|
|
// MARK: - NSDraggingSource
|
|
extension DraggableImageView: NSDraggingSource {
|
|
func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
|
|
return .copy
|
|
}
|
|
|
|
func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) {
|
|
// Drag session beginning
|
|
}
|
|
|
|
func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) {
|
|
guard let appDel = self.appDelegate,
|
|
let gridManager = appDel.gridViewManager,
|
|
let gridWindow = gridManager.gridWindow else { return }
|
|
|
|
let gridFrame = gridWindow.frame
|
|
|
|
let distanceToGrid = min(
|
|
abs(screenPoint.x - gridFrame.minX),
|
|
abs(screenPoint.x - gridFrame.maxX)
|
|
)
|
|
|
|
// Update visual feedback based on proximity
|
|
if Int(distanceToGrid) % 50 == 0 {
|
|
let minScale: CGFloat = 0.05
|
|
let maxScale: CGFloat = 0.35
|
|
let maxDistance: CGFloat = 300
|
|
|
|
if distanceToGrid < maxDistance {
|
|
let progress = distanceToGrid / maxDistance
|
|
_ = minScale + (progress * (maxScale - minScale))
|
|
}
|
|
}
|
|
}
|
|
|
|
func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
|
|
isPerformingDrag = false
|
|
|
|
// NIEUW: Stop drag session direct na drag end
|
|
appDelegate?.gridViewManager?.stopDragSession()
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self, let appDel = self.appDelegate else {
|
|
return
|
|
}
|
|
|
|
let didDropOnGridAction = appDel.didGridHandleDrop
|
|
let didDropOnStashGridAction = appDel.didStashGridHandleDrop
|
|
let didDropOnAnyGridAction = didDropOnGridAction || didDropOnStashGridAction
|
|
let closeAfterDragSetting = SettingsManager.shared.closeAfterDrag
|
|
|
|
if !didDropOnAnyGridAction && appDel.gridViewManager?.gridWindow != nil {
|
|
appDel.gridViewManager?.hideGrid(monitorForReappear: false)
|
|
}
|
|
|
|
if didDropOnAnyGridAction {
|
|
// Drop handled by grid action (main or stash). Preview management deferred to grid action handler.
|
|
print("🔄 DraggableImageView: Grid action detected (main: \(didDropOnGridAction), stash: \(didDropOnStashGridAction))")
|
|
|
|
// 🔧 CRITICAL FIX: Handle ALL grid actions (both main and stash) with closeAfterDrag setting
|
|
print("🔄 Grid action completed - applying closeAfterDrag setting: \(closeAfterDragSetting)")
|
|
if closeAfterDragSetting {
|
|
print("🔄 Closing thumbnail after grid action due to closeAfterDrag setting")
|
|
appDel.closePreviewWithAnimation(immediate: false, preserveTempFile: false)
|
|
} else {
|
|
print("🔄 Keeping thumbnail visible after grid action (closeAfterDrag is OFF)")
|
|
appDel.ensurePreviewVisible()
|
|
}
|
|
} else {
|
|
if operation != [] {
|
|
if closeAfterDragSetting {
|
|
appDel.closePreviewWithAnimation(immediate: true)
|
|
} else {
|
|
appDel.ensurePreviewVisible()
|
|
}
|
|
} else {
|
|
appDel.ensurePreviewVisible()
|
|
}
|
|
}
|
|
|
|
// Reset both flags
|
|
appDel.didGridHandleDrop = false
|
|
appDel.didStashGridHandleDrop = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - NSImage Extension for PNG Data
|
|
extension NSImage {
|
|
func pngData() -> Data? {
|
|
guard let tiffData = self.tiffRepresentation,
|
|
let bitmapRep = NSBitmapImageRep(data: tiffData) else {
|
|
return nil
|
|
}
|
|
return bitmapRep.representation(using: .png, properties: [:])
|
|
}
|
|
} |