Files
shotscreen/ShotScreen/Sources/main.swift
Nick Roodenrijs 0dabed11d2 🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set:

 Core Features:
- Advanced screenshot capture system
- Multi-monitor support
- Professional UI/UX design
- Automated update system with Sparkle
- Apple notarized & code signed

🛠 Technical Excellence:
- Native Swift macOS application
- Professional build & deployment pipeline
- Comprehensive error handling
- Memory optimized performance

📦 Distribution Ready:
- Professional DMG packaging
- Apple notarization complete
- No security warnings for users
- Ready for public distribution

This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
2025-06-28 16:15:15 +02:00

3680 lines
162 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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[..<Range(match.range(at: 0), in: baseName)!.lowerBound])
print(" Gecorrigeerde basisnaam (na verwijderen _getal suffix): \(baseName)")
}
}
var finalFilename = "\(baseName).\(ext)"
var destinationURL = destinationFolderURL.appendingPathComponent(finalFilename)
var counter = 1
// Maak doelmap aan indien nodig
if !fileManager.fileExists(atPath: destinationFolderURL.path) {
do {
try fileManager.createDirectory(at: destinationFolderURL, withIntermediateDirectories: true, attributes: nil)
print("📁 Map aangemaakt: \(destinationFolderURL.path)")
} catch {
print("❌ Opslaan Mislukt: Kon map niet aanmaken: \(error)")
return
}
}
// Loop om een unieke bestandsnaam te vinden
while fileManager.fileExists(atPath: destinationURL.path) {
finalFilename = "\(baseName)_\(counter).\(ext)"
destinationURL = destinationFolderURL.appendingPathComponent(finalFilename)
counter += 1
// Correctie hieronder: gebruik de correct geformatteerde finalFilename in de print statement
print("⚠️ Opslaan: Bestand \"\(destinationFolderURL.appendingPathComponent(baseName).appendingPathExtension(ext).lastPathComponent)\" bestaat al, probeert nu: \(finalFilename)")
}
do {
try fileManager.copyItem(at: sourceURL, to: destinationURL)
print("✅ Screenshot gekopieerd naar: \(destinationURL.path)")
// VOEG TOE: Flash animatie AANROEPEN
if previewManager.getActivePreviewWindow() != nil && isCalledFromPreviewButton {
previewManager.flashPreviewBorder()
}
if SettingsManager.shared.closeAfterSave {
print("⚙️ Temp file \(sourceURL.lastPathComponent) opgeruimd na opslaan (closeAfterSave is ON).")
self.setTempFileURL(nil)
if previewManager.getActivePreviewWindow() != nil { // Sluit preview alleen als die er was
previewManager.closePreviewWithAnimation(immediate: false, preserveTempFile: false)
}
} else {
// Als closeAfterSave UIT is:
// - Bij automatisch opslaan: de capture() methode heeft de preview al getoond.
// - Bij handmatig opslaan (save knop): preview blijft open.
print("⚙️ Preview blijft open na opslaan (closeAfterSave is OFF) of werd al beheerd door capture().")
}
} catch {
print("❌ Opslaan Mislukt: Fout bij kopiëren bestand: \(error)")
if let window = previewManager.getActivePreviewWindow() { // Toon alleen alert als er een UI context is
DispatchQueue.main.async {
let errorAlert = NSAlert(error: error)
errorAlert.messageText = "Error Saving File"
errorAlert.beginSheetModal(for: window, completionHandler: nil)
}
}
}
}
// 🎨 NEW: Save BGR image to configured folder
func saveBGRImageToConfiguredFolder(sourceURL: URL, isCalledFromPreviewButton: Bool = false) {
print("💾 Opslaan BGR naar geconfigureerde map gestart. Oorsprong: \(isCalledFromPreviewButton ? "Preview Knop" : "Automatisch")")
guard let destinationFolderStr = SettingsManager.shared.screenshotFolder,
!destinationFolderStr.isEmpty else {
print("❌ Opslaan Mislukt: Standaard opslagmap niet ingesteld.")
if let window = previewManager.getActivePreviewWindow() {
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
// 2. Verwijder eventueel bestaande "_getal" suffix van de baseName
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]) {
baseName = String(baseName[..<Range(match.range(at: 0), in: baseName)!.lowerBound])
print(" Gecorrigeerde BGR basisnaam (na verwijderen _getal suffix): \(baseName)")
}
}
var finalFilename = "\(baseName).\(ext)"
var destinationURL = destinationFolderURL.appendingPathComponent(finalFilename)
var counter = 1
// Maak doelmap aan indien nodig
if !fileManager.fileExists(atPath: destinationFolderURL.path) {
do {
try fileManager.createDirectory(at: destinationFolderURL, withIntermediateDirectories: true, attributes: nil)
print("📁 BGR Map aangemaakt: \(destinationFolderURL.path)")
} catch {
print("❌ BGR Opslaan Mislukt: Kon map niet aanmaken: \(error)")
return
}
}
// Loop om een unieke bestandsnaam te vinden
while fileManager.fileExists(atPath: destinationURL.path) {
finalFilename = "\(baseName)_\(counter).\(ext)"
destinationURL = destinationFolderURL.appendingPathComponent(finalFilename)
counter += 1
print("⚠️ BGR Opslaan: Bestand \"\(destinationFolderURL.appendingPathComponent(baseName).appendingPathExtension(ext).lastPathComponent)\" bestaat al, probeert nu: \(finalFilename)")
}
do {
try fileManager.copyItem(at: sourceURL, to: destinationURL)
print("✅ BGR Screenshot gekopieerd naar: \(destinationURL.path)")
// Flash animatie voor feedback
if previewManager.getActivePreviewWindow() != nil && isCalledFromPreviewButton {
previewManager.flashPreviewBorder()
}
// BGR mode: don't close automatically, let user continue working with the images
print("⚙️ BGR Preview blijft open na opslaan voor verdere bewerking.")
} catch {
print("❌ BGR Opslaan Mislukt: Fout bij kopiëren bestand: \(error)")
if let window = previewManager.getActivePreviewWindow() {
DispatchQueue.main.async {
let errorAlert = NSAlert(error: error)
errorAlert.messageText = "Error Saving BGR File"
errorAlert.beginSheetModal(for: window, completionHandler: nil)
}
}
}
}
// MARK: - AutoCrop Helper
// Functie autoCropToScreen hieronder is verwijderd.
// Helper: bereken bounding box van niet-transparante pixels
// Functie nonTransparentBoundingBox hieronder is verwijderd.
// Helper: zorg dat de preview weer zichtbaar wordt als hij verborgen was
func ensurePreviewVisible() {
previewManager.ensurePreviewVisible()
}
// MARK: - DraggableImageViewClickHandler
func thumbnailWasClicked(image: NSImage) {
print("🖼️ ScreenshotApp: thumbnailWasClicked delegate method received.")
print("🖼️ ScreenshotApp: Thumbnail click received. Attempting to open with system's Preview.app.")
// 🎨 NEW: Check if we're in BGR mode and use the appropriate URL
let urlToOpen: URL?
if let previewMgr = previewManager, previewMgr.isBackgroundRemovalMode {
if previewMgr.isShowingProcessedImage {
urlToOpen = previewMgr.processedImageURL
print("🖼️ BGR Mode: Opening processed image: \(urlToOpen?.path ?? "nil")")
} else {
urlToOpen = previewMgr.originalImageURL
print("🖼️ BGR Mode: Opening original image: \(urlToOpen?.path ?? "nil")")
}
} else {
urlToOpen = self.tempURL
print("🖼️ Normal Mode: Opening tempURL: \(urlToOpen?.path ?? "nil")")
}
guard let finalURL = urlToOpen else {
print("❌ ScreenshotApp: No URL available. Cannot open in Preview.app.")
return
}
// Close our own active preview window (if any) as we are opening in Preview.app.
// We don't need to preserve the temp file here in the same way,
// as the system will now handle it. However, the temp file should NOT be deleted
// by closePreviewWithAnimation if the system needs it.
// Let's close our preview immediately.
if activePreviewWindow != nil {
print("🖼️ ScreenshotApp: Closing existing activePreviewWindow before opening in system Preview.app.")
// We set preserveTempFile to true because NSWorkspace.open is async,
// and we don't want our app to delete the file before Preview.app opens it.
// Preview.app will make its own copy if needed.
closePreviewWithAnimation(immediate: true, preserveTempFile: true)
}
// Open the file with the default application (usually Preview.app for images)
DispatchQueue.main.async { // NSWorkspace.open can sometimes be better on main queue for UI related actions
if NSWorkspace.shared.open(finalURL) {
print("✅ ScreenshotApp: Successfully requested system to open \(finalURL.path)")
// If we want to DEFINITELY close our thumbnail after opening in Preview:
// This depends on the desired UX. For now, let's assume the thumbnail
// *could* remain if not explicitly closed by other settings (like closeAfterSave, closeAfterDrag).
// If the tempURL is from a screenshot just taken, the normal timer/drag/save logic
// for closing the thumbnail would still apply.
// If the user clicks, and it opens in Preview, should our thumbnail *always* close?
// For now, let's NOT force-close our own thumbnail here.
// It will close based on existing timer/drag/save rules.
} else {
print("❌ ScreenshotApp: Failed to open \(finalURL.path) with system Preview.app.")
// Fallback: If opening with system fails, maybe show our own preview?
// self.showPreview(image: image)
}
}
}
// MARK: - Simple Test View for Debugging Shadow & Corner Radius VERWIJDERD
func showFloatingThumbnail(image: NSImage) -> 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..<numberOfShakes {
// Move left from center
shakePath.addLine(to: CGPoint(
x: frame.minX - frame.size.width * vigourOfShake,
y: frame.minY
))
// Move right from center
shakePath.addLine(to: CGPoint(
x: frame.minX + frame.size.width * vigourOfShake,
y: frame.minY
))
}
shakePath.closeSubpath()
shakeAnimation.path = shakePath
shakeAnimation.duration = durationOfShake
// CRITICAL: Apply animation to window using animator
window.animations = ["frameOrigin": shakeAnimation]
window.animator().setFrameOrigin(window.frame.origin)
print("🔥 CAKeyframeAnimation applied! Window should shake horizontally")
// Debug completion
DispatchQueue.main.asyncAfter(deadline: .now() + durationOfShake + 0.1) {
print("✨ Stash window shake animation completed!")
}
}
var shouldTerminate: Bool = false
}
// MARK: - NSWindowDelegate Extension
extension ScreenshotApp: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
guard let closingWindow = notification.object as? NSWindow else { return }
// Controleer of het gesloten venster ons Stash venster was
if let stashCtrl = stashWindowController, stashCtrl.window == closingWindow {
print("🔥 PERSISTENT STASH: Stash window closing...")
// 🔥 NIEUW: Persistent stash logic!
if SettingsManager.shared.persistentStash {
print("🔥 PERSISTENT STASH: Persistent stash enabled - saving images for next session")
// Save current images to persistent store
if let currentStore = activeStashImageStore, currentStore.images.count > 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)
}
}
}