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