import AppKit import HotKey import Sparkle import UniformTypeIdentifiers import SwiftUI import ServiceManagement import UserNotifications // Grid-gerelateerde klassen en protocol protocol GridViewManagerDelegate: AnyObject { func gridViewManager(_ manager: GridViewManager, didDropImage imageURL: URL, ontoCell cellIndex: Int, at dropPoint: NSPoint) func getActiveWindowForGridPositioning() -> NSWindow? // Nodig voor positionering GridWindow } // Make ScreenshotApp conform to DraggableImageViewClickHandler class ScreenshotApp: NSObject, NSApplicationDelegate, RenameActionHandlerDelegate, GridViewManagerDelegate, DraggableImageViewClickHandler, MenuManagerDelegate, PreviewManagerDelegate, GridActionManagerDelegate { var hotKey: HotKey! var lastImage: NSImage? // 🎯 NEW: Thumbnail restoration backup system for thumbnail restoration after settings apply private var backupImagePath: String? private var backupImage: NSImage? // NIEUW: Runtime Cmd monitoring voor all screens toggle var activePreviewWindow: NSWindow? // Het standaard screenshot preview venster private var currentImageView: DraggableImageView? var tempURL: URL? private var isClosing = false private var menuManager: MenuManager! // NIEUW: MenuManager instance private var previewManager: PreviewManager! // NIEUW: PreviewManager instance private var gridActionManager: GridActionManager! // NIEUW: GridActionManager instance var gridViewManager: GridViewManager? var didGridHandleDrop = false // Flag om dubbele events te voorkomen // πŸ”„ NEW: Flag voor stash grid action handling var didStashGridHandleDrop = false private var isPreviewUpdating = false private var previewDismissTimer: Timer? var renameActionHandler: RenameActionHandler! var activeOverlayWindow: OverlayWindow? private var previewWasVisibleBeforeSettingsOpen: Bool = false let settings = SettingsManager.shared // <-- TOEGEVOEGD: SettingsManager instance // πŸ§ͺ NEW: Cache cleanup timer for dynamic scheduling private var cacheCleanupTimer: Timer? // NIEUW: Provider voor ScreenCaptureKit (Intel Mac compatible) let screenCaptureProvider: ScreenCaptureKitProvider? = { if #available(macOS 12.3, *) { return ScreenCaptureKitProvider() } else { print("⚠️ ScreenCaptureKit requires macOS 12.3 or later (Intel Mac fallback)") return nil } }() // Houd de window controller en de image store voor het Stash venster bij private var stashWindowController: NSWindowController? var activeStashImageStore: GalleryImageStore? // πŸ”₯ NIEUW: Persistent stash image store voor persistent images private var persistentStashImageStore: GalleryImageStore? // πŸ”„ UPDATE: Update Manager for Sparkle functionality private let updateManager = UpdateManager.shared private var activeSettingsWindow: SettingsWindow? // NIEUW: Referentie naar open instellingenvenster private let launchHelperBundleID = "com.shotscreen.launchhelper" // Definieer de Bundle ID van de helper private var firstLaunchWizard: FirstLaunchWizard? // NIEUW: First launch wizard window // NIEUW: FinderWindowManager voor het bewaren en herstellen van Finder vensters private var finderWindowManager: FinderWindowManager! // NIEUW: Thumbnail directory voor permanente screenshot opslag private lazy var thumbnailDirectory: URL = { let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") let thumbnailsDirectory = shotScreenDirectory.appendingPathComponent("Thumbnails") // Maak de directory aan als die niet bestaat try? FileManager.default.createDirectory(at: thumbnailsDirectory, withIntermediateDirectories: true, attributes: nil) // 🎯 NEW: Ensure thumbnail restoration backup directory exists let restorationDirectory = thumbnailsDirectory.appendingPathComponent("thumbnail_restoration") try? FileManager.default.createDirectory(at: restorationDirectory, withIntermediateDirectories: true, attributes: nil) return thumbnailsDirectory }() // MARK: - Multi-Monitor Support Properties var overlayWindows: [NSWindow] = [] var windowScreenMap: [NSWindow: NSScreen] = [:] var isDragging = false var startPoint: NSPoint = NSZeroPoint var currentEndPoint: NSPoint = NSZeroPoint var globalMouseDownMonitor: Any? var globalMouseDragMonitor: Any? var globalMouseUpMonitor: Any? var localMouseDownMonitor: Any? var localMouseDragMonitor: Any? var localMouseUpMonitor: Any? // MARK: - Multi-Monitor State var isMultiMonitorSelectionActive = false var isAllScreensCaptureToggledOn = false // NIEUW: State voor getogglede "alle schermen" modus var lastKnownModifierFlags: NSEvent.ModifierFlags = NSEvent.ModifierFlags() // Nieuwe property voor Command-toggle // MARK: - Event Capture Overlay var eventCaptureWindow: NSWindow? // MARK: - Performance Optimization var lastUpdateTime: CFTimeInterval = 0 let updateThrottle: CFTimeInterval = 1.0 / 120.0 // 120 FPS max // MARK: - Mouse Click Detection var mouseDownLocation: NSPoint = NSZeroPoint var mouseDownTime: TimeInterval = 0 var hasMouseMoved = false var isAllScreenModifierPressed = false // MARK: - Additional Multi-Monitor Properties var globalKeyMonitor: Any? var globalMouseMonitor: Any? var crosshairWindows: [NSWindow] = [] var crosshairTrackingMonitor: Any? var eventCaptureWindows: [NSWindow] = [] // Reset tracking variables for clean state func resetTrackingVariables() { mouseDownTime = 0 mouseDownLocation = NSZeroPoint hasMouseMoved = false isAllScreenModifierPressed = false print("πŸ”„ Tracking variables reset for clean state") } func applicationDidFinishLaunching(_ notification: Notification) { // πŸ”§ DEBUG: Check for command line arguments let arguments = CommandLine.arguments if arguments.contains("--force-debug-update") { print("πŸ”§ DEBUG: Force debug update mode detected") performForceDebugUpdate() return // Don't continue with normal app initialization } hotKey = HotKey(key: .four, modifiers: [.command, .shift]) hotKey.keyDownHandler = { [weak self] in print("πŸ”₯ Hotkey pressed - launching native macOS screencapture") self?.activateMultiMonitorSelection() } renameActionHandler = RenameActionHandler(delegate: self) // NIEUW: Initialize MenuManager menuManager = MenuManager(delegate: self) menuManager.setupMenu() // NIEUW: Initialize PreviewManager previewManager = PreviewManager(delegate: self) // NIEUW: Initialize GridActionManager gridActionManager = GridActionManager(delegate: self) // NIEUW: Initialize FinderWindowManager finderWindowManager = FinderWindowManager() // NIEUW: Initialize WindowCaptureManager initializeWindowCaptureManager() gridViewManager = GridViewManager() gridViewManager?.delegate = self // NIEUW: Use cache management system for thumbnail cleanup instead of fixed count limit // cleanupOldThumbnails() - REMOVED: This limited to 10 files, use cache retention instead // MARK: - Multi-Monitor Setup print("πŸš€ Screenshot System Started with Double Hotkey Detection!") print("πŸ“ Press Cmd+Shift+4 for normal screenshot selection") print(" 🎯 Press Cmd+Shift+4 TWICE (within 1 second) = capture whole screen under cursor") print(" 🎯 Single press = normal selection capture") print(" πŸͺŸ Spatiebalk during drag = window capture") print(" 🚫 ESC = cancel (no thumbnail!)") print("🎯 Double hotkey = instant whole screen capture!") // List available screens listAvailableScreens() // Setup global mouse tracking for multi-monitor support setupGlobalMouseTracking() // Registreer voor setting change notificaties registerForSettingsChanges() // Pas opstarten bij login toe op basis van instelling toggleLaunchAtLogin(shouldLaunch: SettingsManager.shared.startAppOnLogin) // Observeer wijzigingen in de opstartinstelling NotificationCenter.default.addObserver(self, selector: #selector(handleStartAppOnLoginChanged), name: .startAppOnLoginSettingChanged, object: nil) // NIEUW: Add observers for grid actions NotificationCenter.default.addObserver(self, selector: #selector(handleGridCancelAction), name: .gridActionCancelRequested, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleGridRemoveAction), name: .gridActionRemoveRequested, object: nil) // NIEUW: Check for first launch and show wizard checkAndShowFirstLaunchWizard() // NIEUW: Setup initial hotkey based on settings setupHotKey() // πŸ”„ NIEUW: Initialize automatic updates for direct sales setupAutomaticUpdates() // πŸ—‚οΈ NIEUW: Perform cache cleanup on startup performStartupCacheCleanup() // πŸ” LICENSE: Initialize license system initializeLicenseSystem() } // VERWIJDERD: cleanupOldThumbnails() - Deze functie beperkte thumbnails tot 10 bestanden // Nu gebruikt de app het cache retention systeem dat gebaseerd is op tijd (bijv. 1 uur) // Alle thumbnails blijven bewaard totdat de ingestelde retention tijd verstreken is func registerForSettingsChanges() { NotificationCenter.default.addObserver(self, selector: #selector(handleCloseAfterDragChanged), name: .closeAfterDragSettingChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleCloseAfterSaveChanged), name: .closeAfterSaveSettingChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handlePlaySoundOnCaptureChanged), name: .playSoundOnCaptureSettingChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStashAlwaysOnTopChanged), name: .stashAlwaysOnTopSettingChanged, object: nil) // πŸ”₯ NIEUW: Observer for persistent stash setting changes NotificationCenter.default.addObserver(self, selector: #selector(handlePersistentStashChanged), name: .persistentStashChanged, object: nil) // NIEUW: Observer for shortcut changes NotificationCenter.default.addObserver(self, selector: #selector(handleShortcutSettingChanged), name: .shortcutSettingChanged, object: nil) // NIEUW: Observer for desktop icons setting changes NotificationCenter.default.addObserver(self, selector: #selector(handleHideDesktopIconsChanged), name: .hideDesktopIconsSettingChanged, object: nil) // πŸ§ͺ NIEUW: Observer for cache retention setting changes NotificationCenter.default.addObserver(self, selector: #selector(handleCacheRetentionTimeChanged), name: NSNotification.Name("cacheRetentionTimeChanged"), object: nil) // Voeg hier observers toe voor andere settings die directe actie in ScreenshotApp vereisen } @objc func handleCloseAfterDragChanged() { print("Setting changed: closeAfterDrag is now \(SettingsManager.shared.closeAfterDrag)") // Geen directe actie nodig hier, de waarde wordt gelezen wanneer nodig. // Als dit de UI van de preview zou beΓ―nvloeden, roep hier updatePreviewSize() aan. } @objc func handleCloseAfterSaveChanged() { print("Setting changed: closeAfterSave is now \(SettingsManager.shared.closeAfterSave)") // Geen directe actie nodig hier. } @objc func handlePlaySoundOnCaptureChanged() { print("Setting changed: playSoundOnCapture is now \(SettingsManager.shared.playSoundOnCapture)") // Geen directe actie nodig hier, capture() leest de waarde. } // NIEUW: Handler voor wijziging opstartinstelling @objc func handleStartAppOnLoginChanged() { print("Setting changed: startAppOnLogin is now \(SettingsManager.shared.startAppOnLogin)") toggleLaunchAtLogin(shouldLaunch: SettingsManager.shared.startAppOnLogin) } // NIEUW: Handler voor wijziging stash always on top instelling @objc func handleStashAlwaysOnTopChanged() { print("Setting changed: stashAlwaysOnTop is now \(SettingsManager.shared.stashAlwaysOnTop)") updateStashWindowLevel() } // πŸ”₯ NIEUW: Handler voor wijziging persistent stash instelling @objc func handlePersistentStashChanged() { print("πŸ”₯ PERSISTENT STASH: Setting changed - persistentStash is now \(SettingsManager.shared.persistentStash)") if !SettingsManager.shared.persistentStash { print("πŸ”₯ PERSISTENT STASH: Feature disabled - clearing persistent image store") persistentStashImageStore = nil } } // NIEUW: Handler voor wijziging shortcut instelling @objc func handleShortcutSettingChanged() { print("Setting changed: shortcut configuration updated") setupHotKey() } @objc func handleHideDesktopIconsChanged() { print("Setting changed: hideDesktopIconsDuringScreenshot is now \(SettingsManager.shared.hideDesktopIconsDuringScreenshot)") // Update the menu item to reflect the new state menuManager?.refreshDesktopIconsMenuItem() } // πŸ§ͺ NIEUW: Handler for cache retention time changes @objc func handleCacheRetentionTimeChanged() { let retentionTime = SettingsManager.shared.cacheRetentionTime print("πŸ§ͺ CACHE: Setting changed - cacheRetentionTime is now \(retentionTime.displayName)") // Trigger immediate cleanup to test new retention time print("πŸ§ͺ CACHE: Triggering immediate cleanup for testing...") DispatchQueue.global(qos: .utility).async { CacheManager.shared.cleanupOldCache() } // Reschedule periodic cleanup with new interval reschedulePeriodicCacheCleanup() } // NIEUW: Setup hotkey based on current settings private func setupHotKey() { // Remove existing hotkeys hotKey = nil let settings = SettingsManager.shared if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { // Use custom shortcut let modifiers = convertToHotKeyModifiers(settings.customShortcutModifiers) if let key = convertToHotKeyKey(settings.customShortcutKey) { hotKey = HotKey(key: key, modifiers: modifiers) hotKey.keyDownHandler = { [weak self] in print("πŸ”₯ Custom hotkey pressed - activating multi-monitor selection mode") self?.activateMultiMonitorSelection() } print("🎯 Custom hotkey set: \(formatShortcutForLog(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey))") } else { print("❌ Invalid custom shortcut key, falling back to default") setupDefaultHotKey() } } else { // Use default Cmd+Shift+4 setupDefaultHotKey() } } private func setupDefaultHotKey() { hotKey = HotKey(key: .four, modifiers: [.command, .shift]) hotKey.keyDownHandler = { [weak self] in print("πŸ”₯ Default hotkey pressed - activating native screencapture mode") self?.activateMultiMonitorSelection() } print("🎯 Default hotkey set: Cmd+Shift+4 (native screencapture)") } // activateWindowCaptureMode function removed - use native spatiebalk instead private func convertToHotKeyModifiers(_ modifiers: UInt) -> NSEvent.ModifierFlags { var result: NSEvent.ModifierFlags = [] if modifiers & (1 << 0) != 0 { result.insert(.command) } if modifiers & (1 << 1) != 0 { result.insert(.shift) } if modifiers & (1 << 2) != 0 { result.insert(.option) } if modifiers & (1 << 3) != 0 { result.insert(.control) } return result } private func convertToHotKeyKey(_ keyCode: UInt16) -> Key? { // Map key codes to HotKey.Key enum values switch keyCode { case 0: return .a case 1: return .s case 2: return .d case 3: return .f case 4: return .h case 5: return .g case 6: return .z case 7: return .x case 8: return .c case 9: return .v case 11: return .b case 12: return .q case 13: return .w case 14: return .e case 15: return .r case 16: return .y case 17: return .t case 18: return .one case 19: return .two case 20: return .three case 21: return .four case 22: return .six case 23: return .five case 25: return .nine case 26: return .seven case 28: return .eight case 29: return .zero case 31: return .o case 32: return .u case 34: return .i case 35: return .p case 37: return .l case 38: return .j case 40: return .k case 45: return .n case 46: return .m case 36: return .return case 48: return .tab case 49: return .space case 51: return .delete case 53: return .escape case 122: return .f1 case 120: return .f2 case 99: return .f3 case 118: return .f4 case 96: return .f5 case 97: return .f6 case 98: return .f7 case 100: return .f8 case 101: return .f9 case 109: return .f10 case 103: return .f11 case 111: return .f12 default: return nil } } private func formatShortcutForLog(modifiers: UInt, keyCode: UInt16) -> String { var parts: [String] = [] if modifiers & (1 << 3) != 0 { parts.append("Ctrl") } if modifiers & (1 << 2) != 0 { parts.append("Opt") } if modifiers & (1 << 1) != 0 { parts.append("Shift") } if modifiers & (1 << 0) != 0 { parts.append("Cmd") } if let key = convertToHotKeyKey(keyCode) { parts.append("\(key)") } else { parts.append("Key\(keyCode)") } return parts.joined(separator: "+") } // NIEUW: Grid action handlers @objc func handleGridCancelAction() { print("🚫 Grid cancel action received via notification") // Legacy: This is now handled by GridActionManager directly gridViewManager?.hideGrid(monitorForReappear: false) } @objc func handleGridRemoveAction() { print("πŸ—‘ Grid remove action received via notification") // Legacy: This is now handled by GridActionManager directly gridViewManager?.hideGrid(monitorForReappear: false) if let temp = self.tempURL { print("πŸ—‘οΈ Removing temp file: \(temp.path)") try? FileManager.default.removeItem(at: temp) self.setTempFileURL(nil) } closePreviewWithAnimation(immediate: true) } // NIEUW: First Launch Wizard private func checkAndShowFirstLaunchWizard() { if !SettingsManager.shared.hasCompletedFirstLaunch { // Show the wizard firstLaunchWizard = FirstLaunchWizard() firstLaunchWizard?.makeKeyAndOrderFront(nil as Any?) // Optionally, bring to front firstLaunchWizard?.orderFrontRegardless() print("πŸ‘‹ First launch wizard shown") } else { print("πŸ‘‹ Welcome back! First launch already completed") } } // Function to capture the current screen based on mouse location func captureCurrentScreen(at clickLocation: NSPoint) { guard let screen = NSScreen.screens.first(where: { $0.frame.contains(clickLocation) }) else { NSLog("Error: Could not determine screen for single click at \(clickLocation)") return } NSLog("🎯 Capturing full screen: \(screen.customLocalizedName) due to single click.") Task { [weak self] in guard let self = self else { return } // Intel Mac compatible ScreenCaptureKit usage if let provider = self.screenCaptureProvider { let windowsToExclude = await provider.getAllWindowsToExclude() if let image = await provider.captureScreen(screen: screen, excludingWindows: windowsToExclude) { await MainActor.run { self.processCapture(image: image) } } else { NSLog("Error: Failed to capture full screen \(screen.customLocalizedName) using ScreenCaptureKitProvider") } } else { // Fallback to native screencapture for Intel Macs without ScreenCaptureKit print("⚑ Intel Mac fallback: Using native screencapture for full screen capture") self.captureCurrentScreenNative(screen: screen) } } } // NIEUW: Intel Mac fallback for native screencapture of specific screen private func captureCurrentScreenNative(screen: NSScreen) { guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { print("❌ Could not get screen number for native capture") return } // Create temporary file for screenshot let tempDirectory = FileManager.default.temporaryDirectory let tempFileName = "ShotScreen_intel_\(UUID().uuidString).png" let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) print("πŸ“„ Intel Mac using temporary file: \(tempFileURL.path)") // Native screencapture for specific display (NO clipboard) let task = Process() task.launchPath = "/usr/sbin/screencapture" task.arguments = ["-D", "\(screenNumber.intValue)", "-x", tempFileURL.path] // specific display to file, no sounds do { try task.run() DispatchQueue.global(qos: .userInitiated).async { [weak self] in task.waitUntilExit() DispatchQueue.main.async { [weak self] in guard let self = self else { return } print("βœ… Intel Mac: Native screencapture completed for \(screen.customLocalizedName)") // Check if file was created and process it if FileManager.default.fileExists(atPath: tempFileURL.path) { self.processFileImage(at: tempFileURL) } else { print("❌ Intel Mac: No screenshot file created") } } } } catch { print("❌ Intel Mac: Failed to start native screencapture: \(error)") } } // Function to capture all screens // πŸ”₯ NIEUW: Window capture at specific location func captureWindowAt(location: NSPoint) { if #available(macOS 12.3, *) { if let windowManager = windowCaptureManager { Task { await windowManager.captureWindowAt(point: location) } } else { print("❌ WindowCaptureManager not available for window capture") } } else { print("⚠️ Window capture requires macOS 12.3 or later") } } // πŸ”„ UPDATE: Setup automatic updates using UpdateManager @MainActor private func setupAutomaticUpdates() { print("πŸ”„ UPDATE: Setting up UpdateManager...") updateManager.delegate = self updateManager.printDebugInfo() } // πŸ—‚οΈ NEW: Perform cache cleanup on startup private func performStartupCacheCleanup() { print("πŸ—‚οΈ CACHE: Performing startup cache cleanup...") // Perform cleanup in background to avoid blocking startup DispatchQueue.global(qos: .utility).async { CacheManager.shared.cleanupOldCache() } // Schedule periodic cleanup with dynamic interval schedulePeriodicCacheCleanup() } // πŸ—‚οΈ NEW: Schedule periodic cache cleanup with dynamic interval based on retention time private func schedulePeriodicCacheCleanup() { let retentionTime = SettingsManager.shared.cacheRetentionTime let cleanupInterval = getCleanupInterval(for: retentionTime) cacheCleanupTimer = Timer.scheduledTimer(withTimeInterval: cleanupInterval, repeats: true) { _ in print("πŸ—‚οΈ CACHE: Performing scheduled cache cleanup...") DispatchQueue.global(qos: .utility).async { CacheManager.shared.cleanupOldCache() } } print("πŸ—‚οΈ CACHE: Scheduled periodic cleanup every \(Int(cleanupInterval)) seconds for retention: \(retentionTime.displayName)") } // πŸ§ͺ NEW: Reschedule periodic cache cleanup with new settings private func reschedulePeriodicCacheCleanup() { // Cancel existing timer cacheCleanupTimer?.invalidate() cacheCleanupTimer = nil // Schedule new timer with updated interval schedulePeriodicCacheCleanup() } // πŸ§ͺ NEW: Get cleanup interval based on retention time private func getCleanupInterval(for retentionTime: CacheRetentionTime) -> TimeInterval { switch retentionTime { case .oneHour: return 300 // Check every 5 minutes for 1 hour retention case .sixHours, .twelveHours: return 1800 // Check every 30 minutes case .oneDay, .threeDays: return 3600 // Check every hour case .oneWeek, .twoWeeks, .oneMonth: return 7200 // Check every 2 hours case .forever: return 86400 // Check once per day (in case setting changes) } } // πŸ”„ UPDATE: Manual update check via UpdateManager @MainActor func checkForUpdates() { print("πŸ” UPDATE: Triggering manual update check...") updateManager.checkForUpdates() } // MARK: - Dynamic About Dialog Helper private func getDynamicAboutDialogTitle() -> String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" return "🎯 About ShotScreen v\(version) (\(build)) ULTIMATE!" } @MainActor func showAbout() { print("ℹ️ ABOUT: Showing about dialog...") let alert = NSAlert() alert.messageText = getDynamicAboutDialogTitle() let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown" let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown" alert.informativeText = """ Version: \(version) (Build \(buildNumber)) ShotScreen is a powerful screenshot utility for macOS. Features: β€’ Screenshot capture with custom regions β€’ Window capture β€’ All screens capture β€’ Stash for organizing screenshots β€’ Automatic updates via Sparkle Β© 2025 Your Company Name. All rights reserved. """ alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Check for Updates") alert.alertStyle = .informational // Set app icon if available if let appIcon = NSApplication.shared.applicationIconImage { alert.icon = appIcon } let response = alert.runModal() if response == .alertSecondButtonReturn { // User clicked "Check for Updates" checkForUpdates() } } func captureAllScreens() { NSLog("🎯 Capturing all screens.") // Reset de toggle na het capturen if isAllScreensCaptureToggledOn { isAllScreensCaptureToggledOn = false updateAllScreensModeNotifier() // Notifier ook updaten print("πŸ”„ All Screens Capture Toggled Off after capture.") } // 🎯 Check of desktop filtering nodig is let needsDesktopFiltering = SettingsManager.shared.hideDesktopIconsDuringScreenshot || SettingsManager.shared.hideDesktopWidgetsDuringScreenshot if needsDesktopFiltering { // Gebruik ScreenCaptureKit voor filtering print("🎯 Using ScreenCaptureKit for all screens capture (desktop filtering enabled)") captureAllScreensWithFiltering() } else { // Gebruik native screencapture voor alle schermen print("🎯 Using native screencapture for all screens capture") captureAllScreensNative() } } // 🎯 BESTAANDE FUNCTIE: ScreenCaptureKit all screens capture private func captureAllScreensWithFiltering() { // Toon loading indicator voor stitching process previewManager.showLoadingIndicator() Task { [weak self] in guard let self = self else { return } // Intel Mac compatible ScreenCaptureKit usage guard let provider = self.screenCaptureProvider else { print("⚑ Intel Mac fallback: ScreenCaptureKit not available, using native all screens capture") await MainActor.run { self.previewManager.hideLoadingIndicator() self.captureAllScreensNative() } return } let windowsToExclude = await provider.getAllWindowsToExclude() let allScreens = NSScreen.screens guard !allScreens.isEmpty else { NSLog("Error: No screens found to capture.") await MainActor.run { self.previewManager.hideLoadingIndicator() } return } var totalBounds = CGRect.null for screen in allScreens { totalBounds = totalBounds.union(screen.frame) } NSLog("πŸ–ΌοΈ Total combined bounds for all screens: \(totalBounds)") var imagePartsToDraw: [(image: NSImage, rect: NSRect, screenName: String)] = [] var allCapturedSuccessfully = true for screen in allScreens { NSLog("πŸ“Έ Attempting to capture screen for combined image: \(screen.customLocalizedName)") if let imagePart = await provider.captureScreen(screen: screen, excludingWindows: windowsToExclude) { let drawRectX = screen.frame.origin.x - totalBounds.origin.x let drawRectY = screen.frame.origin.y - totalBounds.origin.y let targetRect = NSRect(x: drawRectX, y: drawRectY, width: screen.frame.width, height: screen.frame.height) imagePartsToDraw.append((image: imagePart, rect: targetRect, screenName: screen.customLocalizedName)) } else { NSLog("Error: Failed to capture screen \(screen.customLocalizedName) for combined image.") allCapturedSuccessfully = false // Optioneel: break om te stoppen bij de eerste fout } } if allCapturedSuccessfully && !imagePartsToDraw.isEmpty { await MainActor.run { let combinedImage = NSImage(size: totalBounds.size) combinedImage.lockFocus() for item in imagePartsToDraw { item.image.draw(in: item.rect) NSLog("πŸ–Ό Drawn screen \(item.screenName) at \(item.rect) on combined image") } combinedImage.unlockFocus() // Verberg loading indicator voordat we de preview tonen self.previewManager.hideLoadingIndicator() self.processCapture(image: combinedImage) } } else if !allCapturedSuccessfully { await MainActor.run { NSLog("Error: Not all screens were captured successfully for the combined image.") self.previewManager.hideLoadingIndicator() } } else { await MainActor.run { NSLog("Error: No images were captured to create a combined image.") self.previewManager.hideLoadingIndicator() } } } } // πŸ”§ OUDE WERKENDE FUNCTIE: Screen-by-screen capture en combinatie (restored from working version) private func captureAllScreensNative() { print("πŸ“Έ NATIVE all screens capture starting (using ScreenCaptureKit method)...") print("πŸ”§ BYPASSING desktop filtering for multi-click all screens") // πŸ”§ CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY deactivateMultiMonitorSelection() // Toon loading indicator voor stitching process previewManager.showLoadingIndicator() Task { [weak self] in guard let self = self else { return } let windowsToExclude = await self.screenCaptureProvider?.getAllWindowsToExclude() ?? [] let allScreens = NSScreen.screens guard !allScreens.isEmpty else { print("❌ Error: No screens found to capture.") await MainActor.run { self.previewManager.hideLoadingIndicator() } return } var totalBounds = CGRect.null for screen in allScreens { totalBounds = totalBounds.union(screen.frame) } print("πŸ–ΌοΈ Total combined bounds for all screens: \(totalBounds)") var imagePartsToDraw: [(image: NSImage, rect: NSRect, screenName: String)] = [] var allCapturedSuccessfully = true for screen in allScreens { print("πŸ“Έ Attempting to capture screen for combined image: \(screen.customLocalizedName)") if let imagePart = await self.screenCaptureProvider?.captureScreen(screen: screen, excludingWindows: windowsToExclude) { let drawRectX = screen.frame.origin.x - totalBounds.origin.x let drawRectY = screen.frame.origin.y - totalBounds.origin.y let targetRect = NSRect(x: drawRectX, y: drawRectY, width: screen.frame.width, height: screen.frame.height) imagePartsToDraw.append((image: imagePart, rect: targetRect, screenName: screen.customLocalizedName)) } else { print("❌ Error: Failed to capture screen \(screen.customLocalizedName) for combined image.") allCapturedSuccessfully = false // Optioneel: break om te stoppen bij de eerste fout } } if allCapturedSuccessfully && !imagePartsToDraw.isEmpty { await MainActor.run { let combinedImage = NSImage(size: totalBounds.size) combinedImage.lockFocus() for item in imagePartsToDraw { item.image.draw(in: item.rect) print("πŸ–Ό Drawn screen \(item.screenName) at \(item.rect) on combined image") } combinedImage.unlockFocus() // Verberg loading indicator voordat we de preview tonen self.previewManager.hideLoadingIndicator() self.processCapture(image: combinedImage) } } else if !allCapturedSuccessfully { await MainActor.run { print("❌ Error: Not all screens were captured successfully for the combined image.") self.previewManager.hideLoadingIndicator() } } else { await MainActor.run { print("❌ Error: No images were captured to create a combined image.") self.previewManager.hideLoadingIndicator() } } } } // NIEUW: Methode om de notifier voor "alle schermen" modus bij te werken (placeholder) func updateAllScreensModeNotifier() { let isActive = isAllScreensCaptureToggledOn print("πŸ”” Notifier update: All Screens Mode is \(isActive ? "Active" : "Inactive")") for window in eventCaptureWindows { // Update CrosshairCursorView (voor de blauwe cirkel) if let contentView = window.contentView { for subview in contentView.subviews { if let crosshairView = subview as? CrosshairCursorView { crosshairView.currentMode = isActive ? .allScreensActive : .normal // crosshairView.needsDisplay = true // needsDisplay wordt al in didSet van currentMode afgehandeld } // Update EventCaptureView (voor de tekst-overlay) if let eventCaptureView = subview as? EventCaptureView { eventCaptureView.shouldDisplayAllScreensActiveText = isActive eventCaptureView.needsDisplay = true } } } } } // NIEUW: Reset de toggle expliciet, bijvoorbeeld bij het annuleren van een selectie func resetAllScreensCaptureToggle() { if isAllScreensCaptureToggledOn { isAllScreensCaptureToggledOn = false updateAllScreensModeNotifier() print("πŸ”„ All Screens Capture Toggled Off explicitly (e.g., selection cancelled).") } } // NIEUW: Selector method for wizard close notification @objc private func firstLaunchWizardDidClose(_ notification: Notification) { print("πŸŽ‰ First launch wizard closed") if let wizard = notification.object as? FirstLaunchWizard { // Remove the observer for this specific wizard NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: wizard) } firstLaunchWizard = nil } // NIEUW: Reset first launch wizard (for testing) @objc func resetFirstLaunchWizardInternal() { print("πŸ”„ Resetting first launch wizard for testing") SettingsManager.shared.hasCompletedFirstLaunch = false // FIXED: Better memory management - check if wizard still exists if let existingWizard = firstLaunchWizard { print("πŸ—‘οΈ Closing existing wizard before showing new one") // Remove observer first to prevent issues NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: existingWizard) // Close the wizard safely if existingWizard.isVisible { existingWizard.close() } // Clear the reference immediately firstLaunchWizard = nil } else { print("ℹ️ No existing wizard to close") } // Add small delay to ensure cleanup is complete before creating new wizard DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in guard let self = self else { return } print("πŸŽ‰ Showing wizard after reset delay") self.checkAndShowFirstLaunchWizard() } } // NIEUW: Show Stash via menu @objc func showStash(_ sender: Any?) { print("πŸ—‚οΈ Show Stash menu item selected") // Controleer of er al een stash window open is if let existingController = stashWindowController, let existingWindow = existingController.window, existingWindow.isVisible { print("πŸ—‚οΈ Stash window already open. Bringing to front.") existingWindow.orderFrontRegardless() existingWindow.makeKeyAndOrderFront(nil as Any?) NSApp.activate(ignoringOtherApps: true) return } // πŸ”₯ NIEUW: Load persistent stash images if available print("πŸ—‚οΈ Creating new Stash window") let newImageStore = GalleryImageStore() // Check for persistent stash images if SettingsManager.shared.persistentStash, let persistentStore = persistentStashImageStore, persistentStore.images.count > 0 { print("πŸ”₯ PERSISTENT STASH: Loading \(persistentStore.images.count) saved images") newImageStore.images = persistentStore.images } else { print("πŸ”₯ PERSISTENT STASH: No saved images or persistent stash disabled - starting empty") } let initialStashWidth: CGFloat = 200 let initialStashHeight: CGFloat = 300 let newWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: initialStashWidth, height: initialStashHeight), styleMask: [.borderless, .closable, .fullSizeContentView], backing: .buffered, defer: false) let closeWindowAction = { [weak newWindow] in guard let windowToClose = newWindow else { return } windowToClose.close() } let galleryView = IntegratedGalleryView(imageStore: newImageStore, initialImage: nil, hostingWindow: newWindow, closeAction: closeWindowAction) let hostingView = NSHostingView(rootView: galleryView) newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.hasShadow = true newWindow.titleVisibility = .hidden newWindow.titlebarAppearsTransparent = true newWindow.isMovable = false let rootContentView = NSView(frame: newWindow.contentRect(forFrameRect: newWindow.frame)) rootContentView.wantsLayer = true rootContentView.layer?.cornerRadius = 12 rootContentView.layer?.masksToBounds = true let visualEffectView = NSVisualEffectView() visualEffectView.blendingMode = .behindWindow visualEffectView.material = .hudWindow visualEffectView.state = .active visualEffectView.autoresizingMask = [.width, .height] visualEffectView.frame = rootContentView.bounds visualEffectView.wantsLayer = true visualEffectView.layer?.cornerRadius = 12 visualEffectView.layer?.masksToBounds = true let extraBlurView = NSVisualEffectView() extraBlurView.blendingMode = .behindWindow extraBlurView.material = .hudWindow extraBlurView.state = .active extraBlurView.alphaValue = 0.6 extraBlurView.autoresizingMask = [.width, .height] extraBlurView.frame = rootContentView.bounds rootContentView.addSubview(visualEffectView) rootContentView.addSubview(extraBlurView) hostingView.translatesAutoresizingMaskIntoConstraints = false rootContentView.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.topAnchor.constraint(equalTo: rootContentView.topAnchor), hostingView.bottomAnchor.constraint(equalTo: rootContentView.bottomAnchor), hostingView.leadingAnchor.constraint(equalTo: rootContentView.leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: rootContentView.trailingAnchor) ]) newWindow.contentView = rootContentView // FIXED: Position stash window on the same screen as the main thumbnail let targetScreen = self.getTargetScreenForStashPreview() ?? NSScreen.main ?? NSScreen.screens.first if let screenToDisplayOn = targetScreen { let targetY = (screenToDisplayOn.visibleFrame.height - initialStashHeight) / 2 + screenToDisplayOn.visibleFrame.origin.y let spacing: CGFloat = 20 let targetX = screenToDisplayOn.visibleFrame.maxX - initialStashWidth - spacing newWindow.setFrameOrigin(NSPoint(x: targetX, y: targetY)) print("πŸͺŸ Stash window positioned on screen: \(screenToDisplayOn.customLocalizedName) at {\(targetX), \(targetY)}") } else { newWindow.center() // Fallback print("πŸͺŸ Stash window centered (could not determine target screen).") } newWindow.delegate = self let newWindowController = NSWindowController(window: newWindow) newWindowController.showWindow(self) self.stashWindowController = newWindowController self.activeStashImageStore = newImageStore // Set window level based on always on top setting if SettingsManager.shared.stashAlwaysOnTop { newWindow.level = .floating print("πŸ” New empty stash window created with always on top") } else { newWindow.level = .normal print("πŸ“‹ New empty stash window created with normal level") } newWindow.makeKeyAndOrderFront(nil as Any?) NSApp.activate(ignoringOtherApps: true) // πŸ”₯ NIEUW: Shake animatie ook voor Menu "Show Stash" windows! print("πŸŽ‰ Adding MENU stash shake animation!") addSubtleShakeAnimation(to: newWindow) print("πŸ—‚οΈ Empty Stash window created and shown") } // NIEUW: Update stash window level based on setting func updateStashWindowLevel() { guard let stashWindow = stashWindowController?.window else { return } if SettingsManager.shared.stashAlwaysOnTop { stashWindow.level = .floating print("πŸ” Stash window set to always on top") } else { stashWindow.level = .normal print("πŸ“‹ Stash window set to normal level") } } func applicationWillTerminate(_ notification: Notification) { print("πŸ” DEBUG: applicationWillTerminate called") previewDismissTimer?.invalidate() previewDismissTimer = nil // NIEUW: Cleanup hotkey reset timer hotkeyResetTimer?.invalidate() hotkeyResetTimer = nil // πŸ”§ FIX: Cleanup double-click action timer doubleClickActionTimer?.invalidate() doubleClickActionTimer = nil // πŸ§ͺ NIEUW: Cleanup cache cleanup timer cacheCleanupTimer?.invalidate() cacheCleanupTimer = nil // NIEUW: Cleanup FirstLaunchWizard properly if let wizard = firstLaunchWizard { NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: wizard) wizard.close() firstLaunchWizard = nil } NotificationCenter.default.removeObserver(self) // Verwijder alle observers // MARK: - Multi-Monitor Cleanup cleanupMultiMonitorResources() // NIEUW: Cleanup MenuManager menuManager?.cleanup() // NIEUW: Cleanup PreviewManager previewManager?.cleanup() // NIEUW: Cleanup FinderWindowManager finderWindowManager?.forceCleanup() } // MARK: - Menu functions moved to MenuManager.swift func activateMultiMonitorSelection() { // Sluit een eventueel openstaand rename panel voordat een nieuwe selectie start renameActionHandler.closeRenamePanelAndCleanup() // NIEUW: Reset de "alle schermen" toggle bij het starten van een nieuwe selectie resetAllScreensCaptureToggle() // NIEUW: Reset isDragging state voor een schone start self.isDragging = false // NIEUW: Sluit ook een eventueel bestaande activePreviewWindow direct if let existingPreview = self.activePreviewWindow, existingPreview.isVisible { print("πŸ”΅ Closing existing activePreviewWindow before starting new selection.") self.closePreviewWithAnimation(immediate: true, preserveTempFile: false) } // NIEUW: Ruim een eventueel bestaande actieve overlay op if let existingOverlay = activeOverlayWindow, existingOverlay.isVisible { print("πŸ”΅ Closing existing active overlay window before starting new selection.") existingOverlay.orderOut(nil as Any?) activeOverlayWindow = nil } // 🎯 NIEUWE OPLOSSING: Gebruik native macOS screencapture in plaats van complexe crosshair triggerNativeScreencapture() } // 🎯 VASTE OPLOSSING: Multi-hotkey tracking met correcte timing private var hotkeyPressCount: Int = 0 private var firstHotkeyPressTime: Date = Date.distantPast private var hotkeyResetTimer: Timer? private let multiHotkeyTimeWindow: TimeInterval = 1.5 // 1.5 seconde venster voor betrouwbare multi-click detectie // πŸ”§ FIX: Remember mouse location from first hotkey press for accurate multi-screen detection private var rememberedMouseLocation: NSPoint = NSPoint.zero private func triggerNativeScreencapture() { let now = Date() let timeSinceFirstPress = now.timeIntervalSince(firstHotkeyPressTime) // Reset counter als te veel tijd verstreken is if timeSinceFirstPress > multiHotkeyTimeWindow { hotkeyPressCount = 0 firstHotkeyPressTime = now } // Verhoog counter hotkeyPressCount += 1 // Als dit de eerste press is, stel eerste tijd in en onthoud mouse location if hotkeyPressCount == 1 { firstHotkeyPressTime = now // πŸ”§ FIX: Remember mouse location from first press for accurate multi-screen detection rememberedMouseLocation = NSEvent.mouseLocation print("πŸ–±οΈ Remembered mouse location for multi-click: \(rememberedMouseLocation)") } print("🎯 Hotkey press count: \(hotkeyPressCount), time since first: \(timeSinceFirstPress)") // πŸ”§ FIX: Multi-click detection EERST - boven desktop filtering check // Cancel any existing reset timer hotkeyResetTimer?.invalidate() if hotkeyPressCount == 3 { print("🎯🎯🎯 TRIPLE hotkey press detected - capturing ALL SCREENS!") print("πŸ”§ Triple hotkey bypasses desktop filtering settings - using NATIVE mode") // πŸ”§ CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY deactivateMultiMonitorSelection() // πŸ”§ FIX: KILL active screencapture immediately to remove crosshair killActiveScreencapture() // πŸ”§ FIX: Cancel any pending double-click action doubleClickActionTimer?.invalidate() doubleClickActionTimer = nil print("πŸ”§ Cancelled pending double-click action due to triple-click") // πŸ”§ FIX: Delay reset to allow for any rapid subsequent clicks scheduleDelayedReset() captureAllScreensNative() return } else if hotkeyPressCount == 2 { print("🎯🎯 Double hotkey press detected - capturing whole screen under cursor!") print("πŸ”§ Double hotkey bypasses desktop filtering settings - using NATIVE mode") print("πŸ”§ Waiting for potential 3rd click before executing...") // πŸ”§ CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY deactivateMultiMonitorSelection() // πŸ”§ FIX: KILL active screencapture immediately to remove crosshair killActiveScreencapture() // πŸ”§ FIX: Schedule 2x action with delay to allow 3rd click to interrupt scheduleDelayedDoubleClickAction() return } // πŸ”§ First press only: Check desktop filtering for single press behavior // Start timer to reset if no second press comes startHotkeyResetTimer() // 🎯 HYBRIDE SYSTEEM: Check of desktop filtering nodig is (alleen voor eerste press) let needsDesktopFiltering = SettingsManager.shared.hideDesktopIconsDuringScreenshot || SettingsManager.shared.hideDesktopWidgetsDuringScreenshot if needsDesktopFiltering { // Gebruik ScreenCaptureKit interface voor filtering print("🎯 Desktop filtering enabled - using ScreenCaptureKit selection mode") print("πŸ‘† Press Cmd+Shift+4 again within 1.2 seconds for whole screen capture") print("πŸ‘†πŸ‘† Press Cmd+Shift+4 THREE times for all screens capture!") print("🎯 Use drag selection for area capture (with desktop filtering)") continueActivateMultiMonitorSelection() } else { // Gebruik native screencapture voor normale macOS ervaring print("🎯 Normal mode - using native macOS screencapture with crosshair cursor") print("πŸ‘† Press Cmd+Shift+4 again within 1.2 seconds for whole screen capture") print("πŸ‘†πŸ‘† Press Cmd+Shift+4 THREE times for all screens capture!") print("🎯 Use normal drag selection with native crosshair") startNativeScreencapture() } } // 🎯 NIEUWE FUNCTIE: Start native macOS screencapture private func startNativeScreencapture() { // Create temporary file for screenshot let tempDirectory = FileManager.default.temporaryDirectory let tempFileName = "ShotScreen_temp_\(UUID().uuidString).png" let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) print("πŸ“„ Using temporary file: \(tempFileURL.path)") // Start screencapture to file (NO clipboard) let task = Process() task.launchPath = "/usr/sbin/screencapture" task.arguments = ["-i", "-x", tempFileURL.path] // interactive, no sounds, save to file do { try task.run() // Wait for screencapture completion in background DispatchQueue.global(qos: .userInitiated).async { [weak self] in task.waitUntilExit() DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Check if file was created (indicates successful capture) if FileManager.default.fileExists(atPath: tempFileURL.path) { print("βœ… Native screenshot saved to file - processing...") self.processFileImage(at: tempFileURL) } else { print("🚫 No screenshot file - user cancelled or no selection made") } } } } catch { print("❌ Failed to start screencapture: \(error)") } } // 🎯 NIEUWE FUNCTIE: Process file image (no clipboard pollution) private func processFileImage(at fileURL: URL) { if let image = NSImage(contentsOf: fileURL) { print("βœ… Successfully loaded image from file") processCapture(image: image) // Clean up temporary file try? FileManager.default.removeItem(at: fileURL) print("πŸ—‘οΈ Temporary screenshot file cleaned up") } else { print("⚠️ Failed to load image from file") // Clean up failed file attempt try? FileManager.default.removeItem(at: fileURL) } } // 🎯 LEGACY FUNCTIE: Process clipboard image (for fallback compatibility) private func processClipboardImage() { let pasteboard = NSPasteboard.general if let image = NSImage(pasteboard: pasteboard) { print("βœ… Successfully captured image from clipboard") processCapture(image: image) } else { print("⚠️ No image found in clipboard - user may have cancelled") } } // NIEUWE FUNCTIE: Capture whole screen onder cursor private func captureWholeScreenUnderCursor() { let mouseLocation = NSEvent.mouseLocation print("πŸ–±οΈ Mouse location: \(mouseLocation)") // πŸ”§ CRITICAL FIX: Kill active screencapture immediately to remove crosshair killActiveScreencapture() if let targetScreen = findScreenContaining(point: mouseLocation) { print("πŸ–₯️ Found target screen: \(targetScreen.customLocalizedName)") print("πŸ“Έ Capturing whole screen...") // 🎯 Check of desktop filtering nodig is let needsDesktopFiltering = SettingsManager.shared.hideDesktopIconsDuringScreenshot || SettingsManager.shared.hideDesktopWidgetsDuringScreenshot if needsDesktopFiltering { // Gebruik ScreenCaptureKit voor filtering print("🎯 Using ScreenCaptureKit for whole screen capture (desktop filtering enabled)") Task { [weak self] in guard let self = self else { return } // Intel Mac compatible ScreenCaptureKit usage if let provider = self.screenCaptureProvider { let windowsToExclude = await provider.getAllWindowsToExclude() if let image = await provider.captureScreen(screen: targetScreen, excludingWindows: windowsToExclude) { await MainActor.run { print("βœ… Whole screen captured successfully with filtering!") self.processCapture(image: image) } } else { print("❌ Failed to capture whole screen with filtering") } } else { // Fallback to native screencapture for Intel Macs without ScreenCaptureKit print("⚑ Intel Mac fallback: Using native screencapture for whole screen") await MainActor.run { self.captureWholeScreenNative(screen: targetScreen) } } } } else { // Gebruik native screencapture voor hele scherm print("🎯 Using native screencapture for whole screen capture") captureWholeScreenNative(screen: targetScreen) } } else { print("❌ No target screen found for location: \(mouseLocation)") } } // πŸ”§ NIEUWE MULTI-CLICK FUNCTIE: Direct native whole screen capture (bypasses desktop filtering) private func captureWholeScreenNativeMultiClick() { print("🎯 DEBUG: captureWholeScreenNativeMultiClick CALLED!") // πŸ”§ CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY deactivateMultiMonitorSelection() // πŸ”§ FIX: Use remembered mouse location from first hotkey press for accurate multi-screen detection let mouseLocation = rememberedMouseLocation print("πŸ–±οΈ Multi-click using REMEMBERED mouse location: \(mouseLocation)") print("πŸ”§ Current mouse location would be: \(NSEvent.mouseLocation)") // πŸ”§ CRITICAL FIX: Kill active screencapture immediately to remove crosshair killActiveScreencapture() if let targetScreen = findScreenContaining(point: mouseLocation) { print("πŸ–₯️ Multi-click found target screen: \(targetScreen.customLocalizedName)") print("πŸ“Έ Multi-click capturing whole screen DIRECTLY with native screencapture...") // πŸ”§ DIRECT NATIVE - NO DESKTOP FILTERING CHECK captureWholeScreenNative(screen: targetScreen) } else { print("❌ Multi-click: No target screen found for REMEMBERED location: \(mouseLocation)") print("⚠️ Falling back to main screen for multi-click capture") // Fallback to main screen if remembered location doesn't work if let mainScreen = NSScreen.main { captureWholeScreenNative(screen: mainScreen) } } } // 🎯 NIEUWE FUNCTIE: Native whole screen capture private func captureWholeScreenNative(screen: NSScreen) { let displayID = screen.displayID // πŸ”§ FIX: Get screen index for screencapture (1-based) instead of display ID guard let screenIndex = NSScreen.screens.firstIndex(of: screen) else { print("❌ Could not find screen index for \(screen.customLocalizedName)") return } let screencaptureDisplayNumber = screenIndex + 1 // screencapture is 1-based print("🎯 DEBUG: captureWholeScreenNative called") print(" Screen: \(screen.customLocalizedName)") print(" Frame: \(screen.frame)") print(" Display ID: \(displayID)") print(" Screen Index: \(screenIndex)") print(" Screencapture Display Number: \(screencaptureDisplayNumber)") // Create temporary file for screenshot let tempDirectory = FileManager.default.temporaryDirectory let tempFileName = "ShotScreen_wholescreen_\(UUID().uuidString).png" let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) print("πŸ“„ Using temporary file: \(tempFileURL.path)") // Start screencapture for specific display (NO clipboard) let task = Process() task.launchPath = "/usr/sbin/screencapture" task.arguments = ["-D", "\(screencaptureDisplayNumber)", "-x", tempFileURL.path] // πŸ”§ FIX: No clipboard, save to file print("πŸ”§ DEBUG: Starting screencapture with arguments: \(task.arguments ?? [])") do { try task.run() // Wait for completion DispatchQueue.global(qos: .userInitiated).async { [weak self] in task.waitUntilExit() DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Check if file was created (indicates successful capture) if FileManager.default.fileExists(atPath: tempFileURL.path) { print("βœ… Native whole screen captured successfully!") self.processFileImage(at: tempFileURL) } else { print("❌ No whole screen screenshot file created") } } } } catch { print("❌ Failed to start whole screen screencapture: \(error)") } } // NIEUWE FUNCTIE: Vind scherm waar punt zich bevindt private func findScreenContaining(point: NSPoint) -> NSScreen? { print("πŸ” DEBUG: Looking for screen containing point: \(point)") print("πŸ” DEBUG: Available screens:") for (index, screen) in NSScreen.screens.enumerated() { print(" Screen \(index): \(screen.customLocalizedName) - Frame: \(screen.frame)") if screen.frame.contains(point) { print(" βœ… MATCH! Screen \(index) contains the point") return screen } else { print(" ❌ Screen \(index) does NOT contain the point") } } print("⚠️ DEBUG: No screen found containing point, falling back to main screen") return NSScreen.main // fallback naar main screen } // πŸ”§ CRITICAL FUNCTION: Kill active screencapture to remove crosshair cursor private func killActiveScreencapture() { print("⚑ Killing active screencapture to remove crosshair cursor...") let killTask = Process() killTask.launchPath = "/usr/bin/killall" killTask.arguments = ["screencapture"] do { try killTask.run() killTask.waitUntilExit() print("⚑ Active screencapture killed, crosshair cursor should be restored to normal") } catch { print("⚠️ Failed to kill screencapture: \(error)") } } // NIEUWE FUNCTIES: Hotkey counter management private func resetHotkeyCounter() { hotkeyPressCount = 0 firstHotkeyPressTime = Date.distantPast hotkeyResetTimer?.invalidate() hotkeyResetTimer = nil // πŸ”§ FIX: Also cleanup double-click action timer doubleClickActionTimer?.invalidate() doubleClickActionTimer = nil // πŸ”§ FIX: Reset remembered mouse location rememberedMouseLocation = NSPoint.zero print("πŸ”„ Hotkey counter reset") } private func startHotkeyResetTimer() { hotkeyResetTimer?.invalidate() hotkeyResetTimer = Timer.scheduledTimer(withTimeInterval: multiHotkeyTimeWindow, repeats: false) { [weak self] _ in self?.resetHotkeyCounter() } } // πŸ”§ NEW: Schedule delayed reset to allow rapid multi-clicks private func scheduleDelayedReset() { hotkeyResetTimer?.invalidate() hotkeyResetTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in self?.resetHotkeyCounter() print("πŸ”„ Delayed hotkey counter reset after multi-click action") } } // πŸ”§ NEW: Timer for delayed double-click action private var doubleClickActionTimer: Timer? // πŸ”§ NEW: Schedule delayed double-click action to allow 3rd click to interrupt private func scheduleDelayedDoubleClickAction() { print("⏲️ DEBUG: scheduleDelayedDoubleClickAction CALLED!") doubleClickActionTimer?.invalidate() doubleClickActionTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in guard let self = self else { return } print("🎯🎯 DEBUG: Executing delayed double-click action (no 3rd click detected)") self.captureWholeScreenNativeMultiClick() self.scheduleDelayedReset() } } private func continueActivateMultiMonitorSelection() { // VERWIJDERD: Herstel Finder vensters - niet meer nodig zonder Finder restart // if SettingsManager.shared.cleanDesktopScreenshots { // finderWindowManager.restoreFinderWindows() // } // Hide any existing multi-monitor overlay windows hideMultiMonitorOverlayWindows() // Create event capture window to intercept all mouse events createEventCaptureWindow() // Activate multi-monitor selection mode isMultiMonitorSelectionActive = true // Hide system cursor and setup custom crosshair overlay hideCursor() setupCustomCrosshairOverlay() // Add global key monitor for ESC key setupGlobalKeyMonitor() print("πŸš€ Multi-monitor selection mode activated!") print("🎯 Click and drag across multiple monitors for seamless selection") print("πŸ”§ Press ESC or right-click to cancel selection") } func beginSelection() { // Legacy method - redirect to new multi-monitor system activateMultiMonitorSelection() } func capture(rect: NSRect, isMultiMonitor: Bool = false) { print("πŸ“Έ capture gestart") print("πŸ“ Capture gebied: x=\(rect.origin.x), y=\(rect.origin.y), width=\(rect.width), height=\(rect.height)") // πŸ”§ CRITICAL FIX: Kill any active crosshair cursor immediately when capture starts killActiveScreencapture() // πŸ”§ CRITICAL FIX: Deactivate multi-monitor selection mode to remove custom crosshair overlay deactivateMultiMonitorSelection() DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in guard let self = self else { return } if isMultiMonitor { // Use specialized multi-monitor capture self.captureMultiMonitorRegion(rect: rect) } else { // Use traditional single-screen capture self.captureSingleScreen(rect: rect) } } } private func captureSingleScreen(rect: NSRect) { guard let screen = NSScreen.main else { return } NSLog("🎯 Single-screen capture (main screen selection): \(rect)") Task { [weak self] in guard let self = self else { return } // Intel Mac compatible ScreenCaptureKit usage if let provider = self.screenCaptureProvider { let windowsToExclude = await provider.getAllWindowsToExclude() let selectionCGRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width, height: rect.height) if let image = await provider.captureSelection(selectionRectInPoints: selectionCGRect, screen: screen, excludingWindows: windowsToExclude) { await MainActor.run { self.processCapture(image: image) } } else { NSLog("Error: Failed to capture main screen selection using ScreenCaptureKitProvider") } } else { // Fallback to native screencapture for Intel Macs without ScreenCaptureKit print("⚑ Intel Mac fallback: Single screen selection capture not supported - using full screen") await MainActor.run { self.captureCurrentScreenNative(screen: screen) } } } } // Helper function to flip image vertically (from display coordinates to AppKit coordinates) private func flipImageVertically(_ image: NSImage) -> NSImage { let flippedImage = NSImage(size: image.size) flippedImage.lockFocus() // Create a transform that flips vertically let transform = NSAffineTransform() transform.translateX(by: 0, yBy: image.size.height) transform.scaleX(by: 1.0, yBy: -1.0) transform.concat() // Draw the original image with the transform applied image.draw(at: .zero, from: .zero, operation: .sourceOver, fraction: 1.0) flippedImage.unlockFocus() return flippedImage } private func captureMultiMonitorRegion(rect: NSRect) { print("πŸ”„ Multi-monitor capture: analyzing screen intersections") // Find all screens that intersect with the capture region let intersectingScreens = NSScreen.screens.compactMap { screen -> (screen: NSScreen, intersection: NSRect)? in let intersection = rect.intersection(screen.frame) if intersection.width > 1 && intersection.height > 1 { return (screen: screen, intersection: intersection) } return nil }.sorted { first, second in // Sort by Y coordinate (higher Y = higher on screen = should be at top of final image) return first.intersection.midY > second.intersection.midY } print("πŸ“Ί Found \(intersectingScreens.count) intersecting screens:") for (index, screenInfo) in intersectingScreens.enumerated() { print(" \(index): \(screenInfo.screen.customLocalizedName) - intersection: \(screenInfo.intersection)") // GEWIJZIGD } if intersectingScreens.isEmpty { print("❌ No screens intersect with capture region") return } if intersectingScreens.count == 1 { // Single screen, capture directly let screenInfo = intersectingScreens[0] self.captureSingleScreenRegion(screen: screenInfo.screen, region: screenInfo.intersection) } else { // Multiple screens, capture each and combine self.captureAndCombineScreens(intersectingScreens: intersectingScreens, totalRect: rect) } } private func captureSingleScreenRegion(screen: NSScreen, region: NSRect) { NSLog("πŸ“Έ Capturing single screen region on \(screen.customLocalizedName), region: \(region)") Task { [weak self] in guard let self = self else { return } // Intel Mac compatible ScreenCaptureKit usage if let provider = self.screenCaptureProvider { let windowsToExclude = await provider.getAllWindowsToExclude() let selectionCGRect = CGRect(x: region.origin.x, y: region.origin.y, width: region.width, height: region.height) if let image = await provider.captureSelection(selectionRectInPoints: selectionCGRect, screen: screen, excludingWindows: windowsToExclude) { await MainActor.run { self.processCapture(image: image) } } else { NSLog("Error: Failed to capture screen region on \(screen.customLocalizedName) using ScreenCaptureKitProvider") } } else { // Fallback to native screencapture for Intel Macs without ScreenCaptureKit print("⚑ Intel Mac fallback: Single screen region capture not supported - using full screen") await MainActor.run { self.captureCurrentScreenNative(screen: screen) } } } } private func captureAndCombineScreens(intersectingScreens: [(screen: NSScreen, intersection: NSRect)], totalRect: NSRect) { NSLog("πŸ”„ Capturing and combining \(intersectingScreens.count) screens for totalRect: \(totalRect)") let canvasSize = NSSize(width: totalRect.width, height: totalRect.height) // Show loading indicator for multi-screen stitching previewManager.showLoadingIndicator() Task { [weak self] in guard let self = self else { return } let combinedImage = await self.createCombinedImage(canvasSize: canvasSize) await self.captureAndStitchScreenParts(intersectingScreens: intersectingScreens, totalRect: totalRect, combinedImage: combinedImage) } } // MARK: - Private Helper Methods for Multi-Screen Capture private func createCombinedImage(canvasSize: NSSize) async -> NSImage { return await MainActor.run { NSImage(size: canvasSize) } } private func captureAndStitchScreenParts(intersectingScreens: [(screen: NSScreen, intersection: NSRect)], totalRect: NSRect, combinedImage: NSImage) async { var allPartsCaptured = true for screenInfo in intersectingScreens { let captureSuccessful = await captureScreenPart(screenInfo: screenInfo, totalRect: totalRect, combinedImage: combinedImage) if !captureSuccessful { allPartsCaptured = false break } } await handleCombinedCaptureResult(allPartsCaptured: allPartsCaptured, intersectingScreens: intersectingScreens, combinedImage: combinedImage) } private func captureScreenPart(screenInfo: (screen: NSScreen, intersection: NSRect), totalRect: NSRect, combinedImage: NSImage) async -> Bool { let screen = screenInfo.screen let intersection = screenInfo.intersection NSLog("πŸ“Έ Capturing part from screen: \(screen.customLocalizedName), intersection: \(intersection)") let selectionCGRect = CGRect(x: intersection.origin.x, y: intersection.origin.y, width: intersection.width, height: intersection.height) // Intel Mac compatible ScreenCaptureKit usage guard let provider = self.screenCaptureProvider else { print("⚑ Intel Mac fallback: Multi-screen capture not supported without ScreenCaptureKit") return false } let windowsToExclude = await provider.getAllWindowsToExclude() guard let partImage = await provider.captureSelection( selectionRectInPoints: selectionCGRect, screen: screen, excludingWindows: windowsToExclude ) else { NSLog("Error: Failed to capture part of screen \(screen.customLocalizedName) using ScreenCaptureKitProvider for intersection \(intersection)") return false } await stitchImagePart(partImage: partImage, intersection: intersection, totalRect: totalRect, combinedImage: combinedImage, screenName: screen.customLocalizedName) return true } private func stitchImagePart(partImage: NSImage, intersection: NSRect, totalRect: NSRect, combinedImage: NSImage, screenName: String) async { await MainActor.run { combinedImage.lockFocus() let drawRect = NSRect( x: intersection.origin.x - totalRect.origin.x, y: intersection.origin.y - totalRect.origin.y, width: intersection.width, height: intersection.height ) partImage.draw(in: drawRect) combinedImage.unlockFocus() NSLog("πŸ–Ό Drawn part from \(screenName) at \(drawRect) on combined image") } } private func handleCombinedCaptureResult(allPartsCaptured: Bool, intersectingScreens: [(screen: NSScreen, intersection: NSRect)], combinedImage: NSImage) async { await MainActor.run { self.previewManager.hideLoadingIndicator() if allPartsCaptured && !intersectingScreens.isEmpty { self.processCapture(image: combinedImage) } else if intersectingScreens.isEmpty { NSLog("Error: No intersecting screens found for combined capture.") } else { NSLog("Error: Not all parts captured successfully for combined image.") } } } func processCapture(image: NSImage) { // VERWIJDERD: Desktop icon restore functionality - feature disabled if SettingsManager.shared.playSoundOnCapture { let soundType = SettingsManager.shared.screenshotSoundType let volume = SettingsManager.shared.screenshotSoundVolume if let sound = NSSound(named: soundType.systemSoundName) { sound.volume = volume sound.play() } else { print("⚠️ Could not load sound '\(soundType.systemSoundName)' - falling back to Pop") // Fallback to Pop if selected sound doesn't exist if let fallbackSound = NSSound(named: "Pop") { fallbackSound.volume = volume fallbackSound.play() } } } if let pngData = self.createPngData(from: image) { let tempURL = self.createTempUrl() do { try pngData.write(to: tempURL) print("πŸ“Έ Capture: Setting tempURL to \(tempURL.path)") self.setTempFileURL(tempURL) print("πŸ’Ύ Tijdelijk bestand aangemaakt: \(tempURL.path)") print("πŸ“Š PNG data grootte: \(pngData.count) bytes") // 🎯 NEW: Clear old thumbnail restoration backup before creating new one self.clearBackupAfterNewScreenshot() // 🎯 NEW: Create thumbnail restoration backup self.createImageBackup(image: image, tempURL: tempURL) if SettingsManager.shared.autoSaveScreenshot { print("βš™οΈ Automatisch opslaan is AAN.") self.saveDirectlyToConfiguredFolder() if !SettingsManager.shared.closeAfterSave { self.lastImage = image self.showPreview(image: image) } } else { print("βš™οΈ Automatisch opslaan is UIT. Preview wordt getoond.") self.lastImage = image self.showPreview(image: image) } } catch { print("❌ Fout bij opslaan tijdelijk bestand: \(error)") } } } private func createPngData(from image: NSImage) -> Data? { guard let tiffData = image.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiffData) else { return nil } return bitmap.representation(using: .png, properties: [:]) } internal func createTempUrl() -> URL { let filenameBase = generateFilename() // GEWIJZIGD: Gebruik permanente thumbnail directory in plaats van temp directory return thumbnailDirectory .appendingPathComponent(filenameBase) .appendingPathExtension("png") } internal func generateFilename() -> String { let prefix = SettingsManager.shared.filenamePrefix let preset = SettingsManager.shared.filenameFormatPreset let customFormat = SettingsManager.shared.filenameCustomFormat let now = Date() let dateFormatter = DateFormatter() var filename = prefix switch preset { case .macOSStyle: dateFormatter.dateFormat = "yyyy-MM-dd HH.mm.ss" let dateString = dateFormatter.string(from: now) if prefix == "Schermafbeelding" { filename = "Schermafbeelding \(dateString)" } else { filename += (prefix.isEmpty ? "" : " ") + dateString } case .compactDateTime: dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" filename += (prefix.isEmpty ? "" : "_") + dateFormatter.string(from: now) case .superCompactDateTime: dateFormatter.dateFormat = "yyyyMMdd_HHmmss" filename += (prefix.isEmpty ? "" : "_") + dateFormatter.string(from: now) case .timestamp: filename += (prefix.isEmpty ? "" : "_") + String(Int(now.timeIntervalSince1970)) case .prefixOnly: if filename.isEmpty { filename = "screenshot" } case .custom: var format = customFormat dateFormatter.dateFormat = "yyyy" format = format.replacingOccurrences(of: "{YYYY}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "yy" format = format.replacingOccurrences(of: "{YY}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "MM" format = format.replacingOccurrences(of: "{MM}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "dd" format = format.replacingOccurrences(of: "{DD}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "HH" format = format.replacingOccurrences(of: "{hh}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "mm" format = format.replacingOccurrences(of: "{mm}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "ss" format = format.replacingOccurrences(of: "{ss}", with: dateFormatter.string(from: now)) dateFormatter.dateFormat = "SSS" format = format.replacingOccurrences(of: "{ms}", with: dateFormatter.string(from: now)) if !prefix.isEmpty && !format.isEmpty && !["_", "-", " "].contains(where: { format.hasPrefix($0) }) { filename += "_" } filename += format if filename.isEmpty { filename = "custom_screenshot" } } // FIXED: Ensure we always have a valid filename with fallback if filename.isEmpty { filename = "Schermafbeelding \(dateFormatter.string(from: now))" } return filename.replacingOccurrences(of: ":", with: ".") .replacingOccurrences(of: "/", with: "-") } func showPreview(image: NSImage) { // NIEUW: Delegate to PreviewManager previewManager.showPreview(image: image) // Update activePreviewWindow reference for compatibility activePreviewWindow = previewManager.getActivePreviewWindow() } func updatePreviewSize() { // NIEUW: Delegate to PreviewManager previewManager.updatePreviewSize() // Update activePreviewWindow reference for compatibility activePreviewWindow = previewManager.getActivePreviewWindow() } func closePreviewWithAnimation(immediate: Bool, preserveTempFile: Bool) { // NIEUW: Delegate to PreviewManager previewManager.closePreviewWithAnimation(immediate: immediate, preserveTempFile: preserveTempFile) // Update activePreviewWindow reference for compatibility activePreviewWindow = previewManager.getActivePreviewWindow() } @objc func closePreviewWindow() { // NIEUW: Delegate to PreviewManager previewManager.closePreviewWindow() // Update activePreviewWindow reference for compatibility activePreviewWindow = previewManager.getActivePreviewWindow() } @objc func openSettings(_ sender: Any?) { // Any? om ook vanuit menu te kunnen aanroepen _openSettings(sender) } // Implementatie voor GridViewManagerDelegate func getActiveWindowForGridPositioning() -> NSWindow? { return self.activePreviewWindow // Gebruik het standaard preview venster voor positionering van de grid } // Functie om een nieuw gallery venster te openen, of een bestaande te updaten // MOVED TO GridActionManagerDelegate extension - showOrUpdateStash // MARK: - Private stash implementation helpers // AANGEPAST: Accepteert nu onScreen parameter private func _showOrUpdateStash(with imageURL: URL?, image: NSImage? = nil, imageStore providedStore: GalleryImageStore? = nil, onScreen: NSScreen?) { let callId = UUID().uuidString.prefix(4) print("πŸ—‚οΈ [\(callId)] _showOrUpdateStash CALLED. imageURL: \(imageURL?.lastPathComponent ?? "nil"), image isNil: \(image == nil), providedStore isNil: \(providedStore == nil), onScreen: \(onScreen?.localizedName ?? "nil")") let currentImage = image ?? (imageURL != nil ? NSImage(contentsOf: imageURL!) : nil) print("πŸ—‚οΈ [\(callId)] currentImage isNil: \(currentImage == nil)") if let existingController = stashWindowController, let existingWindow = existingController.window, existingWindow.isVisible, let existingStore = activeStashImageStore { print("πŸ—‚οΈ [\(callId)] Stash window BESTAAT. Store heeft \(existingStore.images.count) items.") if let img = currentImage, let url = imageURL { // url is hier de imageURL parameter print("πŸ—‚οΈ [\(callId)] Afbeelding & URL aanwezig. Gaat naar moveToStashDirectory met url: \(url.lastPathComponent)") let stashURL = moveToStashDirectory(from: url) // url wordt gekopieerd naar stash let suggestedNameForAdd = url.deletingPathExtension().lastPathComponent print("πŸ—‚οΈ [\(callId)] Probeert existingStore.addImage. stashURL: \(stashURL?.lastPathComponent ?? "nil"), suggestedNameForAdd: \(suggestedNameForAdd)") // πŸ”₯ CRITICAL FIX: Skip duplicate check to allow BGR thumbnails to be added multiple times existingStore.addImage(img, fileURL: stashURL, suggestedName: suggestedNameForAdd, skipDuplicateCheck: true) // πŸ”„ REMOVED: Hardcoded preview closing - let DraggableImageView handle this based on closeAfterDrag setting print("πŸ”„ Stash action: Preview closing handled by DraggableImageView with closeAfterDrag setting") } else { print("πŸ—‚οΈ [\(callId)] Geen image of imageURL meegegeven aan bestaande stash, geen addImage.") } existingWindow.makeKeyAndOrderFront(nil as Any?) NSApp.activate(ignoringOtherApps: true) if let targetScreen = onScreen, existingWindow.screen != targetScreen { // ... (positionering code) print("πŸ—‚οΈ [\(callId)] Bestaande Stash window verplaatst naar scherm: \(targetScreen.customLocalizedName)") } return } print("πŸ—‚οΈ [\(callId)] Stash window bestaat NIET. Nieuwe stash wordt aangemaakt.") // ... (rest van de functie voor nieuwe stash, daar zit de init van IntegratedGalleryView) // Clean up orphaned stash files before creating new stash do { let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") let stashDirectory = shotScreenDirectory.appendingPathComponent("StashItems") if FileManager.default.fileExists(atPath: stashDirectory.path) { let stashContents = try FileManager.default.contentsOfDirectory(at: stashDirectory, includingPropertiesForKeys: nil) for fileURL in stashContents { if fileURL.pathExtension == "png" { try? FileManager.default.removeItem(at: fileURL) print("🧹 Cleaned up orphaned stash file: \(fileURL.lastPathComponent)") } } } } catch { print("⚠️ Could not clean up stash directory: \(error)") } let store = providedStore ?? GalleryImageStore() if let img = currentImage, let url = imageURL { // NIEUW: Kopieer bestand van Thumbnail naar StashItems directory let stashURL = moveToStashDirectory(from: url) let suggestedName = url.deletingPathExtension().lastPathComponent // VERWIJDERD: Geen addImage call hier meer - IntegratedGalleryView doet dit print("πŸ”„ DEBUG: Copied thumbnail to stash directory: \(stashURL?.lastPathComponent ?? "nil")") // πŸ”„ REMOVED: Hardcoded preview closing - let DraggableImageView handle this based on closeAfterDrag setting print("πŸ”„ Stash action: Preview closing handled by DraggableImageView with closeAfterDrag setting") // FIXED: Pass image data to IntegratedGalleryView for single addition showOrUpdateStashImplementation(image: img, imageURL: stashURL, suggestedName: suggestedName, imageStore: store, onScreen: onScreen) } else { // No image to add showOrUpdateStashImplementation(image: nil, imageURL: nil, suggestedName: nil, imageStore: store, onScreen: onScreen) } } // NIEUW: Hulpmethode om bestanden van Thumbnail naar StashItems directory te kopiΓ«ren private func moveToStashDirectory(from thumbnailURL: URL) -> URL? { // Verkrijg de stash directory (hetzelfde als in IntegratedGalleryView) let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") let stashDirectory = shotScreenDirectory.appendingPathComponent("StashItems") // Maak de directory aan als die niet bestaat try? FileManager.default.createDirectory(at: stashDirectory, withIntermediateDirectories: true, attributes: nil) // FIXED: Behoud de originele naam en voeg alleen suffix toe bij conflict let originalName = thumbnailURL.deletingPathExtension().lastPathComponent let fileExtension = thumbnailURL.pathExtension let baseStashURL = stashDirectory.appendingPathComponent("\(originalName).\(fileExtension)") // Controleer of er al een bestand bestaat met deze naam var finalStashURL = baseStashURL var counter = 1 while FileManager.default.fileExists(atPath: finalStashURL.path) { finalStashURL = stashDirectory.appendingPathComponent("\(originalName)_\(counter).\(fileExtension)") counter += 1 } do { try FileManager.default.copyItem(at: thumbnailURL, to: finalStashURL) print("βœ… Copied thumbnail from \(thumbnailURL.lastPathComponent) to stash: \(finalStashURL.lastPathComponent)") print("πŸ”„ DEBUG: Copied thumbnail to stash directory: \(finalStashURL.lastPathComponent)") return finalStashURL } catch { print("❌ Failed to copy thumbnail to stash: \(error)") return nil } } // AANGEPAST: Accepteert nu onScreen parameter private func showOrUpdateStashImplementation(image: NSImage?, imageURL: URL?, suggestedName: String?, imageStore: GalleryImageStore, onScreen: NSScreen?) { // πŸ”₯ NIEUW: Load persistent stash images if available and store is empty if SettingsManager.shared.persistentStash, let persistentStore = persistentStashImageStore, persistentStore.images.count > 0, imageStore.images.isEmpty { print("πŸ”₯ PERSISTENT STASH: Loading \(persistentStore.images.count) saved images into new stash") imageStore.images = persistentStore.images } let initialStashWidth: CGFloat = 200 let initialStashHeight: CGFloat = 300 let newWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: initialStashWidth, height: initialStashHeight), styleMask: [.borderless, .closable, .fullSizeContentView], backing: .buffered, defer: false) let closeWindowAction = { [weak newWindow] in guard let windowToClose = newWindow else { return } windowToClose.close() } // FIXED: Pass all data to IntegratedGalleryView for single addition let galleryView = IntegratedGalleryView( imageStore: imageStore, initialImage: image, initialImageURL: imageURL, initialImageName: suggestedName, hostingWindow: newWindow, closeAction: closeWindowAction ) let hostingView = NSHostingView(rootView: galleryView) newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.hasShadow = true newWindow.titleVisibility = .hidden newWindow.titlebarAppearsTransparent = true newWindow.isMovable = false let rootContentView = NSView(frame: newWindow.contentRect(forFrameRect: newWindow.frame)) rootContentView.wantsLayer = true rootContentView.layer?.cornerRadius = 12 rootContentView.layer?.masksToBounds = true let visualEffectView = NSVisualEffectView() visualEffectView.blendingMode = .behindWindow visualEffectView.material = .hudWindow visualEffectView.state = .active visualEffectView.autoresizingMask = [.width, .height] visualEffectView.frame = rootContentView.bounds visualEffectView.wantsLayer = true visualEffectView.layer?.cornerRadius = 12 visualEffectView.layer?.masksToBounds = true let extraBlurView = NSVisualEffectView() extraBlurView.blendingMode = .behindWindow extraBlurView.material = .hudWindow extraBlurView.state = .active extraBlurView.alphaValue = 0.6 extraBlurView.autoresizingMask = [.width, .height] extraBlurView.frame = rootContentView.bounds rootContentView.addSubview(visualEffectView) rootContentView.addSubview(extraBlurView) hostingView.translatesAutoresizingMaskIntoConstraints = false rootContentView.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.topAnchor.constraint(equalTo: rootContentView.topAnchor), hostingView.bottomAnchor.constraint(equalTo: rootContentView.bottomAnchor), hostingView.leadingAnchor.constraint(equalTo: rootContentView.leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: rootContentView.trailingAnchor) ]) newWindow.contentView = rootContentView // FIXED: Position stash window on the determined screen let screenForPositioning = onScreen ?? NSScreen.screenWithMouse() ?? NSScreen.main ?? NSScreen.screens.first if let screenToDisplayOn = screenForPositioning { let targetY = (screenToDisplayOn.visibleFrame.height - initialStashHeight) / 2 + screenToDisplayOn.visibleFrame.origin.y let spacing: CGFloat = 20 let targetX = screenToDisplayOn.visibleFrame.maxX - initialStashWidth - spacing newWindow.setFrameOrigin(NSPoint(x: targetX, y: targetY)) print("πŸͺŸ Stash window positioned on screen: \(screenToDisplayOn.customLocalizedName) at {\(targetX), \(targetY)}") } else { newWindow.center() // Fallback print("πŸͺŸ Stash window centered (could not determine target screen).") } newWindow.delegate = self let newWindowController = NSWindowController(window: newWindow) newWindowController.showWindow(self) self.stashWindowController = newWindowController self.activeStashImageStore = imageStore if SettingsManager.shared.stashAlwaysOnTop { newWindow.level = .floating print("πŸ” New empty stash window created with always on top") } else { newWindow.level = .normal print("πŸ“‹ New empty stash window created with normal level") } newWindow.makeKeyAndOrderFront(nil as Any?) NSApp.activate(ignoringOtherApps: true) // 🎯 NIEUW: Subtiele shake animatie voor nieuwe stash windows print("πŸŽ‰ Adding shake animation to new stash window") addSubtleShakeAnimation(to: newWindow) } func setTempFileURL(_ url: URL?) { // DEBUG LOG if let newURL = url { print("πŸ”„ setTempFileURL: New URL = \(newURL.path)") } else { print("πŸ”„ setTempFileURL: Clearing URL (nil)") } // AANGEPAST: Behoud alle screenshots, laat cache retention systeem de cleanup doen // Als url is nil, clear alleen de tempURL referentie, verwijder het bestand NIET if url == nil { if let oldURL = tempURL { print("ℹ️ Clearing tempURL reference to: \(oldURL.lastPathComponent) (file kept for cache retention)") } tempURL = nil print("ℹ️ Cleared tempURL reference (screenshot preserved).") } else if let newURL = url { // Geen cleanup van oude files - alle screenshots blijven bestaan voor cache retention if let oldURL = tempURL, oldURL != newURL { print("πŸ“Έ Previous screenshot preserved: \(oldURL.lastPathComponent)") } tempURL = newURL print("ℹ️ Updated tempURL to: \(newURL.lastPathComponent)") } } // MOVED: findFilenameLabel function moved to PreviewManagerDelegate extension func saveImageDataToURL(_ url: URL, from sourceURL: URL) -> Bool { print("πŸ’Ύ Attempting to save data from \(sourceURL.lastPathComponent) to \(url.lastPathComponent)") do { let imageData = try Data(contentsOf: sourceURL) try imageData.write(to: url) print("πŸ“Š Saved PNG data size: \(imageData.count) bytes") return true } catch { print("❌ Error reading from \(sourceURL.path) or writing to \(url.path): \(error)") return false } } // Implementatie van RenameActionHandlerDelegate methoden func getActivePreviewWindow() -> NSWindow? { // NIEUW: Delegate to PreviewManager return previewManager.getActivePreviewWindow() } // MARK: - Legacy code cleanup - CancelRemoveButtonsView methods removed // These methods were replaced by the grid-based Cancel/Remove system in GridComponents func renameActionHandler(_ handler: RenameActionHandler, didRenameFileFrom oldURL: URL, to newURL: URL) { print("Delegate: File was renamed from \(oldURL) to \(newURL)") if self.tempURL == oldURL { self.setTempFileURL(newURL) if let previewWindow = getActivePreviewWindow(), let label = self.findFilenameLabel(in: previewWindow) { label.stringValue = newURL.lastPathComponent label.toolTip = newURL.lastPathComponent } } else { // Zoek in stash if let store = self.activeStashImageStore { if let idx = store.images.firstIndex(where: { $0.fileURL == oldURL }) { store.images[idx].fileURL = newURL store.images[idx].customName = newURL.lastPathComponent print("βœ… Stash image updated to new name \(newURL.lastPathComponent)") } } print("Info: Renamed file was not the active tempURL.") } } // TOEGEVOEGD: Vereiste methode voor RenameActionHandlerDelegate func getScreenshotFolder() -> String? { return SettingsManager.shared.screenshotFolder } // REQUIRED: findFilenameLabel method for RenameActionHandlerDelegate protocol func findFilenameLabel(in window: NSWindow?) -> NSTextField? { guard let targetWindow = window else { return nil } return targetWindow.contentView?.viewWithTag(11001) as? NSTextField } // NIEUW: Hide grid delegate method func hideGrid() { print("πŸ”Ά RenameActionHandlerDelegate: Hiding grid after rename action") // GEWIJZIGD: monitorForReappear naar false zodat de grid NIET LATER KAN TERUGKEREN gridViewManager?.hideGrid(monitorForReappear: false) } // NIEUW: Disable grid monitoring delegate method func disableGridMonitoring() { print("πŸ”Ά RenameActionHandlerDelegate: Disabling grid monitoring during rename") gridViewManager?.disableMonitoring() } // NIEUW: Enable grid monitoring delegate method func enableGridMonitoring() { print("πŸ”Ά RenameActionHandlerDelegate: Enabling grid monitoring after rename") gridViewManager?.enableMonitoring() } // HERSTEL / VOEG DEZE IMPLEMENTATIE TOE BINNEN DE KLASSE func gridViewManager(_ manager: GridViewManager, didDropImage imageURL: URL, ontoCell cellIndex: Int, at dropPoint: NSPoint) { print("βœ… ScreenshotApp: Received delegate call for drop on cell \(cellIndex)") print("πŸ–ΌοΈ Dropped image \(imageURL.path) onto cell \(cellIndex)") self.didGridHandleDrop = true // VERWIJDERD: Oude logica voor het direct verbergen van de grid. // De GridActionManager is nu verantwoordelijk voor het correct verbergen van de grid // na het afhandelen van de actie en de FeedbackBubblePanel (indien van toepassing). // De RenameActionHandler beheert het ook zelf via zijn delegate. // NIEUW: Delegate to GridActionManager gridActionManager.handleGridAction( imageURL: imageURL, cellIndex: cellIndex, dropPoint: dropPoint, gridWindow: manager.gridWindow ) { [weak self] actionCompletedSuccessfully, wasSaveToFolder, isStashAction in guard let self = self else { return } self.gridActionManager.handleActionCompletion( actionCompletedSuccessfully: actionCompletedSuccessfully, wasSaveToFolder: wasSaveToFolder, isStashAction: isStashAction ) } } // De @objc func saveImage() methode moet hier ook staan... @objc func saveImage() { // ... volledige implementatie van saveImage ... } // Nieuwe methode om selectie veilig te annuleren func cancelSelection(overlay: OverlayWindow?) { print("πŸ”΅ Canceling selection...") CGDisplayShowCursor(CGMainDisplayID()) DispatchQueue.main.async { print("πŸ”΅ Ordering out overlay asynchronously from cancelSelection...") overlay?.orderOut(nil as Any?) // Zorg ervoor dat de referentie wordt opgeruimd als deze overlay de actieve was if self.activeOverlayWindow === overlay { self.activeOverlayWindow = nil } } } // Nieuwe functie voor visuele feedback func flashPreviewBorder() { // NIEUW: Delegate to PreviewManager previewManager.flashPreviewBorder() } @objc func saveFromPreview(_ sender: Any) { _saveFromPreview(sender) } // HERSTELDE/TOEGEVOEGDE FUNCTIE @objc func openScreenshotFolder() { _openScreenshotFolder() } // MARK: - Background Remove Helper // REMOVED: Legacy BGR window system - now uses thumbnail-based BGR workflow // 🎨 NEW: Background removal thumbnail workflow func showBackgroundRemovalThumbnail(with image: NSImage, originalURL: URL) { print("🎨 ScreenshotApp: Starting BGR thumbnail workflow") // Always hide the grid when starting BGR workflow gridViewManager?.hideGrid(monitorForReappear: false) // πŸ”§ CRITICAL FIX: Do NOT close existing preview here! // Let the normal drag completion logic in DraggableImageView handle preview closing // based on the user's "close thumbnail after dragging" setting. print("πŸ”„ BGR: Starting BGR workflow without closing existing preview (handled by drag completion)") // Use PreviewManager's new BGR mode with original URL previewManager?.showBackgroundRemovalPreview(originalImage: image, originalURL: originalURL) } // MARK: - OCR & Toast Helpers private func performOcrAndCopy(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?) { // MOVED TO GridActionManager.swift - performOcrAndCopy functionality } private func showToast(message: String, near point: NSPoint, gridWindow: NSWindow?) { // MOVED TO GridActionManager.swift - showToast functionality } // Helper to copy image to clipboard and show toast private func copyImageToClipboard(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?) { // MOVED TO GridActionManager.swift - copyImageToClipboard functionality } // NIEUW: Functie om automatisch opstarten te regelen func toggleLaunchAtLogin(shouldLaunch: Bool) { if #available(macOS 13.0, *) { // Voor macOS 13+ gebruiken we SMAppService.mainApp // Dit werkt direct voor de main app zonder extra configuratie let service = SMAppService.mainApp do { if shouldLaunch { // Controleer eerst de huidige status let currentStatus = service.status print("πŸ” LOGIN: Current SMAppService.mainApp status: \(currentStatus)") switch currentStatus { case .notRegistered: try service.register() print("βœ… LOGIN: App successfully registered to launch at login via SMAppService.mainApp") // Check status na registratie let newStatus = service.status print("πŸ” LOGIN: Status after registration: \(newStatus)") // Geen popup meer - gebruiker krijgt macOS native bevestiging case .enabled: print("ℹ️ LOGIN: App is already enabled for launch at login") case .requiresApproval: print("⚠️ LOGIN: Launch at login requires user approval in System Settings") // Geen popup meer - macOS toont native bevestiging automatisch case .notFound: print("❌ LOGIN: App service not found - SMAppService.mainApp failed") // Fallback naar legacy methode print("πŸ”„ LOGIN: Falling back to legacy method for macOS 13+") toggleLaunchAtLoginLegacy(shouldLaunch: shouldLaunch) return @unknown default: print("⚠️ LOGIN: Unknown SMAppService status: \(currentStatus)") } } else { // Uitschakelen let currentStatus = service.status if currentStatus == .enabled || currentStatus == .requiresApproval { try service.unregister() print("βœ… LOGIN: App unregistered from launch at login (SMAppService.mainApp)") } else { print("ℹ️ LOGIN: App was not registered for launch at login (status: \(currentStatus))") } } } catch { print("❌ LOGIN: Error with SMAppService.mainApp: \(error.localizedDescription)") print("πŸ”„ LOGIN: Falling back to legacy method") toggleLaunchAtLoginLegacy(shouldLaunch: shouldLaunch) return } } else { // Voor macOS < 13 print("⚠️ LOGIN: macOS version < 13.0 detected, using legacy method") toggleLaunchAtLoginLegacy(shouldLaunch: shouldLaunch) } } // Legacy methode voor macOS < 13 en fallback private func toggleLaunchAtLoginLegacy(shouldLaunch: Bool) { // Voor macOS < 13 of als SMAppService.mainApp faalt print("πŸ”„ LOGIN: Using legacy approach - LSSharedFileList") // Probeer LSSharedFileList methode (werkt tot macOS 13) if #available(macOS 10.11, *) { if toggleLaunchAtLoginUsingLSSharedFileList(shouldLaunch: shouldLaunch) { print("βœ… LOGIN: Successfully used LSSharedFileList method") return } } // Als alles faalt, toon instructies aan gebruiker DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Manual Setup Required" if shouldLaunch { alert.informativeText = "To enable ShotScreen to start at login:\n\n1. Open System Settings/Preferences\n2. Go to General β†’ Login Items (or Users & Groups β†’ Login Items)\n3. Click + and add ShotScreen from Applications" } else { alert.informativeText = "To disable ShotScreen from starting at login:\n\n1. Open System Settings/Preferences\n2. Go to General β†’ Login Items (or Users & Groups β†’ Login Items)\n3. Select ShotScreen and click - or toggle it off" } alert.addButton(withTitle: "Open System Settings") alert.addButton(withTitle: "OK") let response = alert.runModal() if response == .alertFirstButtonReturn { // Open System Settings/Preferences if #available(macOS 13.0, *) { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems") { NSWorkspace.shared.open(url) } } else { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { NSWorkspace.shared.open(url) } } } } // Reset de setting omdat gebruiker handmatig moet handelen DispatchQueue.main.async { SettingsManager.shared.startAppOnLogin = false } } // Hulpfunctie voor LSSharedFileList (legacy) - alleen voor testen @available(macOS 10.11, *) private func toggleLaunchAtLoginUsingLSSharedFileList(shouldLaunch: Bool) -> Bool { // Deze API's zijn deprecated vanaf 10.11, dus gebruiken we alleen SMAppService.mainApp print("⚠️ LOGIN: LSSharedFileList APIs are deprecated, falling back to manual setup") return false } // Toon instructies voor gebruiker om launch at login goed te keuren private func showLoginItemApprovalAlert() { DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Launch at Login Requires Approval" alert.informativeText = "To enable ShotScreen to start automatically at login:\n\n1. Open System Settings\n2. Go to General β†’ Login Items\n3. Find ShotScreen and enable it" alert.addButton(withTitle: "Open System Settings") alert.addButton(withTitle: "Cancel") let response = alert.runModal() if response == .alertFirstButtonReturn { // Open System Settings to Login Items if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems") { NSWorkspace.shared.open(url) } } } } // NIEUW: Functie om direct op te slaan zonder interactie func saveDirectlyToConfiguredFolder(isCalledFromPreviewButton: Bool = false) { print("πŸ’Ύ Opslaan naar geconfigureerde map gestart. Oorsprong: \(isCalledFromPreviewButton ? "Preview Knop" : "Automatisch")") guard let sourceURL = self.tempURL else { print("❌ Opslaan Mislukt: Geen tijdelijk screenshot bestand URL beschikbaar.") return } guard let destinationFolderStr = SettingsManager.shared.screenshotFolder, !destinationFolderStr.isEmpty else { print("❌ Opslaan Mislukt: Standaard opslagmap niet ingesteld.") if let window = previewManager.getActivePreviewWindow() { // Toon alleen alert als er een UI context is DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Save Folder Not Set" alert.informativeText = "Please set a default save folder in Settings before using the save button." alert.addButton(withTitle: "OK") alert.beginSheetModal(for: window, completionHandler: nil) } } return } let fileManager = FileManager.default let destinationFolderURL = URL(fileURLWithPath: destinationFolderStr) // 1. Bepaal basisnaam en extensie van de sourceURL var baseName = sourceURL.deletingPathExtension().lastPathComponent let ext = sourceURL.pathExtension.isEmpty ? "png" : sourceURL.pathExtension // Behoud en gebruik 'ext' // 2. Verwijder eventueel bestaande "_getal" suffix van de baseName // Dit voorkomt namen zoals "bestand_1_1.png" als "bestand_1.png" al bestond. if let regex = try? NSRegularExpression(pattern: "_(\\d+)$"), let match = regex.firstMatch(in: baseName, range: NSRange(baseName.startIndex..., in: baseName)), match.numberOfRanges > 1 { let numberRange = Range(match.range(at: 1), in: baseName)! if let _ = Int(baseName[numberRange]) { // Controleer of het echt een getal is baseName = String(baseName[.. 1 { let numberRange = Range(match.range(at: 1), in: baseName)! if let _ = Int(baseName[numberRange]) { baseName = String(baseName[.. NSWindow? { // MOVED TO PreviewManager.swift - showFloatingThumbnail functionality return nil } // MARK: - Screen Selection for Thumbnail func getTargetScreenForThumbnail() -> NSScreen? { // MOVED TO PreviewManager.swift - getTargetScreenForThumbnail functionality return previewManager.getTargetScreenForThumbnail() } // HERNOEM EN PAS AAN: showFloatingThumbnail naar createNewThumbnailWindow // MOVED TO PreviewManager.swift - createNewThumbnailWindow functionality // MARK: - Multi-Monitor Functions // Multi-monitor functionality has been moved to MultiMonitorSystem.swift // MARK: - Clean Desktop Functions // VERWIJDERD: Desktop cleaning functions - feature has been disabled // Cleanup function from MultiMonitorSystem.swift is used instead // MARK: - GridActionManagerDelegate Implementation func disableMonitoring() { // Renamed from gridViewManagerDisableMonitoring gridViewManager?.disableMonitoring() } func enableMonitoring() { // Renamed from gridViewManagerEnableMonitoring gridViewManager?.enableMonitoring() } func gridViewManagerHideGrid(monitorForReappear: Bool) { gridViewManager?.hideGrid(monitorForReappear: monitorForReappear) } func getGridCurrentFrame() -> NSRect? { return gridViewManager?.gridWindow?.frame } // NIEUW: Methode om de "alle schermen" toggle modus te beheren func toggleAllScreensCaptureMode() { isAllScreensCaptureToggledOn.toggle() print("πŸ”„ All Screens Capture Toggled: \(isAllScreensCaptureToggledOn ? "ON" : "OFF")") updateAllScreensModeNotifier() } // MARK: - Public Methods for Stash Preview Positioning /// Returns the target screen for stash preview positioning (same as main thumbnail) func getTargetScreenForStashPreview() -> NSScreen? { return previewManager.getTargetScreenForThumbnail() } // MARK: - Private stash implementation helpers // NIEUW: Subtiele shake animatie voor stash window private func addSubtleShakeAnimation(to window: NSWindow) { print("πŸ”₯ STARTING PROVEN CAKeyframeAnimation shake for stash window") // PROVEN WORKING PARAMETERS from Eric Dolecki blog let numberOfShakes = 4 let durationOfShake = 0.3 let vigourOfShake: CGFloat = 0.03 // 3% of window width - more visible let frame = window.frame print("πŸ”₯ Shake parameters: shakes=\(numberOfShakes), duration=\(durationOfShake)s, vigour=\(vigourOfShake)") print("πŸ”₯ Window frame: \(frame)") // Create CAKeyframeAnimation using PROVEN technique let shakeAnimation = CAKeyframeAnimation() // Create shake path with left-right movement let shakePath = CGMutablePath() shakePath.move(to: CGPoint(x: frame.minX, y: frame.minY)) for _ in 0.. 0 { if persistentStashImageStore == nil { persistentStashImageStore = GalleryImageStore() } // Copy all images to persistent store persistentStashImageStore!.images = currentStore.images print("πŸ”₯ PERSISTENT STASH: Saved \(currentStore.images.count) images for next session") } else { print("πŸ”₯ PERSISTENT STASH: No images to save") } } else { print("πŸ”₯ PERSISTENT STASH: Persistent stash disabled - clearing all images") persistentStashImageStore = nil } stashWindowController = nil activeStashImageStore = nil } // Check voor settings window if let settingsWin = activeSettingsWindow, settingsWin == closingWindow { print("Settings window closed. Clearing reference.") activeSettingsWindow = nil } } } // MARK: - MenuManagerDelegate Implementation extension ScreenshotApp { func triggerScreenshot() { activateMultiMonitorSelection() } func triggerWholeScreenCapture() { print("πŸ–₯️ Triggering whole screen capture under cursor from menu!") // Use existing function that handles everything correctly captureWholeScreenUnderCursor() } func triggerNativeWindowCapture() { print("πŸͺŸ Triggering native window capture - direct window selection!") // Create temporary file for screenshot let tempDirectory = FileManager.default.temporaryDirectory let tempFileName = "ShotScreen_window_\(UUID().uuidString).png" let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) print("πŸ“„ Using temporary file: \(tempFileURL.path)") // Start screencapture to file (NO clipboard) let task = Process() task.launchPath = "/usr/sbin/screencapture" task.arguments = [ "-iw", // Interactive WINDOW selection (direct) "-x", // Do not play sounds tempFileURL.path // Save to file ] do { try task.run() // Wait for screencapture completion in background DispatchQueue.global(qos: .userInitiated).async { [weak self] in task.waitUntilExit() DispatchQueue.main.async { [weak self] in guard let self = self else { return } // Check if file was created (indicates successful capture) if FileManager.default.fileExists(atPath: tempFileURL.path) { // Extra check: Is it a valid image file? if let image = NSImage(contentsOf: tempFileURL), image.size.width > 0 && image.size.height > 0 { print("βœ… Valid window screenshot saved to file - processing...") self.processCapture(image: image) // Clean up temporary file try? FileManager.default.removeItem(at: tempFileURL) print("πŸ—‘οΈ Temporary window screenshot file cleaned up") } else { print("🚫 Invalid/empty image file - right-click cancelled") try? FileManager.default.removeItem(at: tempFileURL) } } else { print("🚫 No screenshot file - ESC or other cancellation") } } } } catch { print("❌ Failed to start window capture: \(error)") } } func triggerAllScreensCapture() { // πŸ”§ FIX: Menu item bypasses desktop filtering - direct native capture print("πŸ“‹ Menu: Capture All Screens - using direct native capture (bypasses desktop filtering)") captureAllScreensNative() } func openSettings() { openSettings(nil) } func showStash() { showStash(nil) } func resetFirstLaunchWizard() { self.resetFirstLaunchWizardInternal() } func toggleDesktopIcons() { // Toggle the setting SettingsManager.shared.hideDesktopIconsDuringScreenshot.toggle() // Update the menu item to reflect the new state menuManager?.refreshDesktopIconsMenuItem() // Log the change let newState = SettingsManager.shared.hideDesktopIconsDuringScreenshot print("πŸ–₯️ Desktop icons during screenshots: \(newState ? "HIDDEN" : "VISIBLE")") // DISABLED: Visual feedback causes crashes due to complex animation closures // showDesktopIconsToggleFeedback(hidden: newState) } func toggleDesktopWidgets() { // Toggle the setting SettingsManager.shared.hideDesktopWidgetsDuringScreenshot.toggle() // Update the menu item to reflect the new state menuManager?.refreshDesktopWidgetsMenuItem() // Log the change let newState = SettingsManager.shared.hideDesktopWidgetsDuringScreenshot print("πŸ“± Desktop widgets during screenshots: \(newState ? "HIDDEN" : "VISIBLE")") } // MARK: - License Management private func initializeLicenseSystem() { print("πŸ” LICENSE: Initializing license system...") // Initialize license manager which will check status on startup _ = LicenseManager.shared // Add observer for license status changes NotificationCenter.default.addObserver( self, selector: #selector(handleLicenseStatusChanged), name: .licenseStatusChanged, object: nil ) // Start observing license status DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.checkLicenseStatusAndEnforcePolicy() } } @objc private func handleLicenseStatusChanged() { DispatchQueue.main.async { self.checkLicenseStatusAndEnforcePolicy() } } private func checkLicenseStatusAndEnforcePolicy() { let licenseManager = LicenseManager.shared print("πŸ” LICENSE: Checking current license status...") switch licenseManager.licenseStatus { case .checking: print("πŸ” LICENSE: Status checking in progress...") // Wait a bit and check again DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.checkLicenseStatusAndEnforcePolicy() } case .licensed(let userName, let email): print("βœ… LICENSE: Valid production license found for \(userName) (\(email))") // App can function normally enableAppFunctionality() case .testLicense(let userName, let email): print("πŸ§ͺ LICENSE: Valid test license found for \(userName) (\(email))") print("πŸ§ͺ LICENSE: Test license detected - app functionality enabled for development") // App can function normally (same as production license) enableAppFunctionality() case .trial(let daysLeft): print("⏰ LICENSE: Trial active with \(daysLeft) days remaining") if daysLeft <= 0 { print("❌ LICENSE: Trial has expired!") showTrialExpiredBlocker() } else { // Show trial reminder for last 2 days if daysLeft <= 2 { showTrialReminder(daysLeft: daysLeft) } enableAppFunctionality() } case .expired: print("❌ LICENSE: Trial has expired!") showTrialExpiredBlocker() case .invalid: print("❌ LICENSE: Invalid license detected!") showInvalidLicenseBlocker() } } private func showTrialReminder(daysLeft: Int) { let alert = NSAlert() alert.alertStyle = .informational alert.messageText = "Trial Reminder" alert.informativeText = """ Your ShotScreen trial expires in \(daysLeft) day\(daysLeft == 1 ? "" : "s"). Purchase a license to continue using ShotScreen after the trial period. """ alert.addButton(withTitle: "Purchase License") alert.addButton(withTitle: "Enter License Key") alert.addButton(withTitle: "Continue Trial") let response = alert.runModal() switch response { case .alertFirstButtonReturn: openPurchaseURL() case .alertSecondButtonReturn: showLicenseEntryDialog() default: break } } private func showTrialExpiredBlocker() { // Create and show a blocking window that prevents app usage let expiredWindow = createTrialExpiredWindow() expiredWindow.makeKeyAndOrderFront(nil) // Disable all app functionality except license entry disableAppFunctionality() } private func showInvalidLicenseBlocker() { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "Invalid License" alert.informativeText = """ Your license is invalid or has been revoked. Please contact support or purchase a new license to continue using ShotScreen. """ alert.addButton(withTitle: "Purchase License") alert.addButton(withTitle: "Enter License Key") alert.addButton(withTitle: "Quit") let response = alert.runModal() switch response { case .alertFirstButtonReturn: openPurchaseURL() case .alertSecondButtonReturn: showLicenseEntryDialog() default: NSApplication.shared.terminate(nil) } } private func createTrialExpiredWindow() -> NSWindow { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 400, height: 350), styleMask: [.titled], backing: .buffered, defer: false ) window.title = "ShotScreen - Trial Expired" window.center() window.level = .modalPanel window.isReleasedWhenClosed = false // Make window non-closable and non-resizable window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true let trialExpiredView = TrialExpiredView() let hostingView = NSHostingView(rootView: trialExpiredView) window.contentView = hostingView return window } private func disableAppFunctionality() { // Disable hotkey hotKey = nil // Close any open windows if let preview = activePreviewWindow { preview.close() } if let stash = stashWindowController?.window { stash.close() } // Hide menu or disable menu items menuManager?.setAppEnabled(false) print("πŸ”’ LICENSE: App functionality disabled due to expired/invalid license") } private func enableAppFunctionality() { // Re-enable hotkey setupHotKey() // Re-enable menu menuManager?.setAppEnabled(true) print("πŸ”“ LICENSE: App functionality enabled with valid license") } private func showLicenseEntryDialog() { LicenseManager.shared.showLicenseEntryDialog() } private func openPurchaseURL() { // ShotScreen Gumroad license product if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") { NSWorkspace.shared.open(url) } } } // MARK: - PreviewManagerDelegate Implementation extension ScreenshotApp { func getLastImage() -> NSImage? { return lastImage } func setLastImage(_ image: NSImage) { lastImage = image } // 🎯 NEW: Create backup for thumbnail restoration private func createImageBackup(image: NSImage, tempURL: URL) { // Store the backup image and path self.backupImage = image self.backupImagePath = tempURL.path // 🎯 NEW: Create a persistent backup file that won't be deleted let backupDirectory = thumbnailDirectory.appendingPathComponent("thumbnail_restoration") do { // Always ensure the directory exists (recreate if user deleted it) try FileManager.default.createDirectory(at: backupDirectory, withIntermediateDirectories: true, attributes: nil) let backupFileURL = backupDirectory.appendingPathComponent("latest_backup.png") if let pngData = createPngData(from: image) { try pngData.write(to: backupFileURL) self.backupImagePath = backupFileURL.path print("πŸ’Ύ Thumbnail restoration backup created: \(tempURL.lastPathComponent) β†’ saved to \(backupFileURL.lastPathComponent)") } } catch { print("❌ Failed to create thumbnail restoration backup file: \(error)") // Fall back to original path (might be deleted) self.backupImagePath = tempURL.path print("πŸ’Ύ Thumbnail restoration backup created: \(tempURL.lastPathComponent) (fallback)") } } // 🎯 NEW: Restore thumbnail after settings changes that may have closed it func restoreCurrentThumbnailIfNeeded() { // Check if there's a backup available and no active thumbnail guard let backupImg = backupImage, let backupPath = backupImagePath, previewManager.getActivePreviewWindow() == nil else { print("πŸ”„ No need to restore thumbnail - either no backup available or thumbnail already open") return } print("πŸ”„ Restoring thumbnail from restoration backup: \(URL(fileURLWithPath: backupPath).lastPathComponent)") // Check if backup file still exists before restoring if !FileManager.default.fileExists(atPath: backupPath) { print("❌ Thumbnail restoration backup file no longer exists at: \(backupPath)") clearImageBackup() return } // Small delay to ensure any settings UI updates are complete DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in guard let self = self else { return } // Create a new temp file for the restored image let newTempURL = self.createTempUrl() do { if let pngData = self.createPngData(from: backupImg) { try pngData.write(to: newTempURL) // Restore the state self.lastImage = backupImg self.tempURL = newTempURL // Show the thumbnail again with the backup image self.showPreview(image: backupImg) print("βœ… Thumbnail restored from restoration backup successfully: \(newTempURL.lastPathComponent)") } else { print("❌ Failed to create PNG data for thumbnail restoration backup restore") } } catch { print("❌ Failed to write restored thumbnail restoration backup file: \(error)") } } } func clearTempFile() { setTempFileURL(nil) // 🎯 NEW: Don't clear thumbnail restoration backup here - only clear when explicitly needed // clearImageBackup() - Removed: Let restore function handle backup cleanup } // 🎯 NEW: Clear thumbnail restoration backup system private func clearImageBackup() { // Clean up backup file if it exists if let backupPath = backupImagePath { let backupURL = URL(fileURLWithPath: backupPath) if backupURL.lastPathComponent == "latest_backup.png" { try? FileManager.default.removeItem(at: backupURL) print("πŸ—‘οΈ Thumbnail restoration backup file deleted: \(backupURL.lastPathComponent)") } } backupImage = nil backupImagePath = nil print("πŸ—‘οΈ Thumbnail restoration backup cleared") } // 🎯 NEW: Clear thumbnail restoration backup only in specific cases func clearBackupAfterNewScreenshot() { clearImageBackup() print("πŸ—‘οΈ Thumbnail restoration backup cleared after new screenshot") } func getTempURL() -> URL? { return tempURL } // MARK: - Private implementations to avoid recursion private func _openScreenshotFolder() { if let path = SettingsManager.shared.screenshotFolder, !path.isEmpty { print("Opening folder: \(path)") let url = URL(fileURLWithPath: path) NSWorkspace.shared.open(url) } else { print("Screenshot folder not set.") let alert = NSAlert() alert.messageText = "Screenshot Folder Not Set" alert.informativeText = "Please set a default screenshot folder in Settings." alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Open Settings") let response = alert.runModal() if response == .alertSecondButtonReturn { _openSettings(nil) } } } private func _openSettings(_ sender: Any?) { renameActionHandler.closeRenamePanelAndCleanup() if let existingWindow = activeSettingsWindow, existingWindow.isVisible { print("Settings window already open. Bringing to front.") existingWindow.level = NSWindow.Level.floating existingWindow.orderFrontRegardless() existingWindow.makeKeyAndOrderFront(nil as Any?) NSApp.activate(ignoringOtherApps: true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { existingWindow.level = NSWindow.Level.normal } return } print("Opening new settings window.") let currentFolder = SettingsManager.shared.screenshotFolder let url = currentFolder != nil ? URL(fileURLWithPath: currentFolder!) : nil let timerValue = SettingsManager.shared.thumbnailTimer let newSettingsWindow = SettingsWindow(currentFolder: url, timerValue: timerValue, delegate: self) self.activeSettingsWindow = newSettingsWindow newSettingsWindow.orderFront(nil as Any?) newSettingsWindow.makeKeyAndOrderFront(nil as Any?) NSApp.activate(ignoringOtherApps: true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { newSettingsWindow.orderFront(nil as Any?) } } private func _saveFromPreview(_ sender: Any) { print("πŸ’Ύ Save From Preview button clicked.") // 🎨 NEW: Check if we're in BGR mode and use the appropriate URL if let previewMgr = previewManager, previewMgr.isBackgroundRemovalMode { let bgrSourceURL: URL? if previewMgr.isShowingProcessedImage { bgrSourceURL = previewMgr.processedImageURL print("πŸ’Ύ BGR Mode: Saving processed image from \(bgrSourceURL?.path ?? "nil")") } else { bgrSourceURL = previewMgr.originalImageURL print("πŸ’Ύ BGR Mode: Saving original image from \(bgrSourceURL?.path ?? "nil")") } if let sourceURL = bgrSourceURL { saveBGRImageToConfiguredFolder(sourceURL: sourceURL, isCalledFromPreviewButton: true) } else { print("❌ BGR Mode: No valid source URL available for save") } } else { // Normal mode - use existing implementation saveDirectlyToConfiguredFolder(isCalledFromPreviewButton: true) } } // MARK: - Desktop Icons Toggle Feedback // DISABLED: These functions cause crashes due to complex animation and window management /* private func showDesktopIconsToggleFeedback(hidden: Bool) { // Create a simple notification-style feedback window let message = hidden ? "Desktop icons will be hidden during screenshots" : "Desktop icons will be visible during screenshots" let icon = hidden ? "folder.badge.minus" : "folder.badge.plus" // Find the screen where the mouse is for positioning let mouseLocation = NSEvent.mouseLocation guard let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) else { return } // Create feedback window let feedbackWindow = createFeedbackWindow(message: message, icon: icon, on: screen) // Show and auto-hide the window feedbackWindow.makeKeyAndOrderFront(nil as Any?) // Enhanced Animation with weak self reference and completion handler for safety var animationWorkItem: DispatchWorkItem? let workItem = DispatchWorkItem { [weak feedbackWindow, weak self] in guard let self = self, let strongFeedbackWindow = feedbackWindow else { return } NSAnimationContext.runAnimationGroup({ context in context.duration = 0.5 // Duration of the fade-out animation context.timingFunction = CAMediaTimingFunction(name: .easeIn) strongFeedbackWindow.animator().alphaValue = 0 }, completionHandler: { strongFeedbackWindow.close() // Ensure the work item is nilled out after execution to break potential retain cycles if animationWorkItem === DispatchWorkItem.cancelled || animationWorkItem?.isCancelled == true { // Already cancelled, do nothing further } else { animationWorkItem = nil // Break cycle after completion } }) } animationWorkItem = workItem // Assign to the tracking variable DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem) } private func createFeedbackWindow(message: String, icon: String, on screen: NSScreen) -> NSWindow { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 300, height: 60), // Adjusted size styleMask: [.borderless], backing: .buffered, defer: false ) window.isReleasedWhenClosed = true // Changed to true window.level = .floating // Keep on top window.backgroundColor = .clear window.isOpaque = false window.hasShadow = false // No shadow for this simple notification window.isMovableByWindowBackground = false window.ignoresMouseEvents = true // Click-through // Position the window at the center of the specified screen let screenRect = screen.visibleFrame let windowRect = window.frame let xPos = screenRect.origin.x + (screenRect.width - windowRect.width) / 2 let yPos = screenRect.origin.y + (screenRect.height - windowRect.height) / 2 window.setFrameOrigin(NSPoint(x: xPos, y: yPos)) // Content View with rounded corners and blur let contentView = NSView() contentView.wantsLayer = true contentView.layer?.cornerRadius = 12 // Rounded corners let blurView = NSVisualEffectView() blurView.material = .hudWindow // Or another appropriate material blurView.blendingMode = .behindWindow blurView.state = .active blurView.frame = contentView.bounds blurView.autoresizingMask = [.width, .height] contentView.addSubview(blurView) // StackView for Icon and Text let stackView = NSStackView() stackView.orientation = .horizontal stackView.alignment = .centerY stackView.spacing = 10 stackView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(stackView) // Icon if let nsIcon = NSImage(systemSymbolName: icon, accessibilityDescription: nil) { let imageView = NSImageView(image: nsIcon) imageView.symbolConfiguration = .init(pointSize: 20, weight: .medium) // Larger icon imageView.contentTintColor = .white // Ensure icon is visible on dark blur stackView.addArrangedSubview(imageView) } // Text let textField = NSTextField(labelWithString: message) textField.textColor = .white // Ensure text is visible textField.font = .systemFont(ofSize: 14, weight: .medium) textField.maximumNumberOfLines = 2 textField.lineBreakMode = .byWordWrapping stackView.addArrangedSubview(textField) // Constraints for StackView NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), // Padding stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), // Padding stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) window.contentView = contentView return window } */ } // MARK: - GridActionManagerDelegate Implementation extension ScreenshotApp { func getRenameActionHandler() -> RenameActionHandler { return renameActionHandler } func showOrUpdateStash(with imageURL: URL) { // Call the existing implementation to avoid recursion _showOrUpdateStash(with: imageURL, onScreen: nil) } func closePreviewWithAnimation(immediate: Bool) { previewManager.closePreviewWithAnimation(immediate: immediate, preserveTempFile: false) } // NIEUW: Grid frame delegate method func getGridWindowFrame() -> NSRect? { return self.gridViewManager?.gridWindow?.frame } } // MARK: - UpdateManagerDelegate Implementation extension ScreenshotApp: UpdateManagerDelegate { func updateCheckDidStart() { print("πŸ”„ UPDATE: Update check started...") } func updateCheckDidFinish() { print("βœ… UPDATE: Update check finished") } func updateAvailable(_ update: SUAppcastItem) { print("πŸŽ‰ UPDATE: Update available - \(update.displayVersionString)") print("πŸ“‹ UPDATE: Current version: \(updateManager.currentVersion)") print("πŸ“‹ UPDATE: New version: \(update.displayVersionString)") // Show update notification DispatchQueue.main.async { [weak self] in let alert = NSAlert() alert.messageText = "Update Available! πŸŽ‰" alert.informativeText = """ ShotScreen \(update.displayVersionString) is now available. You have version \(self?.updateManager.currentVersion ?? "unknown"). Would you like to download and install the update? """ alert.addButton(withTitle: "Download & Install") alert.addButton(withTitle: "Skip This Version") alert.addButton(withTitle: "Remind Me Later") alert.alertStyle = .informational // Set app icon if let appIcon = NSApplication.shared.applicationIconImage { alert.icon = appIcon } let response = alert.runModal() switch response { case .alertFirstButtonReturn: print("βœ… UPDATE: User chose to download and install update") // Sparkle will handle the download and installation automatically case .alertSecondButtonReturn: print("❌ UPDATE: User chose to skip this version") case .alertThirdButtonReturn: print("⏰ UPDATE: User chose to be reminded later") default: break } } } func noUpdateAvailable() { print("ℹ️ UPDATE: No updates available") // Show our simple, clean "no updates" popup DispatchQueue.main.async { [weak self] in print("βœ… UPDATE: Showing our custom 'no update' popup") let alert = NSAlert() alert.messageText = "You're up to date!" alert.informativeText = "ShotScreen \(self?.updateManager.currentVersion ?? "unknown") is currently the newest version available." alert.addButton(withTitle: "OK") alert.alertStyle = .informational alert.runModal() } } func updateCheckFailed(error: Error) { print("❌ UPDATE: Update check failed - \(error.localizedDescription)") DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Update Check Failed" alert.informativeText = "Could not check for updates: \(error.localizedDescription)" alert.addButton(withTitle: "OK") alert.alertStyle = .warning alert.runModal() } } } // MARK: - App Entry Point let app = NSApplication.shared let delegate = ScreenshotApp() app.delegate = delegate app.run() extension NSScreen { static func screenWithMouse(at point: NSPoint? = NSEvent.mouseLocation) -> NSScreen? { guard let point = point else { return nil } return NSScreen.screens.first { $0.frame.contains(point) } } var displayID: CGDirectDisplayID { return deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0 } // Add a helper to get localized name for logging, etc. var customLocalizedName: String { // GEWIJZIGD if #available(macOS 10.15, *) { return self.localizedName // Dit roept de native property aan } else { // Fallback for older macOS versions if needed, or just use description return "Screen \(displayID)" } } } // πŸ”§ DEBUG: Force debug update check function for terminal testing private func performForceDebugUpdate() { print("πŸ”§ DEBUG: Starting force debug update check...") print("πŸ”§ DEBUG: This will bypass development detection and show all Sparkle debug info") print("πŸ”§ DEBUG: ========================================") // Initialize UpdateManager and print debug info UpdateManager.shared.printDebugInfo() // Call the force debug update check UpdateManager.shared.forceDebugUpdateCheck() // Keep the run loop alive for a bit to see the results DispatchQueue.main.asyncAfter(deadline: .now() + 10) { print("πŸ”§ DEBUG: Force debug update test completed") print("πŸ”§ DEBUG: Exiting in 2 seconds...") DispatchQueue.main.asyncAfter(deadline: .now() + 2) { NSApplication.shared.terminate(nil) } } }