import AppKit // Extension voor NSButton hover effecten extension NSButton { private struct AssociatedKeys { static var hoverEffectScale: UInt8 = 0 static var originalTransform: UInt8 = 1 static var originalAnchorPoint: UInt8 = 2 static var originalPosition: UInt8 = 3 static var trackingArea: UInt8 = 4 } private var hoverEffectScale: CGFloat? { get { objc_getAssociatedObject(self, &AssociatedKeys.hoverEffectScale) as? CGFloat } set { objc_setAssociatedObject(self, &AssociatedKeys.hoverEffectScale, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } private var originalTransform: CATransform3D? { get { guard let value = objc_getAssociatedObject(self, &AssociatedKeys.originalTransform) as? NSValue else { return nil } var transform = CATransform3DIdentity value.getValue(&transform) return transform } set { var valueToSet: NSValue? = nil if let transform = newValue { valueToSet = NSValue(caTransform3D: transform) } objc_setAssociatedObject(self, &AssociatedKeys.originalTransform, valueToSet, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } private var originalAnchorPointForHover: CGPoint? { get { objc_getAssociatedObject(self, &AssociatedKeys.originalAnchorPoint) as? CGPoint } set { objc_setAssociatedObject(self, &AssociatedKeys.originalAnchorPoint, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } private var originalPositionForHover: CGPoint? { get { objc_getAssociatedObject(self, &AssociatedKeys.originalPosition) as? CGPoint } set { objc_setAssociatedObject(self, &AssociatedKeys.originalPosition, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } private var hoverTrackingArea: NSTrackingArea? { get { objc_getAssociatedObject(self, &AssociatedKeys.trackingArea) as? NSTrackingArea } set { objc_setAssociatedObject(self, &AssociatedKeys.trackingArea, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } func addHoverEffect(scale: CGFloat = 1.2) { self.wantsLayer = true self.hoverEffectScale = scale if self.originalTransform == nil, let layer = self.layer { self.originalTransform = layer.transform } if self.window != nil { self.updateTrackingAreas() } } override open func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) // Check if this button uses the color change effect (our new simple hover) if objc_getAssociatedObject(self, "useZoomColorEffect") != nil { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.2 context.allowsImplicitAnimation = true self.animator().contentTintColor = NSColor.white // Bright white on hover }) return } // Check if this button uses the new hover effect if objc_getAssociatedObject(self, "useNewHoverEffect") != nil { // Use new hover effect with zoom and color change if let hoverHandler = objc_getAssociatedObject(self, "hoverHandler") as? ButtonHoverHandler { hoverHandler.mouseEntered(with: event) } return } // ONLY process buttons that explicitly have hover effects enabled (old zoom system) guard let scale = self.hoverEffectScale else { return } guard let layer = self.layer else { return } // Extra safety: Check if window is still valid guard self.window != nil else { return } if self.originalTransform == nil { self.originalTransform = layer.transform } if self.originalAnchorPointForHover == nil { self.originalAnchorPointForHover = layer.anchorPoint } if self.originalPositionForHover == nil { self.originalPositionForHover = layer.position } layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) if let oPos = self.originalPositionForHover, let oAP = self.originalAnchorPointForHover { layer.position = CGPoint(x: oPos.x + (layer.anchorPoint.x - oAP.x) * layer.bounds.width, y: oPos.y + (layer.anchorPoint.y - oAP.y) * layer.bounds.height) } NSAnimationContext.runAnimationGroup({ context in context.duration = 0.2 context.allowsImplicitAnimation = true layer.transform = CATransform3DScale(self.originalTransform ?? CATransform3DIdentity, scale, scale, 1) }, completionHandler: nil) } override open func mouseExited(with event: NSEvent) { super.mouseExited(with: event) // Check if this button uses the color change effect (our new simple hover) if objc_getAssociatedObject(self, "useZoomColorEffect") != nil { let originalColor = objc_getAssociatedObject(self, "originalColor") as? NSColor ?? NSColor(white: 0.8, alpha: 1.0) NSAnimationContext.runAnimationGroup({ context in context.duration = 0.25 context.allowsImplicitAnimation = true self.animator().contentTintColor = originalColor // Restore original color }) return } // Check if this button uses the new hover effect if objc_getAssociatedObject(self, "useNewHoverEffect") != nil { // Use new hover effect with zoom and color change if let hoverHandler = objc_getAssociatedObject(self, "hoverHandler") as? ButtonHoverHandler { hoverHandler.mouseExited(with: event) } return } // ONLY process buttons that explicitly have hover effects enabled (old zoom system) guard self.hoverEffectScale != nil else { return } guard let layer = self.layer else { return } // Extra safety: Check if window is still valid guard self.window != nil else { return } NSAnimationContext.runAnimationGroup({ context in context.duration = 0.25 context.allowsImplicitAnimation = true layer.transform = self.originalTransform ?? CATransform3DIdentity }, completionHandler: { [weak self] in // Extra safety in completion handler guard let self = self, let safeLayer = self.layer, self.window != nil else { return } if let oAP = self.originalAnchorPointForHover { safeLayer.anchorPoint = oAP } if let oPos = self.originalPositionForHover { safeLayer.position = oPos } }) } override open func viewWillMove(toSuperview newSuperview: NSView?) { super.viewWillMove(toSuperview: newSuperview) if newSuperview == nil, let trackingArea = self.hoverTrackingArea { self.removeTrackingArea(trackingArea) self.hoverTrackingArea = nil } } override open func updateTrackingAreas() { super.updateTrackingAreas() // Check if this button uses the new hover effect let hasNewHoverEffect = objc_getAssociatedObject(self, "useNewHoverEffect") if hasNewHoverEffect != nil { // Don't interfere with custom tracking areas for new hover effect return } // Check if this button uses the color change effect let hasColorEffect = objc_getAssociatedObject(self, "useZoomColorEffect") // Process buttons that have ANY hover effect (old system OR new color effect) guard self.hoverEffectScale != nil || hasColorEffect != nil else { return } if let existingTrackingArea = self.hoverTrackingArea { self.removeTrackingArea(existingTrackingArea) } guard self.bounds != .zero else { return } let trackingArea = NSTrackingArea(rect: self.bounds, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil) self.addTrackingArea(trackingArea) self.hoverTrackingArea = trackingArea } }