🎉 ShotScreen v1.0 - Initial Release
🚀 First official release of ShotScreen with complete feature set: ✨ Core Features: - Advanced screenshot capture system - Multi-monitor support - Professional UI/UX design - Automated update system with Sparkle - Apple notarized & code signed 🛠 Technical Excellence: - Native Swift macOS application - Professional build & deployment pipeline - Comprehensive error handling - Memory optimized performance 📦 Distribution Ready: - Professional DMG packaging - Apple notarization complete - No security warnings for users - Ready for public distribution This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
This commit is contained in:
236
ShotScreen/Sources/ButtonHoverExtension.swift
Normal file
236
ShotScreen/Sources/ButtonHoverExtension.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user