import AppKit // MARK: - Grid Window for Action Selection class GridWindow: NSWindow, NSDraggingDestination { var cellViews: [GridCellView] = [] weak var gridViewManagerDelegate: GridViewManagerDelegate? weak var manager: GridViewManager? private var currentlyHighlightedCell: GridCellView? private var fadeTimer: Timer? private let fadeStart: CGFloat = 50 // afstand in px waarbij fading start private let fadeEnd: CGFloat = 300 // afstand waarbij minimale alpha bereikt is private let minAlpha: CGFloat = 0 // minimale zichtbaarheid var isInitialFadingIn: Bool = false var isPerformingProgrammaticHide: Bool = false // NIEUWE FLAG init(screen: NSScreen, cellsPerRowInput: Int = 2, manager: GridViewManager, previewFrame: NSRect?) { self.manager = manager let settings = SettingsManager.shared var activeActions: [(index: Int, text: String)] = [] // Build actions based on settings actionOrder to respect user's preferred order 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 if BackgroundRemover.shared.isRMBGModelAvailable() { displayText = "Remove BG" } else { displayText = "Remove BG" } case .cancel: isEnabled = settings.isCancelActionEnabled displayText = "Cancel" case .remove: isEnabled = settings.isRemoveActionEnabled displayText = "Remove" } if isEnabled { // Use gridIndex as the cellIndex, which will map to actionOrder position activeActions.append((gridIndex, displayText)) } } let numberOfActiveActions = activeActions.count guard numberOfActiveActions > 0 else { // Geen acties actief, maak een leeg/onzichtbaar window of handle anders // Voor nu, een heel klein, onzichtbaar venster en return vroeg. // Dit voorkomt een crash als numberOfCells 0 is. super.init(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: false) self.isOpaque = false self.backgroundColor = .clear self.ignoresMouseEvents = true // Belangrijk // Roep manager.hideGrid() aan omdat er geen grid is om te tonen DispatchQueue.main.async { manager.hideGrid() } return } print("๐Ÿ”ท GridWindow init: Number of active actions = \(numberOfActiveActions)") // Print na guard // Verticale ActionBar: altijd 1 kolom 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 // Oude, incorrecte positionering verwijderd if let pFrame = previewFrame { let screenVisibleFrame = screen.visibleFrame yPosition = pFrame.maxY + spacing xPosition = screenVisibleFrame.maxX - calculatedGridWidth + 10 // Bounds checking if xPosition < screenVisibleFrame.minX + spacing { xPosition = screenVisibleFrame.minX + spacing } if xPosition + calculatedGridWidth > screenVisibleFrame.maxX - spacing { xPosition = screenVisibleFrame.maxX - calculatedGridWidth - spacing } if yPosition + calculatedGridHeight > screenVisibleFrame.maxY - spacing { yPosition = pFrame.minY - calculatedGridHeight - spacing } if yPosition < screenVisibleFrame.minY + spacing { yPosition = screenVisibleFrame.minY + spacing } } else { let effectiveScreenFrame = screen.visibleFrame xPosition = (effectiveScreenFrame.width - calculatedGridWidth) / 2 + effectiveScreenFrame.origin.x yPosition = (effectiveScreenFrame.height - calculatedGridHeight) / 2 + effectiveScreenFrame.origin.y } let contentRect = NSRect(x: xPosition, y: yPosition, width: calculatedGridWidth, height: calculatedGridHeight) super.init(contentRect: contentRect, styleMask: [.borderless], backing: .buffered, defer: false) self.level = .floating + 2 print("๐Ÿ”ท GridWindow init: Calculated contentRect = \(contentRect)") // Print na super.init self.isOpaque = false self.backgroundColor = .clear self.hasShadow = false // Was false, houd consistent self.ignoresMouseEvents = false self.acceptsMouseMovedEvents = true // BELANGRIJK: Voor hover events in subviews let containerView = NSView(frame: NSRect(origin: .zero, size: contentRect.size)) containerView.wantsLayer = true // Gedeelde achtergrond voor alle iconen (dubbele blur) 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] // Alle hoeken afgerond containerView.layer?.masksToBounds = true self.contentView = containerView // Maak cellen alleen voor actieve acties for (gridIndex, action) in activeActions.enumerated() { let col = gridIndex % cellsPerRow let row = gridIndex / cellsPerRow let cellX = spacing + CGFloat(col) * (fixedCellWidth + spacing) // Y-positie berekend van boven naar beneden voor de grid let cellY = calculatedGridHeight - spacing - CGFloat(row + 1) * fixedCellHeight - CGFloat(row) * spacing let cellFrame = NSRect(x: cellX, y: cellY, width: fixedCellWidth, height: fixedCellHeight) // Gebruik de 'index' van de actie (0 voor Rename, 1 voor Stash, etc.) voor de delegate let cellView = GridCellView(frame: cellFrame, index: action.index, text: action.text) containerView.addSubview(cellView) cellViews.append(cellView) } print("๐Ÿ”ท GridWindow init: Number of cellViews created = \(cellViews.count)") // Print na cell creatie registerForDraggedTypes([.fileURL]) // Start timer voor dynamische transparantie fadeTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { [weak self] _ in self?.updateAlphaBasedOnCursor() }) } deinit { fadeTimer?.invalidate() } private func updateAlphaBasedOnCursor() { guard !isPerformingProgrammaticHide else { return } // NIEUWE CHECK // NIEUW: Als monitoring uitgeschakeld is (rename actief), houd grid vol zichtbaar if let manager = self.manager, manager.isMonitoringDisabled { if abs(self.alphaValue - 1.0) > 0.01 { self.alphaValue = 1.0 // Forceer volle alpha } return // Stop verdere alpha berekening } guard !isInitialFadingIn else { return } // <-- CHECK DE FLAG guard let screenPoint = NSEvent.mouseLocation as NSPoint? else { return } let windowFrame = self.frame let distance: CGFloat if windowFrame.contains(screenPoint) { distance = 0 } else { // Bereken kortste afstand van punt naar rechthoek 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) } // Stel alpha direct in zonder animator voor snellere respons if abs(self.alphaValue - newAlpha) > 0.01 { self.alphaValue = newAlpha } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - NSDraggingDestination Methods (HERSTEL DE IMPLEMENTATIES) func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { // Geef aan dat we een kopieer-operatie accepteren return .copy } func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { let dropLocationInScreen = sender.draggingLocation // Converteer naar coรถrdinaten binnen de content view van het grid window guard let dropLocationInContent = self.contentView?.convert(dropLocationInScreen, from: nil) else { // Als conversie faalt, doe niets (of reset highlight) currentlyHighlightedCell?.setHighlighted(false) currentlyHighlightedCell = nil return [] } var foundCell: GridCellView? = nil // Zoek de cel onder de cursor for cell in cellViews { if cell.frame.contains(dropLocationInContent) { foundCell = cell break } } // Update highlighting alleen als de cel verandert if currentlyHighlightedCell !== foundCell { currentlyHighlightedCell?.setHighlighted(false) currentlyHighlightedCell?.setHovered(false) foundCell?.setHighlighted(true) foundCell?.setHovered(true) currentlyHighlightedCell = foundCell } // Geef aan dat we nog steeds een kopieer-operatie accepteren return .copy } func performDragOperation(_ sender: NSDraggingInfo) -> Bool { // Haal de bestands URL op van het gesleepte item guard let pasteboard = sender.draggingPasteboard.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray, let path = pasteboard[0] as? String else { manager?.hideGrid(monitorForReappear: true) // Verberg grid als we de data niet kunnen lezen return false } let imageURL = URL(fileURLWithPath: path) let dropLocationInContent = self.contentView?.convert(sender.draggingLocation, from: nil) ?? .zero // Verwijder highlight currentlyHighlightedCell?.setHighlighted(false) currentlyHighlightedCell = nil // Zoek de cel waarop gedropt is for cell in cellViews { if cell.frame.contains(dropLocationInContent) { // Roep de delegate aan als deze bestaat if let manager = self.manager, let delegate = manager.delegate { print("โœ… GridWindow: Detected drop on cell \(cell.index). Calling delegate...") delegate.gridViewManager(manager, didDropImage: imageURL, ontoCell: cell.index, at: dropLocationInContent) // Het verbergen van de grid wordt nu afgehandeld door de delegate completion! return true // Succesvolle drop } else { print("โŒ GridWindow: Manager or delegate is nil for drop on cell \(cell.index)!") } // Als manager of delegate nil is, faalt de operatie voor deze cel return false } } // Als er niet op een cel is gedropt manager?.hideGrid(monitorForReappear: true) return false // Geen succesvolle drop } func draggingExited(_ sender: NSDraggingInfo?) { // Verwijder highlight en verberg grid als de cursor het window verlaat currentlyHighlightedCell?.setHighlighted(false) currentlyHighlightedCell?.setHovered(false) currentlyHighlightedCell = nil // Grid blijft zichtbaar; verbergen gebeurt elders als drag eindigt. } // mouseUp override (om grid te sluiten bij klikken buiten cellen) override func mouseUp(with event: NSEvent) { super.mouseUp(with: event) if !event.isARepeat { // NIEUW: Controleer of monitoring is uitgeschakeld (rename actief) guard let manager = self.manager, !manager.isMonitoringDisabled else { print("๐Ÿ”ถ GridWindow: MouseUp ignored, monitoring is disabled (rename active).") return } let locationInWindow = self.contentView?.convert(event.locationInWindow, from: nil) ?? .zero let cellUnderMouse = cellViews.first { $0.frame.contains(locationInWindow) } if cellUnderMouse == nil { manager.hideGrid(monitorForReappear: true) } } } } // MARK: - Grid View Manager class GridViewManager { var gridWindow: GridWindow? weak var delegate: GridViewManagerDelegate? // Laatste bekende positie waarop de grid rond de preview werd getoond private var lastPreviewFrame: NSRect? private var reappearanceTimer: Timer? // Flag om monitoring uit te schakelen tijdens rename operaties (internal access) var isMonitoringDisabled: Bool = false // GEFIXT: Explicit false initialization + reset in init private var isDragSessionActive: Bool = false // 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.isMonitoringDisabled = false self.lastPreviewFrame = nil print("๐Ÿ”ถ GridViewManager: Initialized with clean state - isDragSessionActive: \(self.isDragSessionActive)") } // MARK: - Show Grid func showGrid(previewFrame: NSRect?) { print("๐Ÿ”ถ MAIN DEBUG: GridViewManager: showGrid called for MAIN THUMBNAIL") print("๐Ÿ”ถ MAIN DEBUG: This is MAIN app grid (NOT stash grid)") print("๐Ÿ”ถ MAIN DEBUG: PreviewFrame: \(String(describing: previewFrame))") // Annuleer eventueel lopende timers โ€“ grid is alweer zichtbaar reappearanceTimer?.invalidate(); reappearanceTimer = nil self.lastPreviewFrame = previewFrame if let existingWindow = gridWindow { print("๐Ÿ”ถ MAIN DEBUG: Closing existing main grid window") existingWindow.isPerformingProgrammaticHide = false // Reset voor het geval het vastzat existingWindow.orderOut(nil as Any?) self.gridWindow = nil } // Bepaal het juiste scherm op basis van waar de thumbnail zich bevindt let screen: NSScreen if let pFrame = previewFrame { // Zoek het scherm dat de thumbnail bevat let thumbnailCenter = NSPoint(x: pFrame.midX, y: pFrame.midY) if let thumbnailScreen = NSScreen.screens.first(where: { $0.frame.contains(thumbnailCenter) }) { screen = thumbnailScreen print("๐Ÿ”ถ MAIN DEBUG: Using thumbnail screen for main grid: \(thumbnailScreen.localizedName)") } else { // Fallback naar hoofdscherm als thumbnail scherm niet gevonden screen = NSScreen.main ?? NSScreen.screens.first! print("๐Ÿ”ถ MAIN DEBUG: Thumbnail screen not found, using fallback: \(screen.localizedName)") } } else { // Geen preview frame, gebruik hoofdscherm screen = NSScreen.main ?? NSScreen.screens.first! print("๐Ÿ”ถ MAIN DEBUG: No preview frame, using main screen: \(screen.localizedName)") } print("๐Ÿ”ถ MAIN DEBUG: Creating NEW GridWindow (main app grid, not stash)") gridWindow = GridWindow(screen: screen, manager: self, previewFrame: previewFrame) gridWindow?.gridViewManagerDelegate = self.delegate gridWindow?.alphaValue = 0 gridWindow?.makeKeyAndOrderFront(nil as Any?) // Zorg ervoor dat het key window wordt voor events gridWindow?.orderFrontRegardless() gridWindow?.isInitialFadingIn = true // <-- ZET FLAG VOOR ANIMATIE print("๐Ÿ”ถ MAIN DEBUG: Animating main grid appearance") NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 1.0 // AANGEPAST van 0.2 naar 1.0 self.gridWindow?.animator().alphaValue = 1 } completionHandler: { // <-- COMPLETION HANDLER TOEGEVOEGD self.gridWindow?.isInitialFadingIn = false // <-- RESET FLAG NA ANIMATIE } } // MARK: - Hide Grid func hideGrid(monitorForReappear: Bool = false) { print("๐Ÿ”ถ MAIN DEBUG: GridViewManager: hideGrid called for MAIN THUMBNAIL") print("๐Ÿ”ถ MAIN DEBUG: monitorForReappear = \(monitorForReappear)") guard let window = gridWindow else { print("๐Ÿ”ถ MAIN DEBUG: No main grid window to hide") return } window.isPerformingProgrammaticHide = true // ZET FLAG VOOR ANIMATIE // Stop een bestaande timer zodat we niet meerdere tegelijk hebben if !monitorForReappear { reappearanceTimer?.invalidate(); reappearanceTimer = nil } print("๐Ÿ”ถ MAIN DEBUG: Hiding main grid window") NSAnimationContext.runAnimationGroup({ ctx in ctx.duration = 0.8 // Teruggezet naar 0.8s window.animator().alphaValue = 0 }, completionHandler: { [weak self] in guard let self = self else { return } print("๐Ÿ”ถ MAIN DEBUG: Main grid hide animation complete") window.orderOut(nil as Any?) if self.gridWindow === window { self.gridWindow = nil } // Reset flag na animatie en orderOut, maar VOOR potentiรซle startReappearanceMonitor // Echter, window referentie is nu mogelijk nil als self.gridWindow === window was. // Het is veiliger om de flag te resetten via een referentie die nog geldig is, of de logica herzien. // Voor nu: als window nog bestaat (niet de self.gridWindow was die nil werd), reset het. // Maar de window instance zelf wordt niet direct nil. We kunnen het nog steeds gebruiken. window.isPerformingProgrammaticHide = false // Start monitor na het verbergen ALLEEN als monitoring niet uitgeschakeld is if monitorForReappear && !self.isMonitoringDisabled { self.startReappearanceMonitor() } }) } // NIEUW: Methodes om monitoring te controleren func disableMonitoring() { print("๐Ÿ”ถ GridViewManager: Monitoring disabled (e.g., during rename)") isMonitoringDisabled = true // NIEUW: Stop de reappearance timer volledig reappearanceTimer?.invalidate() reappearanceTimer = nil print("๐Ÿ”ถ GridViewManager: Reappearance timer stopped and invalidated") } func enableMonitoring() { print("๐Ÿ”ถ GridViewManager: Monitoring enabled") isMonitoringDisabled = false // GEEN automatische herstart van timer hier - alleen als grid opnieuw wordt verborgen } // NIEUW: Start drag session - schakelt proximity monitoring in func startDragSession() { print("๐Ÿ”ถ GridViewManager: Drag session started - enabling proximity monitoring") isDragSessionActive = true } // NIEUW: Stop drag session - schakelt proximity monitoring uit func stopDragSession() { print("๐Ÿ”ถ GridViewManager: Drag session ended - disabling proximity monitoring") isDragSessionActive = false // Stop proximity timer als er geen drag actief is reappearanceTimer?.invalidate() reappearanceTimer = nil } // MARK: - Monitoring Logic private func startReappearanceMonitor() { // GEFIXT: Check of monitoring uitgeschakeld is EN of er een actieve drag sessie is // CRITICAL: Only start proximity monitoring during active drag sessions guard !isMonitoringDisabled && isDragSessionActive else { print("๐Ÿ”ถ GridViewManager: Skipping reappearance monitor - monitoring disabled: \(isMonitoringDisabled), drag active: \(isDragSessionActive)") print("๐Ÿ”ถ GridViewManager: This prevents grid from triggering without actual drag") return } // Safety: invalideer vorige timer reappearanceTimer?.invalidate() guard let targetFrame = lastPreviewFrame else { print("๐Ÿ”ถ GridViewManager: No previewFrame โ€“ skipping reappearance monitor.") return } print("๐Ÿ”ถ GridViewManager: Starting proximity monitoring (only during active drag)") reappearanceTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in self?.evaluateMouseProximity(to: targetFrame) } RunLoop.main.add(reappearanceTimer!, forMode: .common) } private func evaluateMouseProximity(to frame: NSRect) { // GEFIXT: Check of monitoring uitgeschakeld is EN of er een actieve drag sessie is // CRITICAL: Only evaluate proximity during active drag sessions guard !isMonitoringDisabled && isDragSessionActive else { print("๐Ÿ”ถ GridViewManager: Skipping proximity evaluation - monitoring disabled: \(isMonitoringDisabled), drag active: \(isDragSessionActive)") // NIEUW: Stop timer when drag session is not active reappearanceTimer?.invalidate() reappearanceTimer = nil return } // Bereken een vergrote zone (200 px marge) rondom de preview let expansion: CGFloat = 200 let enlarged = frame.insetBy(dx: -expansion, dy: -expansion) let currentLoc = NSEvent.mouseLocation if enlarged.contains(currentLoc) { // Cursor is weer in de buurt โ€“ toon grid opnieuw print("๐Ÿ”ถ GridViewManager: Mouse near preview โ€“ showing grid again.") self.showGrid(previewFrame: self.lastPreviewFrame) } } }