import AppKit import SwiftUI // MARK: - Button Hover Handler (glass-effect safe) class ButtonHoverHandler: NSObject { weak var button: NSButton? private let zoomScale: CGFloat init(button: NSButton, zoomScale: CGFloat = 1.3) { self.button = button self.zoomScale = zoomScale super.init() } func mouseEntered(with event: NSEvent) { guard let button = button else { return } print("🎯 HOVER: Mouse entered button - starting zoom (\(zoomScale)x) and color effect") let hoverColor = ThemeManager.shared.buttonHoverColor // Adaptive hover color NSAnimationContext.runAnimationGroup({ context in context.duration = 0.2 context.timingFunction = CAMediaTimingFunction(name: .easeOut) context.allowsImplicitAnimation = true // Color change button.animator().contentTintColor = hoverColor }) // Zoom effect using layer transform (separate from color animation) if let layer = button.layer { CATransaction.begin() CATransaction.setAnimationDuration(0.2) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut)) let scaleTransform = CATransform3DMakeScale(zoomScale, zoomScale, 1.0) layer.transform = scaleTransform CATransaction.commit() print("🎯 HOVER: Applied \(zoomScale)x zoom scale") } } func mouseExited(with event: NSEvent) { guard let button = button else { return } print("🎯 HOVER: Mouse exited button - restoring original size and color") // Get original color let originalColor = objc_getAssociatedObject(button, "originalColor") as? NSColor ?? ThemeManager.shared.buttonOriginalColor NSAnimationContext.runAnimationGroup({ context in context.duration = 0.25 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) context.allowsImplicitAnimation = true // Restore original color button.animator().contentTintColor = originalColor }) // Restore original size (separate from color animation) if let layer = button.layer { CATransaction.begin() CATransaction.setAnimationDuration(0.25) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeInEaseOut)) layer.transform = CATransform3DIdentity CATransaction.commit() print("🎯 HOVER: Restored original scale") } } } // MARK: - Preview Window Management class PreviewManager: NSObject { weak var delegate: PreviewManagerDelegate? // MARK: - Properties var activePreviewWindow: NSWindow? var currentImageView: DraggableImageView? var previewDismissTimer: Timer? var isClosing = false var isPreviewUpdating = false // NIEUW: Loading indicator properties var isShowingLoadingIndicator = false var loadingOverlayWindow: NSWindow? // 🎨 NEW: Background Removal Mode Properties var isBackgroundRemovalMode = false var isBGRTransition = false // πŸ”„ NEW: Flag to prevent BGR reset during BGR actions var originalImage: NSImage? var processedImage: NSImage? var bgrResetButton: BGROverlayButton? var bgrToggleButton: BGROverlayButton? // 🎨 NEW: BGR File Management Properties var originalImageURL: URL? // File URL for original image var processedImageURL: URL? // File URL for processed image var isShowingProcessedImage = false // Track which image is currently displayed // 🎨 NEW: BGR Progress Bar Properties var bgrProgressContainer: NSView? var bgrProgressBar: NSProgressIndicator? var bgrProgressLabel: NSTextField? // MARK: - πŸŽ› TOOLBAR BUTTON AANPASSINGEN // πŸ“ BUTTON GROOTTES let closeButtonSize: CGFloat = 9 // πŸ”΄ Close button (X) - Klein let saveButtonSize: CGFloat = 16 // πŸ’Ύ Save button (Map+Plus) let folderButtonSize: CGFloat = 16 // πŸ“ Folder button (Map) let settingsButtonSize: CGFloat = 14 // βš™οΈ Settings button (Tandwiel) // πŸ“ BUTTON VERTICALE POSITIES (offset van center) let closeButtonVerticalOffset: CGFloat = -0.5 // πŸ”΄ Close button verticaal let saveButtonVerticalOffset: CGFloat = 0 // πŸ’Ύ Save button verticaal let folderButtonVerticalOffset: CGFloat = -0.5 // πŸ“ Folder button verticaal let settingsButtonVerticalOffset: CGFloat = 0 // βš™οΈ Settings button verticaal // ↔️ BUTTON HORIZONTALE POSITIES (extra offset) let saveButtonHorizontalOffset: CGFloat = 0 // πŸ’Ύ Save button horizontaal verschuiving // πŸ”„ BUTTON INTERACTIE & STYLING let buttonHoverZoomScale: CGFloat = 1.1 // πŸ” Hover zoom effect (1.0 = geen zoom) let buttonSpacing: CGFloat = 15 // ↔️ Afstand tussen buttons let toolbarStartPadding: CGFloat = 8 // ←→ Afstand van linkerrand tot eerste button // 🏷 FILENAME LABEL INSTELLINGEN let filenameLabelFontSize: CGFloat = 10 // πŸ”€ Tekst grootte bestandsnaam let filenameLabelVerticalOffset: CGFloat = 1 // πŸ“ Verticale positie bestandsnaam let filenameLabelTextOpacity: CGFloat = 0.7 // 🌫 Tekst doorzichtigheid // 🎨 WINDOW STYLING INSTELLINGEN let windowCornerRadius: CGFloat = 8 // 🎨 Ronde hoeken window let toolbarHeight: CGFloat = 20 // πŸ“ Hoogte van toolbar let containerPadding: CGFloat = 5 // πŸ“¦ Binnenste padding let shadowPadding: CGFloat = 12 // 🌫 Ruimte voor schaduw init(delegate: PreviewManagerDelegate) { self.delegate = delegate super.init() // Setup theme change observer for live preview updates ThemeManager.shared.observeThemeChanges { [weak self] in DispatchQueue.main.async { self?.updateActivePreviewTheme() } } } // MARK: - Theme Management private func updateActivePreviewTheme() { // Update only if there's an active preview window guard let window = activePreviewWindow else { print("🎨 THEME: No active preview window to update") return } print("🎨 THEME: Updating active preview window colors for current theme") updateWindowThemeColors(window) } private func updateWindowThemeColors(_ window: NSWindow) { // Find and update all theme-dependent views recursively updateViewThemeColors(window.contentView) } private func updateViewThemeColors(_ view: NSView?) { guard let view = view else { return } // Update container background colors if view.layer?.backgroundColor != nil && view.layer?.backgroundColor != NSColor.clear.cgColor { view.layer?.backgroundColor = ThemeManager.shared.containerBackground.cgColor print("🎨 THEME: Updated container background") } // Update shadow colors if view.layer?.shadowColor != nil { view.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor view.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity print("🎨 THEME: Updated shadow colors") } // Update button colors if let button = view as? NSButton { button.contentTintColor = ThemeManager.shared.buttonTintColor // Update stored original color for hover restoration objc_setAssociatedObject(button, "originalColor", ThemeManager.shared.buttonTintColor, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) print("🎨 THEME: Updated button colors") } // Update text field colors if let textField = view as? NSTextField, textField.tag == 11001 { // Only filename labels textField.textColor = ThemeManager.shared.filenameLabelTextColor print("🎨 THEME: Updated text field colors") } // Recursively update subviews for subview in view.subviews { updateViewThemeColors(subview) } } // MARK: - Public Interface func showPreview(image: NSImage) { NSApp.activate(ignoringOtherApps: true) print("πŸ“¦ Start showPreview") print("πŸ–Ό Afmeting: \(image.size)") // πŸ”„ SMART FIX: Only reset BGR mode for NEW screenshots, not BGR actions on existing thumbnails if isBackgroundRemovalMode && !isBGRTransition { print("πŸ”„ FORCE RESET: Detected BGR mode active during NEW screenshot - forcing complete reset") forceCompleteBGRReset() } if let _ = self.activePreviewWindow { print("πŸ“¦ Closing existing preview window before showing new one.") // πŸ”§ CRITICAL FIX: Preserve tempURL when closing for new screenshot self.closePreviewWithAnimation(immediate: true, preserveTempFile: true) RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) } guard image.size.width > 0 && image.size.height > 0 else { print("❌ Lege afbeelding, preview wordt overgeslagen") return } delegate?.setLastImage(image) previewDismissTimer?.invalidate() previewDismissTimer = nil guard let window = createNewThumbnailWindow(image: image) else { print("❌ Failed to create new thumbnail window in showPreview using createNewThumbnailWindow.") if isPreviewUpdating { DispatchQueue.main.async { self.isPreviewUpdating = false } } return } activePreviewWindow = window window.alphaValue = 1.0 window.makeKeyAndOrderFront(nil as Any?) // Force window activation to ensure tracking areas work immediately window.level = .floating NSApp.activate(ignoringOtherApps: true) // Ensure window accepts mouse events window.acceptsMouseMovedEvents = true print("DEBUG: showPreview FINISHED - Window: \(window), Visible: \(window.isVisible), Alpha: \(window.alphaValue), Frame: \(window.frame)") // Voeg automatisch sluiten timer toe volgens instellingen let timerValue = SettingsManager.shared.thumbnailTimer if timerValue > 0 { previewDismissTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timerValue), repeats: false) { [weak self] _ in self?.closePreviewWithAnimation() } RunLoop.main.add(previewDismissTimer!, forMode: .common) print("⏱ Scheduled auto-dismiss in \(timerValue) seconds") } else { print("⏱ Auto-dismiss disabled (thumbnailTimer = 0)") } } func updatePreviewSize() { guard let lastImage = delegate?.getLastImage() else { return } if isClosing || isPreviewUpdating { return } isPreviewUpdating = true // Invalidate timer *before* closing, as closing might be asynchronous or delayed slightly previewDismissTimer?.invalidate() previewDismissTimer = nil closePreviewWithAnimation(immediate: true) showPreview(image: lastImage) DispatchQueue.main.async { self.isPreviewUpdating = false } } func closePreviewWithAnimation(immediate: Bool = false, preserveTempFile: Bool = false) { // Invalidate timer at the very beginning of any close operation previewDismissTimer?.invalidate() previewDismissTimer = nil guard let window = activePreviewWindow, !isClosing else { if activePreviewWindow == nil && isPreviewUpdating { DispatchQueue.main.async { self.isPreviewUpdating = false } } return } isClosing = true let cleanup = { [weak self] in guard let self = self else { return } window.orderOut(nil as Any?) if self.activePreviewWindow === window { print("🧼 Cleaning up preview resources...") self.activePreviewWindow = nil self.currentImageView = nil if !preserveTempFile { self.delegate?.clearTempFile() } else { print("πŸ’° Preserving tempURL as requested.") } } self.isClosing = false } if immediate { cleanup() } else { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.4 window.animator().alphaValue = 0 // Voeg "hupje" animatie toe: beweeg venster iets omhoog let currentFrame = window.frame let bounceHeight: CGFloat = 20 // hoogte van het hupje let finalFrame = NSRect(x: currentFrame.origin.x, y: currentFrame.origin.y + bounceHeight, width: currentFrame.width, height: currentFrame.height) window.animator().setFrame(finalFrame, display: true) }, completionHandler: cleanup) } } @objc func closePreviewWindow() { previewDismissTimer?.invalidate() previewDismissTimer = nil closePreviewWithAnimation() } func getActivePreviewWindow() -> NSWindow? { return activePreviewWindow } func ensurePreviewVisible() { guard let preview = self.activePreviewWindow else { if let img = delegate?.getLastImage() { self.showPreview(image: img) } return } if !preview.isVisible { preview.alphaValue = 0 preview.orderFront(nil) NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 2.0 preview.animator().alphaValue = 1 } } } // MARK: - Visual Effects func flashPreviewBorder() { guard let window = activePreviewWindow, let _ = window.contentView else { print("⚠️ FlashEffect: No active preview window or outerContainer (contentView).") return } guard let currentImgView = self.currentImageView, let imageContainer = currentImgView.superview else { print("⚠️ FlashEffect: Could not find imageContainer via currentImageView.superview.") return } let flashView = NSView(frame: imageContainer.bounds) flashView.wantsLayer = true flashView.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.50).cgColor flashView.layer?.cornerRadius = imageContainer.layer?.cornerRadius ?? 10 flashView.alphaValue = 0 imageContainer.addSubview(flashView) print("✨ FlashEffect: Starting white flash animation on imageContainer.") NSAnimationContext.runAnimationGroup({ context in context.duration = 0.08 context.timingFunction = CAMediaTimingFunction(name: .easeOut) flashView.animator().alphaValue = 1.0 }, completionHandler: { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.8 context.timingFunction = CAMediaTimingFunction(name: .easeIn) flashView.animator().alphaValue = 0.0 }, completionHandler: { flashView.removeFromSuperview() print("✨ FlashEffect: Animation complete, flashView removed.") }) }) } // MARK: - Screen Selection for Thumbnail func getTargetScreenForThumbnail() -> NSScreen? { let setting = SettingsManager.shared.thumbnailDisplayScreen switch setting { case .automatic: let mouseLocation = NSEvent.mouseLocation for screen in NSScreen.screens { if screen.frame.contains(mouseLocation) { print("πŸ“Ί Thumbnail scherm: Automatisch - muis op \(screen.localizedName)") return screen } } print("πŸ“Ί Thumbnail scherm: Automatisch fallback naar eerste scherm") return NSScreen.screens.first ?? NSScreen.main case .screen1: if NSScreen.screens.count >= 1 { let screen = NSScreen.screens[0] print("πŸ“Ί Thumbnail scherm: Scherm 1 (\(screen.localizedName))") return screen } print("πŸ“Ί Thumbnail scherm: Scherm 1 niet beschikbaar, fallback naar eerste scherm") return NSScreen.screens.first ?? NSScreen.main case .screen2: if NSScreen.screens.count >= 2 { let screen = NSScreen.screens[1] print("πŸ“Ί Thumbnail scherm: Scherm 2 (\(screen.localizedName))") return screen } print("πŸ“Ί Thumbnail scherm: Scherm 2 niet beschikbaar, fallback naar eerste scherm") return NSScreen.screens.first ?? NSScreen.main case .screen3: if NSScreen.screens.count >= 3 { let screen = NSScreen.screens[2] print("πŸ“Ί Thumbnail scherm: Scherm 3 (\(screen.localizedName))") return screen } print("πŸ“Ί Thumbnail scherm: Scherm 3 niet beschikbaar, fallback naar eerste scherm") return NSScreen.screens.first ?? NSScreen.main case .screen4: if NSScreen.screens.count >= 4 { let screen = NSScreen.screens[3] print("πŸ“Ί Thumbnail scherm: Scherm 4 (\(screen.localizedName))") return screen } print("πŸ“Ί Thumbnail scherm: Scherm 4 niet beschikbaar, fallback naar eerste scherm") return NSScreen.screens.first ?? NSScreen.main case .screen5: if NSScreen.screens.count >= 5 { let screen = NSScreen.screens[4] print("πŸ“Ί Thumbnail scherm: Scherm 5 (\(screen.localizedName))") return screen } print("πŸ“Ί Thumbnail scherm: Scherm 5 niet beschikbaar, fallback naar eerste scherm") return NSScreen.screens.first ?? NSScreen.main } } // MARK: - Cleanup func cleanup() { // Clean up BGR mode if active if isBackgroundRemovalMode { exitBackgroundRemovalMode() } previewDismissTimer?.invalidate() previewDismissTimer = nil activePreviewWindow = nil currentImageView = nil } // MARK: - Window Creation private func createNewThumbnailWindow(image: NSImage) -> NSWindow? { if isPreviewUpdating { print("DEBUG: createNewThumbnailWindow - isPreviewUpdating is true, returning nil early") return nil } print("DEBUG: createNewThumbnailWindow called with image: \(image.size)") // Debug current theme print("🎨 THUMBNAIL: Creating new thumbnail window with current theme:") ThemeManager.shared.printCurrentTheme() // 0. Basisinstellingen en afmetingen let actualImageWidth = SettingsManager.shared.thumbnailFixedSize.dimensions.width let actualImageHeight = SettingsManager.shared.thumbnailFixedSize.dimensions.height // 1. NIEUWE UNIFIED LAYOUT - één container voor alles (gebruik constants) let fixedToolbarHeight: CGFloat = toolbarHeight // πŸŽ› Gebruik constant let spacingBelowToolbar: CGFloat = 8 // Corner Radii - gebruik constants let mainContainerCornerRadius: CGFloat = windowCornerRadius // 🎨 Gebruik constant let imageCornerRadius: CGFloat = windowCornerRadius - 4 // 🎨 Iets kleiner dan container // 2. Bereken afmetingen - UNIFIED let imageWidth = actualImageWidth let imageHeight = actualImageHeight // Main container bevat: padding + toolbar + spacing + image + padding let mainContainerWidth = imageWidth + (2 * containerPadding) let mainContainerHeight = containerPadding + fixedToolbarHeight + spacingBelowToolbar + imageHeight + containerPadding // 3. GLASS EFFECT CONTAINER - FIXED CORNER RADIUS let shadowContainerFrame = NSRect(x: shadowPadding, y: shadowPadding, width: mainContainerWidth, height: mainContainerHeight) let mainContainerFrame = NSRect(x: 0, y: 0, width: mainContainerWidth, height: mainContainerHeight) let mainContainer = NSVisualEffectView(frame: mainContainerFrame) mainContainer.material = ThemeManager.shared.glassEffectMaterial mainContainer.blendingMode = ThemeManager.shared.glassEffectBlending mainContainer.state = .active mainContainer.alphaValue = ThemeManager.shared.glassEffectAlpha mainContainer.wantsLayer = true // πŸ”₯ CRITICAL FIX: NSVisualEffectView corner radius mainContainer.layer?.cornerRadius = mainContainerCornerRadius mainContainer.layer?.masksToBounds = true // πŸ”₯ MOET TRUE zijn voor corner radius! // πŸ”₯ EXTRA FIX: Shadow op PARENT container, niet op visual effect view let shadowContainer = NSView(frame: shadowContainerFrame) shadowContainer.wantsLayer = true shadowContainer.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor shadowContainer.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity shadowContainer.layer?.shadowRadius = 6 shadowContainer.layer?.shadowOffset = CGSize(width: 0, height: -2) shadowContainer.layer?.cornerRadius = mainContainerCornerRadius shadowContainer.layer?.masksToBounds = false // Shadow container mag wel shadow buiten bounds // 4. Toolbar BINNEN de main container (ONDERAAN) let toolbarFrame = NSRect(x: containerPadding, y: containerPadding, width: mainContainerWidth - (2 * containerPadding), height: fixedToolbarHeight) let toolbarView = NSView(frame: toolbarFrame) toolbarView.wantsLayer = false // Geen aparte layer, gebruik parent styling // Toolbar buttons let symbolConfig = NSImage.SymbolConfiguration(pointSize: 14, weight: .regular) let closeSymbolConfig = NSImage.SymbolConfiguration(pointSize: 12.5, weight: .regular) // 🎯 Kleinere icoon voor close button var currentXButton: CGFloat = toolbarStartPadding let closeButtonY = (fixedToolbarHeight - closeButtonSize) / 2 + closeButtonVerticalOffset let closeButton = HoverButton(frame: NSRect(x: currentXButton, y: closeButtonY, width: closeButtonSize, height: closeButtonSize)) closeButton.image = NSImage(systemSymbolName: "xmark.circle", accessibilityDescription: "Close")?.withSymbolConfiguration(closeSymbolConfig) closeButton.isBordered = false; closeButton.bezelStyle = .shadowlessSquare; closeButton.contentTintColor = ThemeManager.shared.buttonTintColor closeButton.action = #selector(closePreviewWindow); closeButton.target = self closeButton.setupHover(zoomScale: buttonHoverZoomScale) toolbarView.addSubview(closeButton) currentXButton += closeButtonSize + buttonSpacing let saveButtonY = (fixedToolbarHeight - saveButtonSize) / 2 + saveButtonVerticalOffset let saveButtonX = currentXButton + saveButtonHorizontalOffset let saveButton = HoverButton(frame: NSRect(x: saveButtonX, y: saveButtonY, width: saveButtonSize, height: saveButtonSize)) saveButton.image = NSImage(systemSymbolName: "folder.fill.badge.plus", accessibilityDescription: "Save")?.withSymbolConfiguration(symbolConfig) saveButton.isBordered = false; saveButton.bezelStyle = .shadowlessSquare; saveButton.contentTintColor = ThemeManager.shared.buttonTintColor saveButton.action = #selector(PreviewManager.saveFromPreview); saveButton.target = self saveButton.setupHover(zoomScale: buttonHoverZoomScale) toolbarView.addSubview(saveButton) currentXButton += saveButtonSize + buttonSpacing var folderButton: HoverButton? if SettingsManager.shared.showFolderButton { let folderButtonY = (fixedToolbarHeight - folderButtonSize) / 2 + folderButtonVerticalOffset folderButton = HoverButton(frame: NSRect(x: currentXButton, y: folderButtonY, width: folderButtonSize, height: folderButtonSize)) folderButton!.image = NSImage(systemSymbolName: "folder.fill", accessibilityDescription: "Open Folder")?.withSymbolConfiguration(symbolConfig) folderButton!.isBordered = false; folderButton!.bezelStyle = .shadowlessSquare; folderButton!.contentTintColor = ThemeManager.shared.buttonTintColor folderButton!.action = #selector(PreviewManager.openScreenshotFolder); folderButton!.target = self folderButton!.setupHover(zoomScale: buttonHoverZoomScale) toolbarView.addSubview(folderButton!) currentXButton += folderButtonSize + buttonSpacing } let settingsButtonX = (mainContainerWidth - (2 * containerPadding)) - settingsButtonSize - 8 let settingsButtonY = (fixedToolbarHeight - settingsButtonSize) / 2 + settingsButtonVerticalOffset let settingsButton = HoverButton(frame: NSRect(x: settingsButtonX, y: settingsButtonY, width: settingsButtonSize, height: settingsButtonSize)) settingsButton.image = NSImage(systemSymbolName: "gearshape.fill", accessibilityDescription: "Settings")?.withSymbolConfiguration(symbolConfig) settingsButton.isBordered = false; settingsButton.bezelStyle = .shadowlessSquare; settingsButton.contentTintColor = ThemeManager.shared.buttonTintColor settingsButton.action = #selector(PreviewManager.openSettings); settingsButton.target = self settingsButton.setupHover(zoomScale: buttonHoverZoomScale) toolbarView.addSubview(settingsButton) let labelX = currentXButton let labelWidth = settingsButtonX - labelX - 8 let labelDesiredHeight: CGFloat = 15.0 let labelYOffset = (fixedToolbarHeight - labelDesiredHeight) / 2.0 + filenameLabelVerticalOffset let filenameLabel = NSTextField(frame: NSRect(x: labelX, y: labelYOffset, width: labelWidth, height: labelDesiredHeight)) filenameLabel.isEditable = false; filenameLabel.isSelectable = true; filenameLabel.isBordered = false filenameLabel.backgroundColor = NSColor.clear; filenameLabel.textColor = ThemeManager.shared.filenameLabelTextColor filenameLabel.font = NSFont.systemFont(ofSize: filenameLabelFontSize) filenameLabel.alignment = .center filenameLabel.lineBreakMode = .byTruncatingMiddle; filenameLabel.tag = 11001 if let url = delegate?.getTempURL() { filenameLabel.stringValue = url.lastPathComponent; filenameLabel.toolTip = url.lastPathComponent } else { let fallbackName = "Schermafbeelding \(DateFormatter().string(from: Date())).png" filenameLabel.stringValue = fallbackName; filenameLabel.toolTip = fallbackName } toolbarView.addSubview(filenameLabel) // 5. Image Container BINNEN de main container (BOVENAAN) let imageFrame = NSRect(x: containerPadding, y: containerPadding + fixedToolbarHeight + spacingBelowToolbar, width: imageWidth, height: imageHeight) let imageContainer = NSView(frame: imageFrame) imageContainer.wantsLayer = true imageContainer.layer?.cornerRadius = imageCornerRadius imageContainer.layer?.masksToBounds = true imageContainer.layer?.backgroundColor = NSColor.clear.cgColor // 6. DraggableImageView DIRECT in image container let imageView = DraggableImageView(frame: imageContainer.bounds) imageView.wantsLayer = true imageView.clipsToBounds = true imageView.image = image imageView.imageScaling = .scaleProportionallyUpOrDown imageView.imageAlignment = .alignCenter imageView.layer?.cornerRadius = imageCornerRadius imageView.layer?.masksToBounds = true imageView.layer?.backgroundColor = NSColor.clear.cgColor imageView.appDelegate = delegate as? ScreenshotApp // πŸ”§ FIX: Set imageURL for dragging functionality // In BGR mode, this is set elsewhere, but for normal screenshots we need the current tempURL if !isBackgroundRemovalMode { imageView.imageURL = delegate?.getTempURL() print("πŸ”§ DraggableImageView: Set imageURL for normal screenshot: \(imageView.imageURL?.lastPathComponent ?? "nil")") } currentImageView = imageView imageContainer.addSubview(imageView) // 7. Window Assembly - SIMPLIFIED let totalWindowWidth = mainContainerWidth + (2 * shadowPadding) let totalWindowHeight = mainContainerHeight + (2 * shadowPadding) let windowFrame = NSRect(x: 0, y: 0, width: totalWindowWidth, height: totalWindowHeight) let window = NSWindow(contentRect: windowFrame, styleMask: [.borderless], backing: .buffered, defer: false) window.level = .floating window.isOpaque = false window.backgroundColor = .clear window.hasShadow = false let rootView = NSView(frame: window.contentRect(forFrameRect: window.frame)) rootView.wantsLayer = true rootView.layer?.masksToBounds = false rootView.layer?.backgroundColor = NSColor.clear.cgColor window.contentView = rootView // FIXED HIERARCHY: root -> shadowContainer -> mainContainer -> (toolbar + imageContainer) shadowContainer.addSubview(mainContainer) rootView.addSubview(shadowContainer) mainContainer.addSubview(toolbarView) mainContainer.addSubview(imageContainer) // Debug prints print("DEBUG: Total Window Frame: \(windowFrame)") print("DEBUG: Main Container Frame: \(mainContainer.frame)") print("DEBUG: Toolbar Frame: \(toolbarView.frame)") print("DEBUG: Image Container Frame: \(imageContainer.frame)") // 5. Positioneer het window op het juiste scherm guard let screen = getTargetScreenForThumbnail() else { print("ERROR: Kon doelscherm niet bepalen") return nil } let screenVisibleFrame = screen.visibleFrame let screenEdgePadding: CGFloat = 20 let windowOriginX = screenVisibleFrame.maxX - totalWindowWidth - screenEdgePadding let windowOriginY = screenVisibleFrame.minY + screenEdgePadding window.setFrameOrigin(NSPoint(x: windowOriginX, y: windowOriginY)) return window } // MARK: - Loading Indicator func showLoadingIndicator() { guard !isShowingLoadingIndicator else { return } print("πŸ”„ ShowLoadingIndicator: Starting loading indicator...") isShowingLoadingIndicator = true // Bepaal de positie en grootte op basis van de thumbnail locatie guard let targetScreen = getTargetScreenForThumbnail() else { print("❌ Could not determine target screen for loading indicator") return } // AANGEPAST: Nieuwe afmetingen - breder en minder hoog let loadingSize = NSSize(width: 200, height: 70) let screenVisibleFrame = targetScreen.visibleFrame let screenEdgePadding: CGFloat = 20 // Plaats de loading indicator op dezelfde positie als waar de thumbnail zou komen let loadingOriginX = screenVisibleFrame.maxX - loadingSize.width - screenEdgePadding let loadingOriginY = screenVisibleFrame.minY + screenEdgePadding let loadingFrame = NSRect(x: loadingOriginX, y: loadingOriginY, width: loadingSize.width, height: loadingSize.height) // CreΓ«er het loading overlay window let loadingWindow = NSWindow( contentRect: loadingFrame, styleMask: [.borderless], backing: .buffered, defer: false ) loadingWindow.backgroundColor = NSColor.clear loadingWindow.isOpaque = false loadingWindow.hasShadow = true loadingWindow.level = .floating loadingWindow.ignoresMouseEvents = true loadingWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] // AANGEPAST: Start met alpha 0 voor fade-in animatie loadingWindow.alphaValue = 0 // CreΓ«er de loading content view let loadingContentView = NSView(frame: NSRect(origin: .zero, size: loadingSize)) loadingContentView.wantsLayer = true loadingContentView.layer?.cornerRadius = 12 loadingContentView.layer?.backgroundColor = ThemeManager.shared.containerBackground.cgColor loadingContentView.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor loadingContentView.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity loadingContentView.layer?.shadowRadius = 6 loadingContentView.layer?.shadowOffset = CGSize(width: 0, height: -2) // AANGEPAST: Horizontale progress bar in plaats van spinning indicator let progressBarWidth: CGFloat = 140 let progressBarHeight: CGFloat = 6 let progressBar = NSProgressIndicator(frame: NSRect( x: (loadingSize.width - progressBarWidth) / 2, y: 25, width: progressBarWidth, height: progressBarHeight )) progressBar.style = .bar progressBar.isIndeterminate = true progressBar.controlSize = .regular progressBar.startAnimation(nil) // AANGEPAST: Label positionering aangepast voor nieuwe layout let loadingLabel = NSTextField(frame: NSRect(x: 10, y: 40, width: loadingSize.width - 20, height: 20)) loadingLabel.isEditable = false loadingLabel.isSelectable = false loadingLabel.isBordered = false loadingLabel.backgroundColor = NSColor.clear loadingLabel.textColor = ThemeManager.shared.primaryTextColor loadingLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium) loadingLabel.alignment = .center loadingLabel.stringValue = "Stitching Screens..." loadingContentView.addSubview(progressBar) loadingContentView.addSubview(loadingLabel) loadingWindow.contentView = loadingContentView // Toon het window loadingWindow.makeKeyAndOrderFront(nil) loadingOverlayWindow = loadingWindow // NIEUW: Fade-in animatie NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 context.timingFunction = CAMediaTimingFunction(name: .easeOut) loadingWindow.animator().alphaValue = 1.0 }, completionHandler: { print("βœ… Loading indicator fade-in complete at \(loadingFrame)") }) } func hideLoadingIndicator() { guard isShowingLoadingIndicator, let loadingWindow = loadingOverlayWindow else { return } print("πŸ”„ HideLoadingIndicator: Hiding loading indicator...") isShowingLoadingIndicator = false // AANGEPAST: Fade-out animatie NSAnimationContext.runAnimationGroup({ context in context.duration = 0.4 context.timingFunction = CAMediaTimingFunction(name: .easeIn) loadingWindow.animator().alphaValue = 0 }, completionHandler: { [weak self] in loadingWindow.orderOut(nil) self?.loadingOverlayWindow = nil print("βœ… Loading indicator fade-out complete and window closed") }) } // MARK: - Button Actions (delegate to main app) @objc func saveFromPreview() { delegate?.saveFromPreview(self) } @objc func openScreenshotFolder() { delegate?.openScreenshotFolder() } @objc func openSettings() { delegate?.openSettings(self) } func closePreview() { print("🧼 Cleaning up preview resources...") // FIXED: Bewaar de huidige tempURL voordat we gaan clearen let currentTempURL = delegate?.getTempURL() // Close window first if let window = activePreviewWindow { window.orderOut(nil) window.close() } activePreviewWindow = nil // FIXED: Alleen de tempURL clearen als er geen nieuwe screenshot bezig is // Dit voorkomt dat we de net gemaakte screenshot URL wegwissen if let tempURL = currentTempURL { let filename = tempURL.lastPathComponent // Controleer of dit de oude screenshot is of een nieuwe // Als de timestamp meer dan 5 seconden oud is, clean het op let now = Date() let fileCreationTime = (try? FileManager.default.attributesOfItem(atPath: tempURL.path)[.creationDate] as? Date) ?? now let timeDifference = now.timeIntervalSince(fileCreationTime) if timeDifference > 5.0 { // Dit is een oude screenshot, veilig om op te ruimen delegate?.setTempFileURL(nil) print("πŸ—‘οΈ Cleaned up old screenshot file: \(filename)") } else { // Dit is een nieuwe screenshot, NIET opruimen print("⚠️ Preserving recent screenshot file: \(filename) (created \(String(format: "%.1f", timeDifference))s ago)") } } } } // MARK: - Custom Tracking Area private class HoverTrackingArea: NSTrackingArea { // This will be used to identify our custom tracking areas } // MARK: - PreviewManager Delegate Protocol protocol PreviewManagerDelegate: AnyObject { func getLastImage() -> NSImage? func setLastImage(_ image: NSImage) func clearTempFile() func getTempURL() -> URL? func findFilenameLabel(in window: NSWindow?) -> NSTextField? func openScreenshotFolder() func openSettings(_ sender: Any?) func saveFromPreview(_ sender: Any) func setTempFileURL(_ url: URL?) } // MARK: - Custom Button with Built-in Hover class HoverButton: NSButton { private var hoverHandler: ButtonHoverHandler? private var trackingArea: NSTrackingArea? override func awakeFromNib() { super.awakeFromNib() setupHover() } func setupHover(zoomScale: CGFloat = 1.3) { wantsLayer = true // Store current theme-aware color as original let originalColor = ThemeManager.shared.buttonTintColor // Create hover handler with custom zoom scale hoverHandler = ButtonHoverHandler(button: self, zoomScale: zoomScale) // Store theme-aware original color objc_setAssociatedObject(self, "originalColor", originalColor, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // Setup tracking area updateTrackingAreas() print("🎯 SETUP: HoverButton setup complete with \(zoomScale)x zoom and theme-aware colors") } override func updateTrackingAreas() { super.updateTrackingAreas() // Remove existing tracking area if let existing = trackingArea { removeTrackingArea(existing) } // Create new tracking area trackingArea = NSTrackingArea( rect: bounds, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil ) if let trackingArea = trackingArea { addTrackingArea(trackingArea) print("🎯 SETUP: HoverButton tracking area added") } } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) print("🎯 HOVER: HoverButton mouseEntered") hoverHandler?.mouseEntered(with: event) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) print("🎯 HOVER: HoverButton mouseExited") hoverHandler?.mouseExited(with: event) } } // MARK: - BGR Overlay Button with Hover Tooltips class BGROverlayButton: NSButton { enum TooltipPosition { case left, right } var tooltipText: String = "" var tooltipPosition: TooltipPosition = .left weak var parentContainer: NSView? private var tooltipView: NSView? private var trackingArea: NSTrackingArea? override func updateTrackingAreas() { super.updateTrackingAreas() // Remove existing tracking area if let existing = trackingArea { removeTrackingArea(existing) } // Create new tracking area trackingArea = NSTrackingArea( rect: bounds, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil ) if let trackingArea = trackingArea { addTrackingArea(trackingArea) print("🎯 BGR Button: Tracking area added - bounds: \(bounds), tooltipText: '\(tooltipText)'") } } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) print("🎯 BGR Button: Mouse entered - tooltipText: '\(tooltipText)'") showTooltip() } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) print("🎯 BGR Button: Mouse exited") hideTooltip() } private func showTooltip() { guard !tooltipText.isEmpty, let container = parentContainer else { print("🚫 Tooltip: Missing text or container - text: '\(tooltipText)', container: \(parentContainer != nil)") return } // Force cleanup any existing tooltip first if let existingTooltip = tooltipView { print("🧹 Tooltip: Cleaning up existing tooltip before showing new one") existingTooltip.removeFromSuperview() tooltipView = nil } print("✨ Tooltip: Showing '\(tooltipText)' at position \(tooltipPosition)") // Create simple text tooltip (no button/container styling) let tooltipLabel = NSTextField() tooltipLabel.stringValue = tooltipText tooltipLabel.textColor = .white tooltipLabel.backgroundColor = .clear tooltipLabel.font = NSFont.systemFont(ofSize: 11, weight: .medium) tooltipLabel.alignment = .center tooltipLabel.isBordered = false tooltipLabel.isEditable = false tooltipLabel.isSelectable = false tooltipLabel.sizeToFit() // Add subtle text shadow for readability tooltipLabel.wantsLayer = true tooltipLabel.layer?.shadowColor = NSColor.black.cgColor tooltipLabel.layer?.shadowOpacity = 0.8 tooltipLabel.layer?.shadowRadius = 2 tooltipLabel.layer?.shadowOffset = CGSize(width: 0, height: -1) // Position text tooltip relative to button in container coordinates let buttonFrameInContainer = self.frame let buttonCenterY = buttonFrameInContainer.midY let tooltipY = buttonCenterY - (tooltipLabel.frame.height / 2) let tooltipX: CGFloat let spacing: CGFloat = 8 switch tooltipPosition { case .left: tooltipX = buttonFrameInContainer.minX - tooltipLabel.frame.width - spacing case .right: tooltipX = buttonFrameInContainer.maxX + spacing } tooltipLabel.frame = NSRect(x: tooltipX, y: tooltipY, width: tooltipLabel.frame.width, height: tooltipLabel.frame.height) print("πŸ“ Tooltip: Button frame: \(buttonFrameInContainer), Tooltip frame: \(tooltipLabel.frame)") // Add directly to container (no wrapper view) container.addSubview(tooltipLabel) // Fade in animation tooltipLabel.alphaValue = 0 NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 tooltipLabel.animator().alphaValue = 1.0 } self.tooltipView = tooltipLabel print("βœ… Tooltip: Added to container with \(container.subviews.count) total subviews") } private func hideTooltip() { guard let tooltip = tooltipView else { print("🚫 Tooltip: No tooltip to hide") return } print("🎯 Tooltip: Hiding tooltip - current alpha: \(tooltip.alphaValue)") // Force immediate removal without animation to prevent hanging tooltip.removeFromSuperview() self.tooltipView = nil print("βœ… Tooltip: Force removed from view") } // πŸ”§ NEW: Public method to force hide tooltips (for cleanup) func forceHideTooltip() { hideTooltip() } } // MARK: - Background Removal Mode Extension extension PreviewManager { // 🎨 NEW: Show BGR preview with identical thumbnail UI func showBackgroundRemovalPreview(originalImage: NSImage, originalURL: URL) { print("🎨 PreviewManager: Starting BGR preview with thumbnail UI") // πŸ”„ NEW: Set transition flag to prevent force reset during BGR action isBGRTransition = true // Store the original image self.originalImage = originalImage self.processedImage = nil self.isShowingProcessedImage = false // Store original image URL from parameter (not delegate which is cleared) self.originalImageURL = originalURL self.processedImageURL = nil print("🎨 BGR: Stored original URL: \(originalURL.path)") // Enable BGR mode isBackgroundRemovalMode = true // πŸ”§ CRITICAL FIX: Check if there's already an active preview window and user has closeAfterDrag OFF let hasActivePreview = activePreviewWindow != nil let shouldReusePreview = hasActivePreview && !SettingsManager.shared.closeAfterDrag if shouldReusePreview { print("πŸ”„ BGR: Reusing existing preview window (closeAfterDrag is OFF)") // Update existing window for BGR mode if let window = activePreviewWindow { window.level = .popUpMenu // Higher level than .floating to stay on top print("🎨 BGR: Set window level to .popUpMenu for always-on-top behavior") } // Update existing image view if let imageView = currentImageView { imageView.image = originalImage imageView.imageURL = originalImageURL print("🎨 BGR: Updated existing imageView with BGR image and URL") } // Update filename label to show original filename in BGR mode updateBGRFilenameLabel() } else { print("πŸ”„ BGR: Creating new preview window (closeAfterDrag is ON or no existing preview)") // Use existing thumbnail system to create new window showPreview(image: originalImage) // 🎨 NEW: Set higher window level for BGR mode to stay on top if let window = activePreviewWindow { window.level = .popUpMenu // Higher level than .floating to stay on top print("🎨 BGR: Set window level to .popUpMenu for always-on-top behavior") } // 🎨 NEW: Initialize DraggableImageView with original URL for BGR mode if isBackgroundRemovalMode { currentImageView?.imageURL = originalImageURL print("🎨 BGR: Set initial imageURL to original: \(originalImageURL?.path ?? "nil")") } // 🎨 NEW: Update filename label to show original filename in BGR mode updateBGRFilenameLabel() } // Add BGR overlay buttons (for both cases) DispatchQueue.main.async { self.addBGROverlayButtons() // Start automatic BGR processing self.startBackgroundRemovalProcessing() // πŸ”„ NEW: Clear transition flag after BGR setup is complete self.isBGRTransition = false print("πŸ”„ BGR transition flag cleared - normal state management resumed") } } // 🎨 NEW: Add small overlay buttons IN the image area private func addBGROverlayButtons() { guard let _ = activePreviewWindow, let imageView = currentImageView, let imageContainer = imageView.superview else { print("❌ Cannot add BGR buttons: missing components") return } let buttonSize: CGFloat = 28 let buttonSpacing: CGFloat = 8 let bottomMargin: CGFloat = 12 // Position buttons at bottom center of image let totalButtonsWidth = (buttonSize * 2) + buttonSpacing let startX = (imageContainer.bounds.width - totalButtonsWidth) / 2 let buttonY = bottomMargin // Reset button (β†Ί) let resetButton = createBGROverlayButton( frame: NSRect(x: startX, y: buttonY, width: buttonSize, height: buttonSize), symbolName: "arrow.clockwise" ) resetButton.action = #selector(bgrResetButtonClicked) resetButton.target = self resetButton.isHidden = true // Initially hidden until processing is complete resetButton.tooltipText = "Reset" resetButton.tooltipPosition = .left resetButton.parentContainer = imageContainer // Toggle button (πŸ‘) let toggleButton = createBGROverlayButton( frame: NSRect(x: startX + buttonSize + buttonSpacing, y: buttonY, width: buttonSize, height: buttonSize), symbolName: "eye" ) toggleButton.action = #selector(bgrToggleButtonClicked) toggleButton.target = self toggleButton.isHidden = true // Initially hidden until processing is complete toggleButton.tooltipText = "Compare" toggleButton.tooltipPosition = .right toggleButton.parentContainer = imageContainer // Add to image container imageContainer.addSubview(resetButton) imageContainer.addSubview(toggleButton) // Store references bgrResetButton = resetButton bgrToggleButton = toggleButton print("🎨 BGR overlay buttons added to image container") } // 🎨 NEW: Add BGR progress bar in center of image private func addBGRProgressBar() { guard let _ = activePreviewWindow, let imageView = currentImageView, let imageContainer = imageView.superview else { print("❌ Cannot add BGR progress bar: missing components") return } // Progress container dimensions let containerWidth: CGFloat = 200 let containerHeight: CGFloat = 60 let containerX = (imageContainer.bounds.width - containerWidth) / 2 let containerY = (imageContainer.bounds.height - containerHeight) / 2 // Create progress container with glass effect let progressContainer = NSView(frame: NSRect(x: containerX, y: containerY, width: containerWidth, height: containerHeight)) progressContainer.wantsLayer = true progressContainer.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.7).cgColor progressContainer.layer?.cornerRadius = 12 progressContainer.layer?.masksToBounds = true progressContainer.layer?.borderColor = NSColor.white.withAlphaComponent(0.2).cgColor progressContainer.layer?.borderWidth = 1 // Create progress bar let progressBarWidth: CGFloat = 160 let progressBarHeight: CGFloat = 6 let progressBarX = (containerWidth - progressBarWidth) / 2 let progressBarY = containerHeight / 2 + 8 let progressBar = NSProgressIndicator(frame: NSRect(x: progressBarX, y: progressBarY, width: progressBarWidth, height: progressBarHeight)) progressBar.style = .bar progressBar.isIndeterminate = false progressBar.minValue = 0 progressBar.maxValue = 100 progressBar.doubleValue = 0 progressBar.wantsLayer = true progressBar.layer?.cornerRadius = 3 // Create progress label let progressLabel = NSTextField(frame: NSRect(x: 0, y: containerHeight / 2 - 20, width: containerWidth, height: 20)) progressLabel.stringValue = "Removing background... 0%" progressLabel.textColor = .white progressLabel.backgroundColor = .clear progressLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium) progressLabel.alignment = .center progressLabel.isBordered = false progressLabel.isEditable = false progressLabel.isSelectable = false // Add subviews progressContainer.addSubview(progressBar) progressContainer.addSubview(progressLabel) imageContainer.addSubview(progressContainer) // Store references bgrProgressContainer = progressContainer bgrProgressBar = progressBar bgrProgressLabel = progressLabel // Fade in animation progressContainer.alphaValue = 0 NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 progressContainer.animator().alphaValue = 1.0 } print("🎨 BGR progress bar added to image container") } // 🎨 NEW: Update BGR progress bar private func updateBGRProgress(_ progress: Double, status: String) { DispatchQueue.main.async { self.bgrProgressBar?.doubleValue = progress self.bgrProgressLabel?.stringValue = "\(status) \(Int(progress))%" } } // 🎨 NEW: Hide BGR progress bar private func hideBGRProgressBar() { guard let progressContainer = bgrProgressContainer else { return } NSAnimationContext.runAnimationGroup({ context in context.duration = 0.4 progressContainer.animator().alphaValue = 0 }) { progressContainer.removeFromSuperview() self.bgrProgressContainer = nil self.bgrProgressBar = nil self.bgrProgressLabel = nil } } // 🎨 NEW: Create clean BGR overlay button (icon only, no button styling) private func createBGROverlayButton(frame: NSRect, symbolName: String) -> BGROverlayButton { let button = BGROverlayButton(frame: frame) // Completely transparent button - no background or borders button.wantsLayer = true button.layer?.backgroundColor = NSColor.clear.cgColor // No button styling button.isBordered = false button.bezelStyle = .shadowlessSquare // SF Symbol icon with subtle shadow for visibility (20% smaller: 16pt -> 13pt) let symbolConfig = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold) button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)?.withSymbolConfiguration(symbolConfig) button.contentTintColor = .white // Add subtle shadow to icon for better visibility button.layer?.shadowColor = NSColor.black.cgColor button.layer?.shadowOpacity = 0.7 button.layer?.shadowRadius = 2 button.layer?.shadowOffset = CGSize(width: 0, height: -1) // Ensure tracking areas are set up button.updateTrackingAreas() return button } // 🎨 NEW: Start BGR processing automatically private func startBackgroundRemovalProcessing() { guard let originalImg = originalImage else { print("❌ No original image for BGR processing") return } print("🎨 Starting automatic BGR processing with user's preferred method...") // Show progress bar addBGRProgressBar() // Store processing result to be accessed by progress checker var processingResult: NSImage? = nil var isProcessingComplete = false // Start actual background removal processing BackgroundRemover.shared.processWithPreferredMethod(from: originalImg) { [weak self] result in processingResult = result isProcessingComplete = true print("🎨 BGR: Background removal processing completed with result: \(result != nil ? "success" : "failed")") } // Start progress simulation that waits for actual completion simulateBGRProgress( checkProcessingComplete: { isProcessingComplete }, getProcessingResult: { processingResult } ) { [weak self] finalResult in // This runs when both progress and processing are complete DispatchQueue.main.async { // Hide progress bar self?.hideBGRProgressBar() if let processedImg = finalResult { print("βœ… BGR processing completed successfully") self?.processedImage = processedImg // 🎨 NEW: Save processed image to file for dragging self?.saveProcessedImageToFile(processedImg) // Update image view to show processed image self?.currentImageView?.image = processedImg self?.isShowingProcessedImage = true // 🎨 NEW: Update DraggableImageView to use processed image URL self?.updateDraggableImageURL() // 🎨 NEW: Update filename label to show processed filename self?.updateBGRFilenameLabel() // Show overlay buttons self?.bgrResetButton?.isHidden = false self?.bgrToggleButton?.isHidden = false // Add subtle animation to reveal buttons NSAnimationContext.runAnimationGroup { context in context.duration = 0.5 self?.bgrResetButton?.animator().alphaValue = 1.0 self?.bgrToggleButton?.animator().alphaValue = 1.0 } } else { print("❌ BGR processing failed") // Keep original image showing, don't show overlay buttons } } } } // 🎨 NEW: Smart BGR progress that adapts to actual processing speed private func simulateBGRProgress( checkProcessingComplete: @escaping () -> Bool, getProcessingResult: @escaping () -> NSImage?, completion: @escaping (NSImage?) -> Void ) { let processingStartTime = Date() var currentProgress: Double = 0 // Start monitoring actual completion immediately func checkForEarlyCompletion() { let elapsedTime = Date().timeIntervalSince(processingStartTime) // Check if processing is already complete if checkProcessingComplete() { let result = getProcessingResult() print("πŸš€ FAST COMPLETION: BGR finished in \(String(format: "%.1f", elapsedTime)) seconds!") // Rapidly complete the progress bar finishProgressBar(with: result, completion: completion) return } // Update progress based on elapsed time with smart acceleration let targetProgress: Double if elapsedTime < 1.0 { // Very fast progress for Vision Framework targetProgress = min(70.0, elapsedTime * 70.0) updateBGRProgress(targetProgress, status: "Processing...") } else if elapsedTime < 2.0 { // Continue fast for Vision Framework targetProgress = 70.0 + ((elapsedTime - 1.0) * 15.0) updateBGRProgress(targetProgress, status: "Applying mask...") } else if elapsedTime < 3.0 { // Near completion for Vision Framework targetProgress = 85.0 + ((elapsedTime - 2.0) * 5.0) updateBGRProgress(targetProgress, status: "Finalizing...") } else { // Fallback for slower methods (RMBG with E5RT issues) targetProgress = min(90.0, 90.0 + ((elapsedTime - 3.0) * 0.5)) updateBGRProgress(targetProgress, status: "Processing...") } currentProgress = targetProgress // Check again in 0.2 seconds for very responsive updates DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { checkForEarlyCompletion() } } // Start progress immediately updateBGRProgress(5.0, status: "Starting...") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { checkForEarlyCompletion() } } // 🎨 NEW: Rapidly finish progress bar when processing completes private func finishProgressBar(with result: NSImage?, completion: @escaping (NSImage?) -> Void) { // Smoothly animate to 100% updateBGRProgress(95.0, status: "Complete!") DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self.updateBGRProgress(100.0, status: "Complete!") DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { completion(result) } } } // 🎨 NEW: Wait for actual BGR processing to complete private func waitForActualCompletion( checkComplete: @escaping () -> Bool, getResult: @escaping () -> NSImage?, completion: @escaping (NSImage?) -> Void ) { // Check every 0.5 seconds if processing is actually done func checkCompletion() { if checkComplete() { print("🎨 Actual BGR processing detected as complete!") let result = getResult() completion(result) } else { // Keep waiting and checking DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { checkCompletion() } } } checkCompletion() } // 🎨 NEW: Reset to main thumbnail (exit BGR mode completely) @objc private func bgrResetButtonClicked() { guard let originalImg = originalImage else { return } print("πŸ”„ BGR Reset: Exiting BGR mode and returning to main thumbnail") // Smooth animation to original image first NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 self.currentImageView?.animator().alphaValue = 0.7 }) { self.currentImageView?.image = originalImg NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 self.currentImageView?.animator().alphaValue = 1.0 }) { // After animation completes, exit BGR mode completely self.exitBackgroundRemovalMode() // Reset to normal thumbnail state self.showNormalThumbnail(with: originalImg) // πŸ”„ NEW: Handle post-action completion just like main thumbnail self.handleBGRActionCompletion(actionType: "reset") } } print("βœ… BGR Reset: Returned to main thumbnail - BGR buttons removed") } // 🎨 NEW: Show normal thumbnail (no BGR mode) private func showNormalThumbnail(with image: NSImage) { print("πŸ”„ Restoring normal thumbnail state...") // Reset all BGR-specific properties (already done in exitBackgroundRemovalMode) isBackgroundRemovalMode = false isShowingProcessedImage = false // Reset window level to normal if let window = activePreviewWindow { window.level = .floating print("🎨 Reset window level to normal (.floating)") } // πŸ”„ CRITICAL FIX: Enhanced URL validation and recovery if let originalURL = originalImageURL { print("πŸ” Checking original URL validity: \(originalURL.path)") // Verify file accessibility if FileManager.default.fileExists(atPath: originalURL.path) { // File exists - set it properly currentImageView?.imageURL = originalURL print("βœ… DraggableImageView reset to verified original URL: \(originalURL.lastPathComponent)") // Double verify by trying to load the image if NSImage(contentsOf: originalURL) != nil { print("βœ… Original image file loads successfully") } else { print("⚠️ WARNING: Original URL exists but image won't load") } } else { print("❌ CRITICAL: Original file missing at reset time: \(originalURL.path)") // RECOVERY: Try multiple fallback strategies var recoveredURL: URL? = nil // Strategy 1: Check if it's in temp directory let tempDir = FileManager.default.temporaryDirectory let fileName = originalURL.lastPathComponent let tempURL = tempDir.appendingPathComponent(fileName) if FileManager.default.fileExists(atPath: tempURL.path) { recoveredURL = tempURL print("πŸ”„ RECOVERY: Found file in temp directory: \(tempURL.path)") } // Strategy 2: Use DraggableImageView's current URL if recoveredURL == nil, let fallbackURL = currentImageView?.imageURL { if FileManager.default.fileExists(atPath: fallbackURL.path) { recoveredURL = fallbackURL print("πŸ”„ RECOVERY: Using DraggableImageView fallback URL: \(fallbackURL.path)") } } // Strategy 3: Try to recreate the file from the current image if recoveredURL == nil { print("πŸ”„ RECOVERY: Attempting to save current image to new temp file...") let newTempURL = FileManager.default.temporaryDirectory.appendingPathComponent("recovered_\(UUID().uuidString).png") if let imageData = image.pngData() { do { try imageData.write(to: newTempURL) recoveredURL = newTempURL print("βœ… RECOVERY: Created new temp file: \(newTempURL.path)") } catch { print("❌ RECOVERY: Failed to save recovery file: \(error)") } } } // Apply the recovered URL if let finalURL = recoveredURL { originalImageURL = finalURL currentImageView?.imageURL = finalURL print("βœ… URL RECOVERED: Now using \(finalURL.lastPathComponent)") } else { print("❌ RECOVERY FAILED: No valid URL available") } } } else { print("❌ ERROR: No originalImageURL available for normal thumbnail!") // Last resort: Use DraggableImageView's current URL if let fallbackURL = currentImageView?.imageURL { originalImageURL = fallbackURL print("πŸ”„ EMERGENCY: Set originalImageURL from DraggableImageView: \(fallbackURL.lastPathComponent)") } } // Reset filename label to original if let finalURL = originalImageURL, let filenameLabel = delegate?.findFilenameLabel(in: activePreviewWindow) { filenameLabel.stringValue = finalURL.lastPathComponent filenameLabel.toolTip = finalURL.lastPathComponent print("🎨 Reset filename label to: \(finalURL.lastPathComponent)") } print("βœ… Normal thumbnail state restored - ready for new actions") } // 🎨 NEW: Toggle between original and processed @objc private func bgrToggleButtonClicked() { guard let originalImg = originalImage, let processedImg = processedImage else { return } // Determine current state and toggle let nextImage = isShowingProcessedImage ? originalImg : processedImg isShowingProcessedImage = !isShowingProcessedImage let nextSymbol = isShowingProcessedImage ? "eye.slash" : "eye" print("πŸ”„ BGR Toggle: Switching to \(isShowingProcessedImage ? "processed" : "original") image") // Update image with animation NSAnimationContext.runAnimationGroup({ context in context.duration = 0.3 self.currentImageView?.animator().alphaValue = 0.7 }) { self.currentImageView?.image = nextImage // 🎨 NEW: Update DraggableImageView URL for proper dragging self.updateDraggableImageURL() // 🎨 NEW: Update filename label to reflect current image self.updateBGRFilenameLabel() NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 self.currentImageView?.animator().alphaValue = 1.0 } } // Update toggle button icon let symbolConfig = NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold) bgrToggleButton?.image = NSImage(systemSymbolName: nextSymbol, accessibilityDescription: nil)?.withSymbolConfiguration(symbolConfig) } // 🎨 NEW: Exit BGR mode and return to normal thumbnail func exitBackgroundRemovalMode() { print("🎨 Exiting BGR mode") // πŸ”„ PRESERVE original image and URL for normal thumbnail to continue working let preservedOriginalImage = originalImage let preservedOriginalURL = originalImageURL // Use the force reset to clean everything up forceCompleteBGRReset() // πŸ”„ CRITICAL: Restore the preserved values for normal thumbnail functionality originalImage = preservedOriginalImage originalImageURL = preservedOriginalURL print("βœ… Preserved originalImage and originalImageURL for normal thumbnail: \(originalImageURL?.lastPathComponent ?? "nil")") // Verify the file still exists and report detailed status if let originalURL = originalImageURL { if FileManager.default.fileExists(atPath: originalURL.path) { print("βœ… VERIFIED: Original file exists and is accessible: \(originalURL.path)") } else { print("❌ CRITICAL: Original file missing after BGR reset: \(originalURL.path)") // Try to get alternative URL from DraggableImageView if let fallbackURL = currentImageView?.imageURL { print("πŸ”„ Attempting fallback URL: \(fallbackURL.path)") if FileManager.default.fileExists(atPath: fallbackURL.path) { originalImageURL = fallbackURL print("βœ… Using fallback URL: \(fallbackURL.path)") } } } } } // 🎨 NEW: Save processed image to file for dragging private func saveProcessedImageToFile(_ image: NSImage) { guard let originalURL = originalImageURL else { print("❌ No original image URL found to save processed image") return } // Create processed image filename let originalFilename = originalURL.deletingPathExtension().lastPathComponent let processedFilename = "\(originalFilename)_BGR.png" let processedURL = originalURL.deletingLastPathComponent().appendingPathComponent(processedFilename) // Save processed image to file do { if let pngData = image.pngData() { try pngData.write(to: processedURL) self.processedImageURL = processedURL print("βœ… Processed image saved to: \(processedURL.path)") } else { print("❌ Failed to convert processed image to PNG data") } } catch { print("❌ Error saving processed image: \(error)") } } // 🎨 NEW: Update DraggableImageView to use processed image URL private func updateDraggableImageURL() { guard isBackgroundRemovalMode else { return } // Use the correct URL based on which image is currently showing let currentURL = isShowingProcessedImage ? processedImageURL : originalImageURL // Update DraggableImageView to use the correct URL if let url = currentURL { currentImageView?.imageURL = url print("βœ… DraggableImageView updated to use \(isShowingProcessedImage ? "processed" : "original") image URL: \(url.lastPathComponent)") } else { print("❌ No URL available for \(isShowingProcessedImage ? "processed" : "original") image") } } // 🎨 NEW: Update filename label to show original filename in BGR mode private func updateBGRFilenameLabel() { guard let originalURL = originalImageURL else { print("❌ No original image URL found to update filename label") return } // Determine which filename to show based on current image let displayURL: URL if isShowingProcessedImage, let processedURL = processedImageURL { displayURL = processedURL } else { displayURL = originalURL } // Update filename label to show correct filename in BGR mode if let filenameLabel = delegate?.findFilenameLabel(in: activePreviewWindow) { filenameLabel.stringValue = displayURL.lastPathComponent filenameLabel.toolTip = displayURL.lastPathComponent print("🎨 BGR: Updated filename label to: \(displayURL.lastPathComponent)") } else { print("❌ No filename label found in active preview window") } } // 🎨 NEW: Handle post-action completion just like main thumbnail private func handleBGRActionCompletion(actionType: String) { print("🎨 BGR Action Completion: Handling '\(actionType)' action completion") // BGR reset action should behave like a "cancel" action - no specific closing behavior // The user explicitly chose to go back to the main thumbnail, so keep it visible print("ℹ️ BGR reset action: Keeping thumbnail visible after reset") } // πŸ”„ CRITICAL: Force complete BGR state reset for new screenshots private func forceCompleteBGRReset() { print("πŸ”„ FORCE RESET: Starting complete BGR state reset...") // 1. Force hide any hanging tooltips immediately bgrResetButton?.forceHideTooltip() bgrToggleButton?.forceHideTooltip() // 2. Remove BGR overlay buttons immediately bgrResetButton?.removeFromSuperview() bgrToggleButton?.removeFromSuperview() bgrResetButton = nil bgrToggleButton = nil // 3. Remove BGR progress bar immediately if let progressContainer = bgrProgressContainer { progressContainer.removeFromSuperview() bgrProgressContainer = nil bgrProgressBar = nil bgrProgressLabel = nil } // 4. Clean up BGR file references if let processedURL = processedImageURL { try? FileManager.default.removeItem(at: processedURL) print("πŸ—‘οΈ Force cleaned processed image file: \(processedURL.lastPathComponent)") } processedImageURL = nil // 5. Reset all BGR mode properties to clean state isBackgroundRemovalMode = false isBGRTransition = false // πŸ”„ NEW: Also reset transition flag originalImage = nil processedImage = nil originalImageURL = nil isShowingProcessedImage = false // 6. Reset window level if needed if let window = activePreviewWindow { window.level = .floating print("πŸ”„ Reset window level to normal (.floating)") } print("βœ… FORCE RESET: Complete BGR state reset finished - ready for normal thumbnail") } }