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