import AppKit import Vision import SwiftUI // MARK: - Grid Action Manager protocol GridActionManagerDelegate: AnyObject { func showOrUpdateStash(with imageURL: URL) func closePreviewWithAnimation(immediate: Bool) func ensurePreviewVisible() func getLastImage() -> NSImage? func getTempURL() -> URL? func getActivePreviewWindow() -> NSWindow? func getRenameActionHandler() -> RenameActionHandler func flashPreviewBorder() func disableMonitoring() func enableMonitoring() func gridViewManagerHideGrid(monitorForReappear: Bool) func getGridCurrentFrame() -> NSRect? // 🎨 NEW: Background removal thumbnail workflow with original URL func showBackgroundRemovalThumbnail(with image: NSImage, originalURL: URL) } class GridActionManager { weak var delegate: GridActionManagerDelegate? init(delegate: GridActionManagerDelegate) { self.delegate = delegate } // MARK: - Grid Action Handler func handleGridAction(imageURL: URL, cellIndex: Int, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { print("✅ GridActionManager: Handling action for cell \(cellIndex)") // Get the action type based on the dynamic action order let actionOrder = SettingsManager.shared.actionOrder guard cellIndex < actionOrder.count else { print("â„šī¸ Cell index \(cellIndex) out of range for action order.") completion(false, false, false) return } let actionType = actionOrder[cellIndex] switch actionType { case .rename: print("📝 Rename action triggered for \(imageURL.path)") handleRenameAction(imageURL: imageURL, completion: completion) case .stash: print("✨ Stash action triggered for \(imageURL.path)") handleStashAction(imageURL: imageURL, completion: completion) case .ocr: print("📑 OCR action triggered for \(imageURL.path)") handleOCRAction(imageURL: imageURL, dropPoint: dropPoint, gridWindow: gridWindow, completion: completion) case .clipboard: print("📋 Clipboard action triggered for \(imageURL.path)") handleClipboardAction(imageURL: imageURL, dropPoint: dropPoint, gridWindow: gridWindow, completion: completion) case .backgroundRemove: print("🎨 Background Remove action triggered for \(imageURL.path)") handleBackgroundRemoveAction(imageURL: imageURL, dropPoint: dropPoint, gridWindow: gridWindow, completion: completion) case .cancel: print("đŸšĢ Cancel action triggered") handleCancelAction(completion: completion) case .remove: print("🗑 Remove action triggered") handleRemoveAction(completion: completion) } } // MARK: - Individual Action Handlers private func handleRenameAction(imageURL: URL, completion: @escaping (Bool, Bool, Bool) -> Void) { print("🔍 DEBUG: handleRenameAction called with URL: \(imageURL)") print("🔍 DEBUG: File exists at URL: \(FileManager.default.fileExists(atPath: imageURL.path))") delegate?.getRenameActionHandler().promptAndRename(originalURL: imageURL) { responseForRename in let successfulAction = (responseForRename != .alertThirdButtonReturn) let saveToFolderTriggered = (responseForRename == .alertSecondButtonReturn) print("🔍 DEBUG: Rename response: \(responseForRename), successful: \(successfulAction)") completion(successfulAction, saveToFolderTriggered, false) } } private func handleStashAction(imageURL: URL, completion: @escaping (Bool, Bool, Bool) -> Void) { print("✨ Stash action triggered for \(imageURL.path)") // CRITICALLY IMPORTANT: Close main grid immediately after stash action print("đŸ”ļ STASH ACTION: Closing main grid and disabling proximity monitoring") delegate?.gridViewManagerHideGrid(monitorForReappear: false) delegate?.showOrUpdateStash(with: imageURL) completion(true, false, true) // Pass true for isStashAction } private func handleOCRAction(imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { delegate?.disableMonitoring() performOcrAndCopy(from: imageURL, dropPoint: dropPoint, gridWindow: gridWindow) { success in // Completion for performOcrAndCopy is now async after panel closes // The actual success of OCR (text found vs. no text) is handled by the message in the panel. // For the grid action itself, we consider it 'successful' if the process ran. self.delegate?.enableMonitoring() self.delegate?.gridViewManagerHideGrid(monitorForReappear: false) completion(true, false, false) // OCR action always 'succeeds' in terms of grid interaction } } private func handleClipboardAction(imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { delegate?.disableMonitoring() copyImageToClipboard(from: imageURL, dropPoint: dropPoint, gridWindow: gridWindow) { self.delegate?.enableMonitoring() self.delegate?.gridViewManagerHideGrid(monitorForReappear: false) completion(true, false, false) } } private func handleBackgroundRemoveAction(imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { performBackgroundRemove(from: imageURL, dropPoint: dropPoint, gridWindow: gridWindow) completion(true, false, false) } private func handleCancelAction(completion: @escaping (Bool, Bool, Bool) -> Void) { // Handle cancel action through delegate callback mechanism NotificationCenter.default.post(name: .gridActionCancelRequested, object: nil) completion(false, false, false) } private func handleRemoveAction(completion: @escaping (Bool, Bool, Bool) -> Void) { // Handle remove action through delegate callback mechanism NotificationCenter.default.post(name: .gridActionRemoveRequested, object: nil) completion(false, false, false) } // MARK: - OCR Implementation private func performOcrAndCopy(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool) -> Void) { // Intel Mac compatible Vision framework OCR guard #available(macOS 10.15, *) else { print("❌ OCR requires macOS 10.15 or later") if let gridFrame = delegate?.getGridCurrentFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "OCR requires macOS 10.15+", autoCloseAfter: 1.8, onAutoCloseCompletion: { completion(false) }) } else { completion(false) } return } guard let nsImage = NSImage(contentsOf: imageURL) else { print("❌ OCR: Could not load image from URL: \(imageURL.path)") if let gridFrame = delegate?.getGridCurrentFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "Image load failed for OCR", autoCloseAfter: 1.8, onAutoCloseCompletion: { completion(false) }) } else { completion(false) } return } guard let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("❌ OCR: Could not obtain CGImage") if let gridFrame = delegate?.getGridCurrentFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "Could not get image data for OCR", autoCloseAfter: 1.8, onAutoCloseCompletion: { completion(false) }) } else { completion(false) } return } let request = VNRecognizeTextRequest { [weak self] request, error in guard let self = self else { completion(false); return } var message: String var ocrDidFindText = false if let err = error { print("❌ OCR error: \(err.localizedDescription)") message = "OCR error: \(err.localizedDescription)" } else { let observations = request.results as? [VNRecognizedTextObservation] ?? [] let recognizedText = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") if recognizedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { message = "No text found" } else { NSPasteboard.general.clearContents() NSPasteboard.general.setString(recognizedText, forType: .string) message = "Text copied to clipboard!" ocrDidFindText = true } } DispatchQueue.main.async { if let gridFrame = self.delegate?.getGridCurrentFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Processing...") panel.show(aroundGridFrame: gridFrame, text: message, autoCloseAfter: 1.8, onAutoCloseCompletion: { completion(ocrDidFindText) }) } else { completion(ocrDidFindText) // Call completion if gridFrame is nil } } } request.recognitionLevel = VNRequestTextRecognitionLevel.accurate request.usesLanguageCorrection = true let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { print("❌ OCR: Failed to perform request – \(error.localizedDescription)") DispatchQueue.main.async { self.showToast(message: "OCR failed", near: dropPoint, gridWindow: gridWindow) } } } } // MARK: - Clipboard Implementation private func copyImageToClipboard(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping () -> Void) { guard let nsImage = NSImage(contentsOf: imageURL) else { print("❌ Clipboard: Could not load image from URL: \(imageURL.path)") // Consider showing error with FeedbackBubblePanel if gridFrame is available if let gridFrame = delegate?.getGridCurrentFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "Image load failed for Clipboard", autoCloseAfter: 2.0) panel.closeWithAnimation { completion() } } else { completion() } return } NSPasteboard.general.clearContents() NSPasteboard.general.writeObjects([nsImage]) if let gridFrame = delegate?.getGridCurrentFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Copied!") panel.show(aroundGridFrame: gridFrame, text: "Copied to clipboard!", autoCloseAfter: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { panel.closeWithAnimation { completion() } } } else { print("âš ī¸ Could not get grid frame to show feedback bubble for clipboard.") completion() } } // MARK: - Background Remove Implementation private func performBackgroundRemove(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?) { print("🎨 Background Remove action triggered for \(imageURL.path)") // 🔄 ENHANCED: Better error handling and recovery for image loading var finalImageURL = imageURL var originalImage: NSImage? = nil // First attempt: Direct load originalImage = NSImage(contentsOf: finalImageURL) // If failed, try recovery strategies if originalImage == nil { print("âš ī¸ Initial image load failed, attempting recovery...") // Strategy 1: Check if file exists but has load issues if FileManager.default.fileExists(atPath: imageURL.path) { print("🔍 File exists but NSImage can't load it, trying alternative loading...") // Try loading with different methods if let imageData = try? Data(contentsOf: imageURL) { originalImage = NSImage(data: imageData) if originalImage != nil { print("✅ RECOVERED: Successfully loaded image via Data method") } } } // Strategy 2: Check temp directory for file if originalImage == nil { let tempDir = FileManager.default.temporaryDirectory let fileName = imageURL.lastPathComponent let tempURL = tempDir.appendingPathComponent(fileName) if FileManager.default.fileExists(atPath: tempURL.path) { originalImage = NSImage(contentsOf: tempURL) if originalImage != nil { finalImageURL = tempURL print("✅ RECOVERED: Found image in temp directory: \(tempURL.path)") } } } // Strategy 3: Check if it's a cache file issue and try alternative cache locations if originalImage == nil { let supportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let shotScreenDir = supportDir.appendingPathComponent("ShotScreen/Thumbnails") let cacheName = imageURL.lastPathComponent let cacheURL = shotScreenDir.appendingPathComponent(cacheName) if FileManager.default.fileExists(atPath: cacheURL.path) { originalImage = NSImage(contentsOf: cacheURL) if originalImage != nil { finalImageURL = cacheURL print("✅ RECOVERED: Found image in cache: \(cacheURL.path)") } } } } // Final check guard let finalImage = originalImage else { print("❌ Could not load image for background removal after all recovery attempts") print("📍 Attempted URLs:") print(" - Original: \(imageURL.path)") print(" - File exists: \(FileManager.default.fileExists(atPath: imageURL.path))") showToast(message: "Image load failed", near: dropPoint, gridWindow: gridWindow) return } print("✅ Successfully loaded image for BGR: \(finalImageURL.lastPathComponent)") print("🎨 Background Remove thumbnail UI starting...") // đŸŽ¯ NEW: Use thumbnail-based BGR workflow via delegate with recovered URL delegate?.showBackgroundRemovalThumbnail(with: finalImage, originalURL: finalImageURL) } // MARK: - Toast Notification System private func showToast(message: String, near point: NSPoint, gridWindow: NSWindow?) { let toastWidth: CGFloat = 180 let toastHeight: CGFloat = 40 // Kies vaste positie links (anders rechts) naast de grid let margin: CGFloat = 5 var screenPoint: NSPoint if let gw = gridWindow { let gridFrame = gw.frame let screenVisible = gw.screen?.visibleFrame ?? NSScreen.main!.visibleFrame var x = gridFrame.minX - toastWidth - margin // links van grid if x < screenVisible.minX + margin { // niet genoeg ruimte links → rechts x = gridFrame.maxX + margin } let y = gridFrame.midY - toastHeight / 2 screenPoint = NSPoint(x: x, y: y) } else { // Fallback: gebruik muislocatie zoals voorheen screenPoint = NSEvent.mouseLocation screenPoint.x -= toastWidth / 2 screenPoint.y += 20 } let toastWindow = NSWindow(contentRect: NSRect(x: screenPoint.x, y: screenPoint.y, width: toastWidth, height: toastHeight), styleMask: [.borderless], backing: .buffered, defer: false) toastWindow.isOpaque = false toastWindow.backgroundColor = .clear toastWindow.level = .floating + 5 toastWindow.hasShadow = true // Opbouw content let container = NSView(frame: NSRect(x: 0, y: 0, width: toastWidth, height: toastHeight)) let blur = NSVisualEffectView(frame: container.bounds) blur.blendingMode = .behindWindow blur.material = .hudWindow blur.state = .active blur.alphaValue = 0.9 blur.wantsLayer = true blur.layer?.cornerRadius = 12 blur.layer?.masksToBounds = true let label = NSTextField(labelWithString: message) label.textColor = .white label.alignment = .center label.font = .systemFont(ofSize: 13, weight: .medium) label.frame = NSRect(x: 0, y: (toastHeight - 20) / 2, width: toastWidth, height: 20) container.addSubview(blur) container.addSubview(label) toastWindow.contentView = container toastWindow.alphaValue = 0 toastWindow.makeKeyAndOrderFront(nil as Any?) NSAnimationContext.runAnimationGroup({ ctx in ctx.duration = 0.35 toastWindow.animator().alphaValue = 1 }, completionHandler: { let flyUp: CGFloat = 30 let targetOrigin = NSPoint(x: toastWindow.frame.origin.x, y: toastWindow.frame.origin.y + flyUp) NSAnimationContext.runAnimationGroup({ ctx in ctx.duration = 2 toastWindow.animator().alphaValue = 0 toastWindow.animator().setFrameOrigin(targetOrigin) }, completionHandler: { toastWindow.orderOut(nil as Any?) }) }) } // MARK: - Action Completion Handler func handleActionCompletion(actionCompletedSuccessfully: Bool, wasSaveToFolder: Bool, isStashAction: Bool) { print("â„šī¸ Action panel completion: Success - \(actionCompletedSuccessfully), WasSaveToFolder - \(wasSaveToFolder), IsStashAction - \(isStashAction)") if actionCompletedSuccessfully { if wasSaveToFolder { // Save to folder actions handle their own preview closing via closeAfterSave setting. print("â„šī¸ Save to folder action: Preview management handled by save logic.") } else if isStashAction { // CRITICAL: Ensure main grid stays closed after stash action print("đŸ”ļ STASH ACTION COMPLETION: Ensuring main grid stays closed - no proximity monitoring") delegate?.gridViewManagerHideGrid(monitorForReappear: false) // FIXED: For stash action, preview is already properly closed in main.swift _showOrUpdateStash // No need to close again here or check tempURL - it's already moved to stash directory print("â„šī¸ Stash action: Preview management handled by stash logic in main.swift") } else { if delegate?.getTempURL() != nil { print("â„šī¸ Other successful grid action (not save/stash): Ensuring preview is visible.") delegate?.ensurePreviewVisible() } } } else { if delegate?.getTempURL() != nil { print("â„šī¸ Action not successful or cancelled: Ensuring preview is visible.") delegate?.ensurePreviewVisible() } } } } // MARK: - Notification Names extension Notification.Name { static let gridActionCancelRequested = Notification.Name("gridActionCancelRequested") static let gridActionRemoveRequested = Notification.Name("gridActionRemoveRequested") }