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) } }