🚀 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.
1140 lines
53 KiB
Swift
1140 lines
53 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers // NIEUW: Voor UTType support
|
|
|
|
// NIEUW: Wrapper class voor PNG data dragging naar browsers
|
|
class PNGDataWrapper: NSObject, NSPasteboardWriting {
|
|
let data: Data
|
|
|
|
init(data: Data) {
|
|
self.data = data
|
|
}
|
|
|
|
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
|
// Gereduceerd tot pure image data types + generiek public.data
|
|
return [
|
|
.png, // Native PNG type
|
|
NSPasteboard.PasteboardType("public.png"), // Standaard UTI voor PNG
|
|
.tiff, // TIFF fallback
|
|
// NSPasteboard.PasteboardType("public.tiff"), // Optioneel: UTI voor TIFF
|
|
NSPasteboard.PasteboardType.URL // Voor het plakken als URL in sommige apps (bijv. Notes)
|
|
// VERWIJDERD: .fileURL and NSFilenamesPboardType om conflicten te voorkomen
|
|
// VERWIJDERD: NSPasteboard.PasteboardType("public.file-url")
|
|
]
|
|
}
|
|
|
|
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
|
|
switch type {
|
|
case .png, NSPasteboard.PasteboardType("public.png"):
|
|
print("🎁 PNGDataWrapper: Providing PNG data")
|
|
return data
|
|
case .tiff:
|
|
print("🎁 PNGDataWrapper: Providing TIFF data (via PNG data)")
|
|
return data // PNG data kan vaak als TIFF gelezen worden
|
|
case .URL: // Case voor NSPasteboard.PasteboardType.URL
|
|
print("🎁 PNGDataWrapper: Providing data as a base64 data URL string")
|
|
let base64String = data.base64EncodedString()
|
|
// Maak een data URL. Zorg ervoor dat dit formaat correct is voor de doelapplicaties.
|
|
// Dit is een voorbeeld; niet alle apps ondersteunen het plakken van data-URLs.
|
|
return "data:image/png;base64,\(base64String)"
|
|
default:
|
|
print("🎁 PNGDataWrapper: Type '\(type)' niet ondersteund voor property list")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Stash Grid Delegate Protocol
|
|
protocol StashGridDelegate: AnyObject {
|
|
func stashGridDidDropImage(at cellIndex: Int, stashItem: IdentifiableImage, imageURL: URL)
|
|
}
|
|
|
|
// MARK: - Stash Grid Manager
|
|
class StashGridManager {
|
|
var gridWindow: StashGridWindow?
|
|
var delegate: StashGridDelegate?
|
|
|
|
// GEFIXT: Explicit initialization to prevent state inconsistencies
|
|
private var proximityTimer: Timer?
|
|
private var isDragSessionActive: Bool = false
|
|
private var lastStashFrame: NSRect?
|
|
|
|
// NIEUW: Initializer om state correct te resetten
|
|
init() {
|
|
// CRITICAL: Reset all state to ensure consistent behavior between swift run and .app
|
|
self.isDragSessionActive = false
|
|
self.lastStashFrame = nil
|
|
print("🔶 STASH DEBUG: StashGridManager initialized with clean state - isDragSessionActive: \(self.isDragSessionActive)")
|
|
}
|
|
|
|
func showGrid(previewFrame: NSRect?, for stashItem: IdentifiableImage, on originatingScreen: NSScreen) {
|
|
print("🔶 STASH DEBUG: StashGridManager: showGrid START for STASH item \(stashItem.id) on screen \(originatingScreen.localizedName)")
|
|
print("🔶 STASH DEBUG: Delegate is \(self.delegate == nil ? "NIL" : "SET") when showing grid.")
|
|
print("🔶 STASH DEBUG: PreviewFrame: \(String(describing: previewFrame))")
|
|
|
|
// NIEUW: Stop proximity timer when grid is shown
|
|
proximityTimer?.invalidate()
|
|
proximityTimer = nil
|
|
|
|
if let appDelegate = NSApp.delegate as? ScreenshotApp,
|
|
let imageStore = appDelegate.activeStashImageStore { // Krijg referentie naar de image store
|
|
print("🔶 STASH DEBUG: imageStore.images.count BEFORE appDelegate calls: \(imageStore.images.count)")
|
|
print("🔶 STASH DEBUG: Stopping main grid proximity monitoring to prevent conflicts")
|
|
appDelegate.gridViewManager?.stopDragSession() // Kan UI updates triggeren
|
|
appDelegate.gridViewManager?.hideGrid() // Kan UI updates triggeren
|
|
print("🔶 STASH DEBUG: imageStore.images.count AFTER appDelegate calls: \(imageStore.images.count)")
|
|
} else {
|
|
print("🔶 STASH DEBUG: Kon appDelegate of activeStashImageStore niet verkrijgen voor pre/post logging.")
|
|
}
|
|
|
|
// Close existing stash grid if any
|
|
if let existingWindow = self.gridWindow {
|
|
print("🔶 STASH DEBUG: Closing existing stash grid window")
|
|
existingWindow.orderOut(nil)
|
|
self.gridWindow = nil
|
|
}
|
|
|
|
// AANGEPAST: Gebruik stash window frame voor positionering in plaats van preview frame
|
|
var effectivePreviewFrame: NSRect
|
|
|
|
if let providedFrame = previewFrame {
|
|
effectivePreviewFrame = providedFrame
|
|
print("🔶 STASH DEBUG: Using provided preview frame: \(providedFrame)")
|
|
} else {
|
|
// VERBETERD: Real-time stash window detection met betere logging
|
|
var stashWindowFrame: NSRect?
|
|
print("🔍 STASH DEBUG: FALLBACK - Scanning \(NSApp.windows.count) windows for stash window...")
|
|
|
|
for (index, window) in NSApp.windows.enumerated() {
|
|
let windowTitle = window.title
|
|
let hasStashInTitle = windowTitle.contains("Stash")
|
|
let hasIntegratedGallery = window.contentView?.className.contains("IntegratedGalleryView") == true
|
|
let windowFrame = window.frame
|
|
let isVisible = window.isVisible
|
|
|
|
print("🔍 STASH DEBUG: Window[\(index)]: title='\(windowTitle)', hasStash=\(hasStashInTitle), hasGallery=\(hasIntegratedGallery), visible=\(isVisible), frame=\(windowFrame)")
|
|
|
|
if (hasStashInTitle || hasIntegratedGallery) && isVisible {
|
|
stashWindowFrame = windowFrame
|
|
print("🔍 STASH DEBUG: ✅ FOUND active stash window - frame: \(windowFrame)")
|
|
break
|
|
}
|
|
}
|
|
|
|
if let foundFrame = stashWindowFrame {
|
|
effectivePreviewFrame = foundFrame
|
|
print("🔶 STASH DEBUG: Using FOUND real-time stash window frame: \(foundFrame)")
|
|
} else {
|
|
// LAATSTE FALLBACK: Gebruik screen center
|
|
let fallbackFrame = NSRect(x: originatingScreen.frame.midX - 100, y: originatingScreen.frame.midY - 50, width: 200, height: 100)
|
|
effectivePreviewFrame = fallbackFrame
|
|
print("⚠️ STASH DEBUG: NO stash window found - using screen center fallback: \(fallbackFrame)")
|
|
}
|
|
}
|
|
|
|
// NIEUW: Store stash frame for proximity monitoring
|
|
self.lastStashFrame = effectivePreviewFrame
|
|
|
|
print("🔶 STASH DEBUG: Using provided screen for stash grid: \(originatingScreen.localizedName)")
|
|
print("🔶 STASH DEBUG: Screen frame: \(originatingScreen.frame)")
|
|
print("🔶 STASH DEBUG: Screen visible frame: \(originatingScreen.visibleFrame)")
|
|
|
|
// AANGEPAST: IDENTIEKE POSITIONERING ALS MAIN GRID - ALTIJD RECHTS VAN SCHERM
|
|
let gridSize = NSSize(width: 176, height: 344) // 7 cells * 44 + spacing
|
|
let spacing: CGFloat = 16
|
|
|
|
// NIEUW: Gebruik ABSOLUTE positionering zoals main grid (GridWindow line 86-89)
|
|
var gridOrigin: NSPoint
|
|
let screenVisibleFrame = originatingScreen.visibleFrame
|
|
|
|
// HOOFDREGEL: ALTIJD RECHTS VAN SCHERM (blijft hetzelfde)
|
|
let gridX = screenVisibleFrame.maxX - gridSize.width - 10 // Identiek aan main grid + 10px padding
|
|
|
|
// NIEUW: VERTICALE POSITIONERING - BOVEN STASH WINDOW
|
|
var gridY: CGFloat
|
|
let gridAboveStash = effectivePreviewFrame.maxY + spacing // Boven stash window
|
|
|
|
// CHECK 1: Past grid boven stash binnen scherm?
|
|
if gridAboveStash + gridSize.height <= screenVisibleFrame.maxY {
|
|
gridY = gridAboveStash
|
|
print("🔶 STASH DEBUG: Positioning grid ABOVE stash window")
|
|
}
|
|
// FALLBACK: Grid onder stash window
|
|
else {
|
|
gridY = effectivePreviewFrame.minY - gridSize.height - spacing
|
|
print("🔶 STASH DEBUG: Not enough space above - positioning grid BELOW stash window")
|
|
|
|
// CHECK 2: Past grid onder stash binnen scherm?
|
|
if gridY < screenVisibleFrame.minY {
|
|
// Laatste redmiddel: naast stash window (midden)
|
|
gridY = effectivePreviewFrame.midY - gridSize.height / 2
|
|
print("🔶 STASH DEBUG: Not enough space below - positioning grid BESIDE stash window (centered)")
|
|
}
|
|
}
|
|
|
|
gridOrigin = NSPoint(x: gridX, y: gridY)
|
|
print("🔶 STASH DEBUG: Positioning grid at RIGHT edge of screen, smart vertical placement")
|
|
print("🔶 STASH DEBUG: Grid X position: screen.maxX(\(screenVisibleFrame.maxX)) - gridWidth(\(gridSize.width)) - 10px = \(gridX)")
|
|
print("🔶 STASH DEBUG: Grid Y position: \(gridY) (relative to stash at Y=\(effectivePreviewFrame.minY)-\(effectivePreviewFrame.maxY))")
|
|
|
|
// BACKUP POSITIONERING: Als er geen effectivePreviewFrame is, centreer alles
|
|
if effectivePreviewFrame.width == 0 || effectivePreviewFrame.height == 0 {
|
|
gridOrigin.y = screenVisibleFrame.midY - gridSize.height / 2
|
|
print("🔶 STASH DEBUG: No valid stash window frame, centering grid vertically on screen")
|
|
}
|
|
|
|
// Bounds checking (behoud de bestaande logica) - EXTRA VEILIGHEID
|
|
gridOrigin.x = max(screenVisibleFrame.minX, min(gridOrigin.x, screenVisibleFrame.maxX - gridSize.width))
|
|
gridOrigin.y = max(screenVisibleFrame.minY, min(gridOrigin.y, screenVisibleFrame.maxY - gridSize.height))
|
|
|
|
let gridRect = NSRect(origin: gridOrigin, size: gridSize)
|
|
print("🔶 STASH DEBUG: Final calculated grid rect: \(gridRect)")
|
|
|
|
print("🔶 STASH DEBUG: Creating NEW StashGridWindow (not regular GridWindow)")
|
|
let newGridWindow = StashGridWindow(contentRect: gridRect, stashItem: stashItem, manager: self)
|
|
|
|
// AANGEPAST: Assign the grid window
|
|
self.gridWindow = newGridWindow
|
|
|
|
guard let assignedGridWindow = self.gridWindow else {
|
|
print("❌ STASH DEBUG: ERROR - Failed to assign StashGridWindow!")
|
|
return
|
|
}
|
|
|
|
print("🔶 STASH DEBUG: StashGridWindow created successfully")
|
|
print("🔶 STASH DEBUG: Grid window frame: \(assignedGridWindow.frame)")
|
|
print("🔶 STASH DEBUG: Grid window level: \(assignedGridWindow.level)")
|
|
|
|
// Setup window properties
|
|
assignedGridWindow.alphaValue = 0
|
|
print("🔶 STASH DEBUG: Set initial alpha to 0")
|
|
|
|
assignedGridWindow.makeKeyAndOrderFront(nil)
|
|
print("🔶 STASH DEBUG: Called makeKeyAndOrderFront")
|
|
|
|
assignedGridWindow.orderFrontRegardless()
|
|
print("🔶 STASH DEBUG: Called orderFrontRegardless")
|
|
|
|
assignedGridWindow.isInitialFadingIn = true
|
|
print("🔶 STASH DEBUG: Set isInitialFadingIn = true")
|
|
|
|
// NIEUW: Make window first responder voor key events (ESC)
|
|
_ = assignedGridWindow.becomeFirstResponder()
|
|
print("🔶 STASH DEBUG: Made window first responder for key events")
|
|
|
|
print("🔶 STASH DEBUG: Grid window isVisible: \(assignedGridWindow.isVisible)")
|
|
print("🔶 STASH DEBUG: Grid window alphaValue: \(assignedGridWindow.alphaValue)")
|
|
|
|
print("🔶 STASH DEBUG: Animating stash grid appearance")
|
|
NSAnimationContext.runAnimationGroup { ctx in
|
|
ctx.duration = 0.2
|
|
assignedGridWindow.animator().alphaValue = 1
|
|
print("🔶 STASH DEBUG: Animation started - target alpha: 1.0")
|
|
} completionHandler: {
|
|
assignedGridWindow.isInitialFadingIn = false
|
|
print("🔶 STASH DEBUG: Stash grid appearance animation complete")
|
|
print("🔶 STASH DEBUG: Final grid window alphaValue: \(assignedGridWindow.alphaValue)")
|
|
print("🔶 STASH DEBUG: Final grid window isVisible: \(assignedGridWindow.isVisible)")
|
|
}
|
|
}
|
|
|
|
func hideGrid() {
|
|
print("🔶 STASH DEBUG: StashGridManager: hideGrid called for STASH")
|
|
guard let window = gridWindow else {
|
|
print("🔶 STASH DEBUG: No stash grid window to hide")
|
|
return
|
|
}
|
|
|
|
// NIEUW: Stop proximity timer when manually hiding
|
|
proximityTimer?.invalidate()
|
|
proximityTimer = nil
|
|
|
|
print("🔶 STASH DEBUG: Hiding stash grid window")
|
|
NSAnimationContext.runAnimationGroup({ ctx in
|
|
ctx.duration = 0.2
|
|
window.animator().alphaValue = 0
|
|
}, completionHandler: { [weak self] in
|
|
print("🔶 STASH DEBUG: Stash grid hide animation complete")
|
|
window.orderOut(nil)
|
|
if self?.gridWindow === window {
|
|
self?.gridWindow = nil
|
|
}
|
|
})
|
|
}
|
|
|
|
// NIEUW: Drag session management (identiek aan main grid)
|
|
func startDragSession() {
|
|
print("🔶 STASH DEBUG: Drag session started - enabling proximity monitoring")
|
|
isDragSessionActive = true
|
|
}
|
|
|
|
func stopDragSession() {
|
|
print("🔶 STASH DEBUG: Drag session ended - disabling proximity monitoring")
|
|
isDragSessionActive = false
|
|
|
|
// GEFIXT: Alleen start proximity monitoring als er nog een actieve drag zou zijn
|
|
// Dit voorkomt dat de grid blijft hangen na een drag
|
|
// (Main grid heeft dezelfde logica)
|
|
print("🔶 STASH DEBUG: Drag session stopped - grid should hide soon unless mouse stays near")
|
|
|
|
// NIEUW: Start een eenvoudige timer om de grid te verbergen na een korte delay
|
|
// Dit geeft de gebruiker de kans om nog te interacteren, maar zorgt dat de grid verdwijnt
|
|
if gridWindow != nil {
|
|
print("🔶 STASH DEBUG: Starting auto-hide timer for stash grid")
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
|
// Alleen verbergen als er geen actieve drag is
|
|
if self?.isDragSessionActive == false {
|
|
print("🔶 STASH DEBUG: Auto-hiding stash grid after drag completion")
|
|
self?.hideGrid()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GEFIXT: Proximity monitoring alleen starten wanneer expliciet gevraagd
|
|
private func startProximityMonitoring() {
|
|
// KRITIEK: Check of er een actieve drag sessie is
|
|
// Anders start de monitoring niet (consistent met main grid)
|
|
guard isDragSessionActive else {
|
|
print("🔶 STASH DEBUG: Skipping proximity monitoring - no active drag session")
|
|
return
|
|
}
|
|
|
|
guard let targetFrame = lastStashFrame else {
|
|
print("🔶 STASH DEBUG: No stash frame - skipping proximity monitoring")
|
|
return
|
|
}
|
|
|
|
// Stop existing timer
|
|
proximityTimer?.invalidate()
|
|
|
|
print("🔶 STASH DEBUG: Starting proximity monitoring (only during active drag)")
|
|
proximityTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in
|
|
self?.evaluateMouseProximity(to: targetFrame)
|
|
}
|
|
RunLoop.main.add(proximityTimer!, forMode: .common)
|
|
}
|
|
|
|
private func evaluateMouseProximity(to frame: NSRect) {
|
|
// GEFIXT: Check of er een actieve drag sessie is
|
|
guard isDragSessionActive else {
|
|
print("🔶 STASH DEBUG: Stopping proximity evaluation - no active drag session")
|
|
proximityTimer?.invalidate()
|
|
proximityTimer = nil
|
|
// NIEUW: Hide grid when no active drag session
|
|
hideGrid()
|
|
return
|
|
}
|
|
|
|
// VERBETERD: Gebruik real-time stash window frame in plaats van cached frame
|
|
var currentStashFrame = frame // Start met cached frame
|
|
|
|
// NIEUW: Probeer real-time stash window frame te verkrijgen
|
|
for window in NSApp.windows {
|
|
let windowTitle = window.title
|
|
if (windowTitle.contains("Stash") || window.contentView?.className.contains("IntegratedGalleryView") == true) && window.isVisible {
|
|
currentStashFrame = window.frame
|
|
// print("🔶 PROXIMITY DEBUG: Using real-time stash frame: \(currentStashFrame)")
|
|
break
|
|
}
|
|
}
|
|
|
|
// Bereken een vergrote zone (200 px marge) rondom de REAL-TIME stash window
|
|
let expansion: CGFloat = 200
|
|
let enlarged = currentStashFrame.insetBy(dx: -expansion, dy: -expansion)
|
|
|
|
let currentLoc = NSEvent.mouseLocation
|
|
if !enlarged.contains(currentLoc) {
|
|
// Mouse is ver weg - verberg grid
|
|
print("🔶 STASH DEBUG: Mouse far from stash window - hiding grid")
|
|
proximityTimer?.invalidate()
|
|
proximityTimer = nil
|
|
hideGrid()
|
|
}
|
|
// Als mouse nog in de buurt is, blijft grid zichtbaar
|
|
}
|
|
}
|
|
|
|
// MARK: - Stash Grid Window
|
|
class StashGridWindow: NSWindow, NSDraggingDestination {
|
|
var cellViews: [GridCellView] = []
|
|
weak var manager: StashGridManager?
|
|
private var currentlyHighlightedCell: GridCellView?
|
|
private var fadeTimer: Timer?
|
|
private let fadeStart: CGFloat = 50
|
|
private let fadeEnd: CGFloat = 300
|
|
private let minAlpha: CGFloat = 0
|
|
var isInitialFadingIn: Bool = false
|
|
var isPerformingProgrammaticHide: Bool = false
|
|
|
|
// Store stash item reference for actions
|
|
let stashItem: IdentifiableImage
|
|
|
|
init(contentRect: NSRect, stashItem: IdentifiableImage, manager: StashGridManager) {
|
|
self.manager = manager
|
|
self.stashItem = stashItem
|
|
let settings = SettingsManager.shared
|
|
|
|
// IDENTIEK AAN MAIN GRID: Gebruik dezelfde settings en layout
|
|
var activeActions: [(index: Int, text: String)] = []
|
|
|
|
for (gridIndex, actionType) in settings.actionOrder.enumerated() {
|
|
let isEnabled: Bool
|
|
let displayText: String
|
|
|
|
switch actionType {
|
|
case .rename:
|
|
isEnabled = settings.isRenameActionEnabled
|
|
displayText = "Rename"
|
|
case .stash:
|
|
isEnabled = settings.isStashActionEnabled
|
|
displayText = "Stash"
|
|
case .ocr:
|
|
isEnabled = settings.isOCRActionEnabled
|
|
displayText = "Text"
|
|
case .clipboard:
|
|
isEnabled = settings.isClipboardActionEnabled
|
|
displayText = "Clipboard"
|
|
case .backgroundRemove:
|
|
isEnabled = settings.isBackgroundRemoveActionEnabled
|
|
displayText = "Remove BG"
|
|
case .cancel:
|
|
isEnabled = settings.isCancelActionEnabled
|
|
displayText = "Cancel"
|
|
case .remove:
|
|
isEnabled = settings.isRemoveActionEnabled
|
|
displayText = "Remove"
|
|
}
|
|
|
|
if isEnabled {
|
|
activeActions.append((gridIndex, displayText))
|
|
}
|
|
}
|
|
|
|
let numberOfActiveActions = activeActions.count
|
|
guard numberOfActiveActions > 0 else {
|
|
print("🔷 STASH DEBUG: No active actions - creating empty stash grid window")
|
|
super.init(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: false)
|
|
self.isOpaque = false
|
|
self.backgroundColor = .clear
|
|
self.ignoresMouseEvents = true
|
|
DispatchQueue.main.async { manager.hideGrid() }
|
|
return
|
|
}
|
|
|
|
print("🔷 STASH DEBUG: StashGridWindow init: Number of active actions = \(numberOfActiveActions)")
|
|
print("🔷 STASH DEBUG: Using same layout as main grid but for STASH actions")
|
|
|
|
// IDENTIEKE LAYOUT ALS MAIN GRID
|
|
let cellsPerRow = 1
|
|
let numberOfRows = Int(ceil(Double(numberOfActiveActions) / Double(cellsPerRow)))
|
|
let spacing: CGFloat = 8
|
|
let fixedCellHeight: CGFloat = 40.0
|
|
let fixedCellWidth: CGFloat = 160.0
|
|
|
|
let calculatedGridWidth = (fixedCellWidth * CGFloat(cellsPerRow)) + (spacing * (CGFloat(cellsPerRow) + 1))
|
|
let calculatedGridHeight = (fixedCellHeight * CGFloat(numberOfRows)) + (spacing * (CGFloat(numberOfRows) + 1))
|
|
|
|
var xPosition: CGFloat
|
|
var yPosition: CGFloat
|
|
|
|
xPosition = contentRect.origin.x
|
|
yPosition = contentRect.origin.y
|
|
|
|
let contentRect = NSRect(x: xPosition, y: yPosition, width: calculatedGridWidth, height: calculatedGridHeight)
|
|
|
|
print("🔷 STASH DEBUG: StashGridWindow init: Calculated contentRect = \(contentRect)")
|
|
|
|
super.init(contentRect: contentRect, styleMask: [.borderless], backing: .buffered, defer: false)
|
|
self.level = .floating + 10
|
|
print("🔷 STASH DEBUG: StashGridWindow init: Window level set to \(self.level.rawValue)")
|
|
self.isOpaque = false
|
|
self.backgroundColor = .clear
|
|
self.hasShadow = false
|
|
self.ignoresMouseEvents = false
|
|
self.acceptsMouseMovedEvents = true
|
|
|
|
// IDENTIEKE STYLING ALS MAIN GRID
|
|
let containerView = NSView(frame: NSRect(origin: .zero, size: contentRect.size))
|
|
containerView.wantsLayer = true
|
|
|
|
let barBlur1 = NSVisualEffectView()
|
|
barBlur1.blendingMode = .behindWindow
|
|
barBlur1.material = .hudWindow
|
|
barBlur1.state = .active
|
|
barBlur1.frame = containerView.bounds
|
|
barBlur1.autoresizingMask = [.width, .height]
|
|
|
|
let barBlur2 = NSVisualEffectView()
|
|
barBlur2.blendingMode = .behindWindow
|
|
barBlur2.material = .hudWindow
|
|
barBlur2.state = .active
|
|
barBlur2.alphaValue = 0.6
|
|
barBlur2.frame = containerView.bounds
|
|
barBlur2.autoresizingMask = [.width, .height]
|
|
|
|
containerView.addSubview(barBlur1, positioned: .below, relativeTo: nil)
|
|
containerView.addSubview(barBlur2, positioned: .below, relativeTo: nil)
|
|
|
|
containerView.layer?.cornerRadius = 12
|
|
containerView.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
containerView.layer?.masksToBounds = true
|
|
self.contentView = containerView
|
|
|
|
// Maak cellen
|
|
for (gridIndex, action) in activeActions.enumerated() {
|
|
let col = gridIndex % cellsPerRow
|
|
let row = gridIndex / cellsPerRow
|
|
|
|
let cellX = spacing + CGFloat(col) * (fixedCellWidth + spacing)
|
|
let cellY = calculatedGridHeight - spacing - CGFloat(row + 1) * fixedCellHeight - CGFloat(row) * spacing
|
|
|
|
let cellFrame = NSRect(x: cellX, y: cellY, width: fixedCellWidth, height: fixedCellHeight)
|
|
let cellView = GridCellView(frame: cellFrame, index: action.index, text: action.text)
|
|
containerView.addSubview(cellView)
|
|
cellViews.append(cellView)
|
|
}
|
|
|
|
print("🔷 STASH DEBUG: StashGridWindow init: Number of cellViews created = \(cellViews.count)")
|
|
|
|
registerForDraggedTypes([.fileURL])
|
|
|
|
// Start timer voor dynamische transparantie
|
|
fadeTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { [weak self] _ in
|
|
self?.updateAlphaBasedOnCursor()
|
|
})
|
|
|
|
print("🔷 STASH DEBUG: StashGridWindow init completed successfully")
|
|
}
|
|
|
|
deinit {
|
|
fadeTimer?.invalidate()
|
|
}
|
|
|
|
private func updateAlphaBasedOnCursor() {
|
|
guard !isPerformingProgrammaticHide else { return }
|
|
guard !isInitialFadingIn else { return }
|
|
guard let screenPoint = NSEvent.mouseLocation as NSPoint? else { return }
|
|
let windowFrame = self.frame
|
|
|
|
let distance: CGFloat
|
|
if windowFrame.contains(screenPoint) {
|
|
distance = 0
|
|
} else {
|
|
let dx: CGFloat
|
|
if screenPoint.x < windowFrame.minX { dx = windowFrame.minX - screenPoint.x }
|
|
else if screenPoint.x > windowFrame.maxX { dx = screenPoint.x - windowFrame.maxX } else { dx = 0 }
|
|
|
|
let dy: CGFloat
|
|
if screenPoint.y < windowFrame.minY { dy = windowFrame.minY - screenPoint.y }
|
|
else if screenPoint.y > windowFrame.maxY { dy = screenPoint.y - windowFrame.maxY } else { dy = 0 }
|
|
|
|
distance = sqrt(dx*dx + dy*dy)
|
|
}
|
|
|
|
let newAlpha: CGFloat
|
|
if distance <= fadeStart { newAlpha = 1 }
|
|
else if distance >= fadeEnd { newAlpha = minAlpha }
|
|
else {
|
|
let ratio = (distance - fadeStart) / (fadeEnd - fadeStart)
|
|
newAlpha = 1 - ratio * (1 - minAlpha)
|
|
}
|
|
|
|
if abs(self.alphaValue - newAlpha) > 0.01 {
|
|
self.alphaValue = newAlpha
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
// MARK: - NSDraggingDestination Methods
|
|
func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
|
return .copy
|
|
}
|
|
|
|
func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
|
|
let dropLocationInScreen = sender.draggingLocation
|
|
guard let dropLocationInContent = self.contentView?.convert(dropLocationInScreen, from: nil) else {
|
|
currentlyHighlightedCell?.setHighlighted(false)
|
|
currentlyHighlightedCell = nil
|
|
return []
|
|
}
|
|
|
|
var foundCell: GridCellView? = nil
|
|
for cell in cellViews {
|
|
if cell.frame.contains(dropLocationInContent) {
|
|
foundCell = cell
|
|
break
|
|
}
|
|
}
|
|
|
|
if currentlyHighlightedCell !== foundCell {
|
|
currentlyHighlightedCell?.setHighlighted(false)
|
|
currentlyHighlightedCell?.setHovered(false)
|
|
foundCell?.setHighlighted(true)
|
|
foundCell?.setHovered(true)
|
|
currentlyHighlightedCell = foundCell
|
|
}
|
|
|
|
return .copy
|
|
}
|
|
|
|
func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
|
guard let pasteboard = sender.draggingPasteboard.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray,
|
|
let path = pasteboard[0] as? String else {
|
|
self.manager?.hideGrid()
|
|
return false
|
|
}
|
|
let imageURL = URL(fileURLWithPath: path)
|
|
let dropLocationInContent = self.contentView?.convert(sender.draggingLocation, from: nil) ?? .zero
|
|
|
|
currentlyHighlightedCell?.setHighlighted(false)
|
|
currentlyHighlightedCell = nil
|
|
|
|
for cell in cellViews {
|
|
if cell.frame.contains(dropLocationInContent) {
|
|
if let currentManager = self.manager, let currentActionDelegate = currentManager.delegate {
|
|
print("✅ StashGridWindow: Detected drop on cell \(cell.index). Calling manager's delegate...")
|
|
currentActionDelegate.stashGridDidDropImage(at: cell.index, stashItem: stashItem, imageURL: imageURL)
|
|
return true
|
|
} else {
|
|
print("❌ StashGridWindow: Manager (\(self.manager == nil ? "NIL" : "SET")) or manager.delegate (\(self.manager?.delegate == nil ? "NIL" : "SET")) is nil for drop on cell \(cell.index)!")
|
|
if self.manager == nil {
|
|
print("❌ Detail: StashGridWindow.manager is nil.")
|
|
} else if self.manager?.delegate == nil {
|
|
print("❌ Detail: StashGridWindow.manager.delegate is nil.")
|
|
}
|
|
}
|
|
self.manager?.hideGrid()
|
|
return false
|
|
}
|
|
}
|
|
|
|
self.manager?.hideGrid()
|
|
return false
|
|
}
|
|
|
|
func draggingExited(_ sender: NSDraggingInfo?) {
|
|
currentlyHighlightedCell?.setHighlighted(false)
|
|
currentlyHighlightedCell?.setHovered(false)
|
|
currentlyHighlightedCell = nil
|
|
}
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
super.mouseUp(with: event)
|
|
|
|
// NIEUW: Hide grid on mouse up outside cells (same behavior as main grid)
|
|
let locationInWindow = event.locationInWindow
|
|
print("🔶 STASH DEBUG: StashGridWindow: mouseUp at \(locationInWindow)")
|
|
|
|
// Check if click was inside any cell
|
|
var clickWasInCell = false
|
|
for cellView in cellViews {
|
|
let cellFrame = cellView.frame
|
|
if cellFrame.contains(locationInWindow) {
|
|
clickWasInCell = true
|
|
print("🔶 STASH DEBUG: Click was inside cell at frame \(cellFrame)")
|
|
break
|
|
}
|
|
}
|
|
|
|
if !clickWasInCell {
|
|
print("🔶 STASH DEBUG: Click was OUTSIDE all cells - hiding stash grid")
|
|
// Close the grid when clicking outside cells
|
|
DispatchQueue.main.async {
|
|
if let manager = self.manager {
|
|
manager.hideGrid()
|
|
} else {
|
|
// Fallback: hide window directly
|
|
self.orderOut(nil)
|
|
}
|
|
}
|
|
} else {
|
|
print("🔶 STASH DEBUG: Click was inside cell - keeping grid open")
|
|
}
|
|
}
|
|
|
|
override func keyDown(with event: NSEvent) {
|
|
// NIEUW: Handle ESC key to close stash grid
|
|
if event.keyCode == 53 { // ESC key
|
|
print("🔶 STASH DEBUG: ESC key pressed - hiding stash grid")
|
|
DispatchQueue.main.async {
|
|
if let manager = self.manager {
|
|
manager.hideGrid()
|
|
} else {
|
|
// Fallback: hide window directly
|
|
self.orderOut(nil)
|
|
}
|
|
}
|
|
} else {
|
|
super.keyDown(with: event)
|
|
}
|
|
}
|
|
|
|
override var canBecomeKey: Bool {
|
|
return true // NIEUW: Nodig om key events te ontvangen
|
|
}
|
|
|
|
override var acceptsFirstResponder: Bool {
|
|
return true // NIEUW: Nodig om key events te ontvangen
|
|
}
|
|
}
|
|
|
|
// MARK: - Stash Draggable Image View Protocol
|
|
protocol StashDraggableImageViewDelegate: AnyObject {
|
|
func stashImageDidStartDrag(imageURL: URL, from view: StashDraggableNSImageView)
|
|
func stashImageDragDidEnd(imageURL: URL, operation: NSDragOperation, from view: StashDraggableNSImageView)
|
|
}
|
|
|
|
// MARK: - Stash Draggable Image View (NSView)
|
|
class StashDraggableNSImageView: NSImageView, NSFilePromiseProviderDelegate {
|
|
weak var delegate: StashDraggableImageViewDelegate?
|
|
var imageURL: URL?
|
|
var suggestedName: String?
|
|
var stashItem: IdentifiableImage?
|
|
private var mouseDownEvent: NSEvent?
|
|
private let dragThreshold: CGFloat = 3.0
|
|
private var isPerformingDrag: Bool = false
|
|
|
|
// NIEUW: Eigen grid manager voor stash items
|
|
var stashGridManager: StashGridManager?
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
setupView()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setupView()
|
|
}
|
|
|
|
private func setupView() {
|
|
self.imageScaling = .scaleProportionallyUpOrDown
|
|
self.imageAlignment = .alignCenter
|
|
self.animates = true
|
|
self.imageFrameStyle = .none
|
|
self.registerForDraggedTypes([.fileURL, .URL, .tiff, .png])
|
|
|
|
// Maak eigen grid manager aan
|
|
self.stashGridManager = StashGridManager()
|
|
}
|
|
|
|
// NIEUW: Setup delegate wanneer deze wordt geset
|
|
func setDelegate(_ delegate: StashDraggableImageViewDelegate) {
|
|
self.delegate = delegate
|
|
|
|
// VERBETERDE: Robuustere setup van grid manager connectie
|
|
if let stashDelegate = delegate as? StashDragDelegate {
|
|
// Zorg ervoor dat grid manager bestaat
|
|
if self.stashGridManager == nil {
|
|
self.stashGridManager = StashGridManager()
|
|
print("🔶 STASH DEBUG: Created new StashGridManager in setDelegate")
|
|
}
|
|
|
|
// Verbind delegate met grid manager
|
|
stashDelegate.setStashGridManager(self.stashGridManager)
|
|
}
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
self.mouseDownEvent = event
|
|
self.isPerformingDrag = false
|
|
print("🖱️ STASH DEBUG: StashDraggableNSImageView: mouseDown. Event stored.")
|
|
print("🖱️ STASH DEBUG: Mouse location in window: \(event.locationInWindow)")
|
|
print("🖱️ STASH DEBUG: imageURL: \(imageURL?.lastPathComponent ?? "nil")")
|
|
print("🖱️ STASH DEBUG: stashItem: \(stashItem?.id.uuidString.prefix(8) ?? "nil")")
|
|
print("🖱️ STASH DEBUG: stashGridManager: \(stashGridManager != nil ? "SET" : "NIL")")
|
|
}
|
|
|
|
override func mouseDragged(with event: NSEvent) {
|
|
guard let mouseDownEvent = self.mouseDownEvent else {
|
|
print("🖱️ STASH DEBUG: mouseDragged called but no mouseDownEvent - calling super")
|
|
super.mouseDragged(with: event)
|
|
return
|
|
}
|
|
|
|
print("🖱️ STASH DEBUG: mouseDragged called, isPerformingDrag: \(isPerformingDrag)")
|
|
|
|
if !isPerformingDrag {
|
|
let deltaX = abs(event.locationInWindow.x - mouseDownEvent.locationInWindow.x)
|
|
let deltaY = abs(event.locationInWindow.y - mouseDownEvent.locationInWindow.y)
|
|
|
|
print("🖱️ STASH DEBUG: Delta X: \(deltaX), Delta Y: \(deltaY), threshold: \(dragThreshold)")
|
|
|
|
if deltaX > dragThreshold || deltaY > dragThreshold {
|
|
print("🖱️ STASH DEBUG: ✅ THRESHOLD EXCEEDED - Starting drag process")
|
|
isPerformingDrag = true
|
|
self.mouseDownEvent = nil
|
|
|
|
guard let url = imageURL, let item = stashItem else {
|
|
print("❌ STASH DEBUG: StashDraggableNSImageView: mouseDragged - Missing imageURL or stashItem for drag.")
|
|
print("❌ STASH DEBUG: imageURL is nil: \(imageURL == nil), stashItem is nil: \(stashItem == nil)")
|
|
isPerformingDrag = false
|
|
return
|
|
}
|
|
|
|
print("🎯 STASH DEBUG: StashDraggableNSImageView: Starting STASH GRID (not main grid) for item \(item.id)")
|
|
|
|
// VERBETERDE: Extra safety check voor grid manager
|
|
if self.stashGridManager == nil {
|
|
print("⚠️ STASH DEBUG: Grid manager was nil, creating new one during drag")
|
|
self.stashGridManager = StashGridManager()
|
|
|
|
// Reconnect delegate if available
|
|
if let stashDelegate = delegate as? StashDragDelegate {
|
|
stashDelegate.setStashGridManager(self.stashGridManager)
|
|
print("🔶 STASH DEBUG: Re-connected delegate during drag")
|
|
}
|
|
} else {
|
|
print("✅ STASH DEBUG: Grid manager already exists")
|
|
}
|
|
|
|
// Start stash grid op de juiste screen
|
|
if let window = self.window, let screen = window.screen {
|
|
print("🖱️ STASH DEBUG: Window and screen available - \(screen.localizedName)")
|
|
print("🔥 STASH DEBUG: REAL-TIME stash window frame: \(window.frame)")
|
|
if let manager = stashGridManager {
|
|
print("🖱️ STASH DEBUG: About to call manager.showGrid with REAL-TIME frame...")
|
|
// CRITICAL FIX: Pass REAL-TIME stash window frame instead of nil
|
|
manager.showGrid(previewFrame: window.frame, for: item, on: screen)
|
|
print("🖱️ STASH DEBUG: manager.showGrid called successfully with dynamic frame")
|
|
|
|
// NIEUW: Start drag session voor proximity monitoring
|
|
manager.startDragSession()
|
|
print("🖱️ STASH DEBUG: Started stash drag session for proximity monitoring")
|
|
} else {
|
|
print("❌ STASH DEBUG: ERROR - stashGridManager is still nil after safety check!")
|
|
isPerformingDrag = false
|
|
return
|
|
}
|
|
} else {
|
|
print("❌ STASH DEBUG: ERROR - No window or screen available!")
|
|
print("❌ STASH DEBUG: self.window is nil: \(self.window == nil)")
|
|
if let w = self.window {
|
|
print("❌ STASH DEBUG: window.screen is nil: \(w.screen == nil)")
|
|
}
|
|
isPerformingDrag = false
|
|
return
|
|
}
|
|
|
|
// Notify delegate about drag start
|
|
print("🖱️ STASH DEBUG: Notifying delegate about drag start")
|
|
delegate?.stashImageDidStartDrag(imageURL: url, from: self)
|
|
|
|
// NIEUW: Voeg ook direct NSImage support toe (zoals hoofdthumbnail)
|
|
var draggingItems: [NSDraggingItem] = []
|
|
|
|
// CRITICAL FIX: Use only ONE multifunctional dragging item to prevent count badge
|
|
if let stashFileURL = self.stashItem?.fileURL, let nsImage = self.image {
|
|
print("🎯 STASH DEBUG: Creating SINGLE multifunctional dragging item WITH NSFilePromiseProvider")
|
|
|
|
// 🔥 ULTRA FIX: Add thumbnail scaling like hoofdgrid
|
|
let fullFrame = convert(bounds, to: nil)
|
|
let scale: CGFloat = 0.05 // 5% van originele grootte (exact zoals hoofdgrid)
|
|
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
|
|
)
|
|
|
|
// ULTRA FIX: Add NSFilePromiseProvider for proper filename handling in Finder
|
|
let filePromiseProvider = NSFilePromiseProvider(fileType: UTType.png.identifier, delegate: self)
|
|
filePromiseProvider.userInfo = [
|
|
"isFilePromise": true,
|
|
"fileURL": stashFileURL
|
|
]
|
|
|
|
// Create dragging item with file promise provider for Finder compatibility
|
|
let filePromiseDragItem = NSDraggingItem(pasteboardWriter: filePromiseProvider)
|
|
filePromiseDragItem.setDraggingFrame(scaledFrame, contents: nsImage) // 🔥 SCALED!
|
|
draggingItems.append(filePromiseDragItem)
|
|
|
|
// Create one comprehensive pasteboard item with ALL types for other apps
|
|
let comprehensiveItem = NSPasteboardItem()
|
|
|
|
// 1. File URL types
|
|
comprehensiveItem.setString(stashFileURL.absoluteString, forType: .fileURL)
|
|
comprehensiveItem.setString(stashFileURL.absoluteString, forType: NSPasteboard.PasteboardType("public.file-url"))
|
|
comprehensiveItem.setString(stashFileURL.path, forType: .string)
|
|
|
|
// 2. File path list
|
|
if let pathData = try? PropertyListSerialization.data(fromPropertyList: [stashFileURL.path], format: .binary, options: 0) {
|
|
comprehensiveItem.setData(pathData, forType: NSPasteboard.PasteboardType("NSFilenamesPboardType"))
|
|
}
|
|
|
|
// 3. Image data types
|
|
if let tiffRepresentation = nsImage.tiffRepresentation,
|
|
let bitmapImageRep = NSBitmapImageRep(data: tiffRepresentation),
|
|
let pngData = bitmapImageRep.representation(using: .png, properties: [:]) {
|
|
comprehensiveItem.setData(pngData, forType: .png)
|
|
comprehensiveItem.setData(pngData, forType: NSPasteboard.PasteboardType("public.png"))
|
|
comprehensiveItem.setData(tiffRepresentation, forType: .tiff)
|
|
|
|
// Base64 data URL for browser compatibility
|
|
let base64String = pngData.base64EncodedString()
|
|
comprehensiveItem.setString("data:image/png;base64,\(base64String)", forType: .URL)
|
|
}
|
|
|
|
// Create additional dragging item for compatibility
|
|
let compatibilityDragItem = NSDraggingItem(pasteboardWriter: comprehensiveItem)
|
|
compatibilityDragItem.setDraggingFrame(scaledFrame, contents: nsImage) // 🔥 SCALED!
|
|
draggingItems.append(compatibilityDragItem)
|
|
|
|
print("🎯 STASH DEBUG: Created comprehensive dragging session with NSFilePromiseProvider + compatibility item")
|
|
} else {
|
|
// Fallback to simple NSImage
|
|
if let nsImage = self.image {
|
|
// 🔥 ULTRA FIX: Add scaling for fallback too
|
|
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
|
|
)
|
|
|
|
let nsImageDragItem = NSDraggingItem(pasteboardWriter: nsImage)
|
|
nsImageDragItem.setDraggingFrame(scaledFrame, contents: nsImage) // 🔥 SCALED!
|
|
draggingItems.append(nsImageDragItem)
|
|
print("🎯 STASH DEBUG: Created fallback NSImage dragging item WITH SCALING")
|
|
}
|
|
}
|
|
|
|
print("🎯 STASH DEBUG: Total dragging items: \(draggingItems.count) (should be 1 - no badge)")
|
|
|
|
// FIXED: Veilige access naar mouseDownEvent met fallback
|
|
guard let mouseEvent = self.mouseDownEvent else {
|
|
print("❌ STASH DEBUG: mouseDownEvent is nil! Creating fallback event")
|
|
// Maak een fallback event met huidige mouse locatie
|
|
let currentLocation = NSEvent.mouseLocation
|
|
let windowLocation = self.window?.convertPoint(fromScreen: currentLocation) ?? NSPoint.zero
|
|
let localLocation = self.convert(windowLocation, from: nil)
|
|
|
|
// Create a minimal mouse event as fallback
|
|
let fallbackEvent = NSEvent.mouseEvent(
|
|
with: .leftMouseDown,
|
|
location: localLocation,
|
|
modifierFlags: [],
|
|
timestamp: ProcessInfo.processInfo.systemUptime,
|
|
windowNumber: self.window?.windowNumber ?? 0,
|
|
context: nil,
|
|
eventNumber: 0,
|
|
clickCount: 1,
|
|
pressure: 1.0
|
|
)
|
|
|
|
if let fallbackEvent = fallbackEvent {
|
|
let draggingSession = self.beginDraggingSession(with: draggingItems, event: fallbackEvent, source: self)
|
|
draggingSession.draggingFormation = .none
|
|
draggingSession.animatesToStartingPositionsOnCancelOrFail = true
|
|
print("🎯 STASH DEBUG: Used fallback event for dragging session")
|
|
} else {
|
|
print("❌ STASH DEBUG: Failed to create fallback event - aborting drag")
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
let draggingSession = self.beginDraggingSession(with: draggingItems, event: mouseEvent, source: self)
|
|
draggingSession.draggingFormation = .none
|
|
draggingSession.animatesToStartingPositionsOnCancelOrFail = true
|
|
|
|
print("🎯 STASH DEBUG: Stash drag session beginning at (\(NSEvent.mouseLocation.x), \(NSEvent.mouseLocation.y))")
|
|
print("🎯 STASH DEBUG: Dragging session configured with \(draggingItems.count) items treated as single unit")
|
|
} else {
|
|
print("🖱️ STASH DEBUG: ⏸️ Drag threshold not exceeded yet")
|
|
}
|
|
} else {
|
|
print("🖱️ STASH DEBUG: Already performing drag, ignoring mouseDragged")
|
|
}
|
|
}
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
print("🖱️ STASH DEBUG: StashDraggableNSImageView: mouseUp called, isPerformingDrag: \(isPerformingDrag)")
|
|
|
|
// VERBETERD: Alleen mouseDownEvent clearen als we niet aan het draggen zijn
|
|
if !isPerformingDrag {
|
|
self.mouseDownEvent = nil
|
|
print("🖱️ STASH DEBUG: Cleared mouseDownEvent (not dragging)")
|
|
} else {
|
|
print("🖱️ STASH DEBUG: Keeping mouseDownEvent during drag operation")
|
|
}
|
|
}
|
|
|
|
// MARK: - NSFilePromiseProviderDelegate
|
|
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
|
|
// Check if this is a file URL promise
|
|
if let userInfo = filePromiseProvider.userInfo as? [String: Any],
|
|
let isFilePromise = userInfo["isFilePromise"] as? Bool,
|
|
isFilePromise,
|
|
let fileURL = userInfo["fileURL"] as? URL {
|
|
print("🔄 filePromiseProvider (file URL) asking for filename: '\(fileURL.lastPathComponent)'")
|
|
return fileURL.lastPathComponent
|
|
}
|
|
|
|
// Fallback to original implementation for regular promises
|
|
guard let item = stashItem else {
|
|
return "StashImage.png"
|
|
}
|
|
|
|
// Bepaal de originele naam: customName > filename (zonder extensie) > fallback
|
|
let originalName: String
|
|
if let customName = item.customName, !customName.isEmpty {
|
|
originalName = customName
|
|
} else if let fileURL = item.fileURL {
|
|
originalName = fileURL.deletingPathExtension().lastPathComponent
|
|
} else {
|
|
originalName = "StashImage"
|
|
}
|
|
|
|
let filename = "\(originalName).png"
|
|
print("🔄 filePromiseProvider asking for filename: '\(filename)'")
|
|
return filename
|
|
}
|
|
|
|
func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) {
|
|
print("🔄 filePromiseProvider writePromiseTo: \(url.lastPathComponent)")
|
|
|
|
// Check if this is a file URL promise
|
|
if let userInfo = filePromiseProvider.userInfo as? [String: Any],
|
|
let isFilePromise = userInfo["isFilePromise"] as? Bool,
|
|
isFilePromise,
|
|
let sourceFileURL = userInfo["fileURL"] as? URL {
|
|
print("🔄 filePromiseProvider (file URL) copying from: \(sourceFileURL.lastPathComponent)")
|
|
|
|
do {
|
|
// Check if destination already exists
|
|
if FileManager.default.fileExists(atPath: url.path) {
|
|
try FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
try FileManager.default.copyItem(at: sourceFileURL, to: url)
|
|
print("✅ Successfully copied file URL promise to: \(url.lastPathComponent)")
|
|
completionHandler(nil)
|
|
} catch {
|
|
print("❌ Failed to copy file URL promise: \(error)")
|
|
completionHandler(error)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fallback to original implementation
|
|
guard let item = stashItem else {
|
|
completionHandler(NSError(domain: "StashDragError", code: 1, userInfo: [NSLocalizedDescriptionKey: "No stash item available"]))
|
|
return
|
|
}
|
|
|
|
// Kopieer het bestand van stash naar de gevraagde locatie
|
|
if let stashFileURL = item.fileURL {
|
|
do {
|
|
// Check if destination already exists
|
|
if FileManager.default.fileExists(atPath: url.path) {
|
|
try FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
try FileManager.default.copyItem(at: stashFileURL, to: url)
|
|
print("✅ Successfully copied stash file to: \(url.lastPathComponent)")
|
|
completionHandler(nil)
|
|
} catch {
|
|
print("❌ Failed to copy stash file: \(error)")
|
|
completionHandler(error)
|
|
}
|
|
} else {
|
|
// Fallback: converteer NSImage naar PNG data
|
|
guard let tiffRepresentation = item.nsImage.tiffRepresentation,
|
|
let bitmapImageRep = NSBitmapImageRep(data: tiffRepresentation),
|
|
let pngData = bitmapImageRep.representation(using: .png, properties: [:]) else {
|
|
print("❌ Could not convert stash image to PNG data")
|
|
completionHandler(NSError(domain: "StashDragError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not convert image to PNG"]))
|
|
return
|
|
}
|
|
|
|
do {
|
|
try pngData.write(to: url)
|
|
print("✅ Successfully wrote stash image data to: \(url.lastPathComponent)")
|
|
completionHandler(nil)
|
|
} catch {
|
|
print("❌ Failed to write stash image data: \(error)")
|
|
completionHandler(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - NSDraggingSource Extension
|
|
extension StashDraggableNSImageView: NSDraggingSource {
|
|
func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
|
|
return .copy
|
|
}
|
|
|
|
func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) {
|
|
print("🎯 STASH DEBUG: Stash drag session beginning at \(screenPoint)")
|
|
print("🎯 STASH DEBUG: This should ONLY show STASH grid, NEVER main grid")
|
|
}
|
|
|
|
func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) {
|
|
// Stash items hebben geen proximity feedback - simpel houden
|
|
// print("🎯 STASH DEBUG: StashDraggableNSImageView: Drag moved to \(screenPoint) - no proximity feedback (stash only)")
|
|
}
|
|
|
|
func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
|
|
let wasDraggingPreviously = self.isPerformingDrag
|
|
isPerformingDrag = false
|
|
print("🖱️ STASH DEBUG: StashDraggableNSImageView (ended): Drag session ended. Operation: \(operation.rawValue), wasDraggingPreviously: \(wasDraggingPreviously)")
|
|
|
|
// NIEUW: Stop drag session voor proximity monitoring
|
|
stashGridManager?.stopDragSession()
|
|
print("🖱️ STASH DEBUG: Stopped stash drag session - proximity monitoring activated")
|
|
|
|
// AANGEPAST: Laat grid zichtbaar voor interactie, maar start proximity monitoring
|
|
if operation != .copy {
|
|
print("🔶 STASH DEBUG: Drag ended with no drop (operation \(operation.rawValue)) - starting proximity monitoring")
|
|
print("🔶 STASH DEBUG: Grid will auto-hide when mouse moves away from stash window")
|
|
} else {
|
|
print("🔶 STASH DEBUG: Successful drop (operation \(operation.rawValue)) - grid handled by drop action")
|
|
}
|
|
|
|
// NIEUW: Delegate notificatie
|
|
if let imageURL = self.imageURL {
|
|
delegate?.stashImageDragDidEnd(imageURL: imageURL, operation: operation, from: self)
|
|
}
|
|
|
|
// FIXED: Nu pas mouseDownEvent clearen na drag completion
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.mouseDownEvent = nil
|
|
print("🖱️ STASH DEBUG: StashDraggableNSImageView (ended): Async cleanup completed for \(self?.imageURL?.lastPathComponent ?? "unknown")")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SwiftUI Representable Wrapper
|
|
struct StashDraggableImageView: NSViewRepresentable {
|
|
let nsImage: NSImage
|
|
let imageURL: URL
|
|
let suggestedName: String?
|
|
let stashItem: IdentifiableImage
|
|
let delegate: StashDraggableImageViewDelegate
|
|
|
|
func makeNSView(context: Context) -> StashDraggableNSImageView {
|
|
let nsView = StashDraggableNSImageView()
|
|
nsView.image = nsImage
|
|
nsView.imageURL = imageURL
|
|
nsView.suggestedName = suggestedName
|
|
nsView.stashItem = stashItem
|
|
nsView.setDelegate(delegate)
|
|
return nsView
|
|
}
|
|
|
|
func updateNSView(_ nsView: StashDraggableNSImageView, context: Context) {
|
|
nsView.image = nsImage
|
|
nsView.imageURL = imageURL
|
|
nsView.suggestedName = suggestedName
|
|
nsView.stashItem = stashItem
|
|
nsView.setDelegate(delegate)
|
|
}
|
|
} |