🚀 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.
3680 lines
162 KiB
Swift
3680 lines
162 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|