66//
77
88import Cocoa
9+ import CommonCrypto
910
1011class MouseViewController : NSViewController {
1112 private var cursorImageView : NSImageView !
12- private var activeMonitorObserver : NSKeyValueObservation !
13+ private var enabledMonitorObserver : NSKeyValueObservation !
14+ private var cursorObserver : NSKeyValueObservation !
1315 private var enabledMonitors : Set < CGDirectDisplayID > = Set < CGDirectDisplayID > ( )
14- private var currentCursor : NSCursor = NSCursor . current
15- private var lastUpdate : Date = Date ( milliseconds: 0 )
16+ private var currentMonitor : NSScreen ?
1617
17- private let hideRefreshIntervalMs = 1500
18+ @objc private dynamic var currentCursor : NSCursor = NSCursor . current
19+
20+ private let pointingHandHash : String ? = NSCursor . pointingHand. image. tiffRepresentation? . sha256
21+ private let openHandHash : String ? = NSCursor . openHand. image. tiffRepresentation? . sha256
22+ private let closedHandHash : String ? = NSCursor . closedHand. image. tiffRepresentation? . sha256
23+ private let hideRefreshIntervalSecs = 1.5
1824
1925 override init ( nibName nibNameOrNil: NSNib . Name ? , bundle nibBundleOrNil: Bundle ? ) {
2026 super. init ( nibName: nibNameOrNil, bundle: nibBundleOrNil)
@@ -26,82 +32,125 @@ class MouseViewController: NSViewController {
2632 setupMouseViewController ( )
2733 }
2834
35+ deinit {
36+ enabledMonitorObserver? . invalidate ( )
37+ cursorObserver? . invalidate ( )
38+ }
39+
2940 private func setupMouseViewController( ) {
30- activeMonitorObserver = UserDefaults . standard. observe ( \. activeMonitors, options: [ . initial, . new] , changeHandler: { ( defaults, change) in
31- if let mons = defaults. activeMonitors as [ CGDirectDisplayID ] ? {
32- self . enabledMonitors = Set ( mons. map { $0 } )
33- }
34- } )
35- NSCursor . addObserver ( self , forKeyPath: " currentSystem " , options: [ . new, . old] , context: nil )
3641 cursorImageView = NSImageView ( )
3742 cursorImageView. wantsLayer = true
3843 cursorImageView. image = currentCursor. image
3944 cursorImageView. imageScaling = . scaleNone
4045 cursorImageView. layer? . backgroundColor = NSColor . clear. cgColor
4146 self . view = cursorImageView
47+
48+ setupObservers ( )
4249 }
4350
44- override func observeValue ( forKeyPath keyPath : String ? , of object : Any ? , change : [ NSKeyValueChangeKey : Any ] ? , context : UnsafeMutableRawPointer ? ) {
45- if keyPath == " currentSystem " {
46- if let newCursor = change ? [ NSKeyValueChangeKey . newKey ] as? NSCursor {
47- self . cursorImageView . image = newCursor . image
51+ private func setupObservers ( ) {
52+ enabledMonitorObserver = UserDefaults . standard . observe ( \ . activeMonitors , options : [ . initial , . new ] , changeHandler : { ( defaults , change ) in
53+ if let mons = defaults . activeMonitors as [ CGDirectDisplayID ] ? {
54+ self . enabledMonitors = Set ( mons . map { $0 } )
4855 }
49- }
56+ } )
57+ cursorObserver = self . observe ( \. currentCursor, options: [ . initial, . new] , changeHandler: { ( this, change) in
58+ self . cursorImageView. image = this. currentCursor. image
59+ } )
5060 }
5161
62+ /** These magic numbers work for the default mouse cursor at the default size on MacOS Monterey.
63+ I can't guarantee it will work for custom mouse cursors or different OS versions, but I think it'll be pretty close. **/
5264 func getFrameOrigin( _ pt: NSPoint ) -> NSPoint {
5365 var offset : ( Double , Double )
66+
67+ // There is no simple way to check the exact image of a cursor, but size is cheap, easy, and works in most cases to determine the offset.
5468 switch ( currentCursor. image. size) {
5569 case NSCursor . arrow. image. size:
56- fallthrough
70+ offset = ( 21 , 32 )
5771 case NSCursor . pointingHand. image. size:
58- offset = ( 21.5 , 32 )
59- break
72+ // There are representations that match the size of pointingHand but not the offset, so check the hash of the image
73+ switch ( currentCursor. image. tiffRepresentation? . sha256) {
74+ case pointingHandHash:
75+ offset = ( 21.5 , 32 )
76+ case openHandHash:
77+ offset = ( 24.5 , 24.5 )
78+ case closedHandHash:
79+ offset = ( 24.5 , 25 )
80+ default :
81+ // It is likely a type of iBeam
82+ offset = ( 24 , 28 )
83+ }
6084 case NSCursor . iBeam. image. size:
6185 fallthrough
6286 case NSCursor . resizeLeftRight. image. size:
63- fallthrough
64- case NSCursor . resizeUpDown. image. size:
6587 offset = ( 24.5 , 24.5 )
66- break
88+ case NSCursor . disappearingItem. image. size:
89+ offset = ( 16.5 , 38.5 )
90+ case NSCursor . dragLink. image. size:
91+ offset = ( 28 , 32 )
6792 default :
6893 offset = ( 23.5 , 25.5 )
6994 }
7095
7196 return NSPoint ( x: pt. x - offset. 0 , y: pt. y - offset. 1 )
7297 }
7398
74- func update( _ event: NSEvent ) {
75- currentCursor = NSCursor . currentSystem != nil ? NSCursor . currentSystem! : currentCursor
76- cursorImageView. image = currentCursor. image
99+
100+ override func viewDidLoad( ) {
101+ let aCursor = NSCursor . resizeUpDown
102+ view. addCursorRect ( view. bounds, cursor: aCursor)
103+ aCursor. set ( )
104+ view. addTrackingRect ( view. bounds, owner: aCursor, userData: nil , assumeInside: true )
77105
78- let now = Date . now
79- guard ( now. millisecondsSince1970 - lastUpdate. millisecondsSince1970) > hideRefreshIntervalMs else { return }
80- cursorImageView. isHidden = NSScreen . screens. reduce ( true ) { ( result, screen) in
81- // If the monitor is not enabled, return immediately with a vote to `hide`
82- guard enabledMonitors. contains ( screen. deviceDescription [ NSDeviceDescriptionKey ( " NSScreenNumber " ) ] as! CGDirectDisplayID ) else { return result && true }
83-
84- let screenBounds = screen. visibleFrame
85- let mousePosition = event. locationInWindow
86-
87- return result && !screenBounds. contains ( mousePosition)
106+ }
107+
108+ func resetCurrentMonitor( _ mousePosition: NSPoint ? = nil ) {
109+ if ( mousePosition != nil ) {
110+ currentMonitor = NSScreen . screens. first ( where: { $0. frame. contains ( mousePosition!) } )
88111 }
89- lastUpdate = now
112+ cursorImageView . isHidden = !enabledMonitors . contains ( currentMonitor ? . deviceDescription [ NSDeviceDescriptionKey ( " NSScreenNumber " ) ] as? CGDirectDisplayID ?? kCGNullDirectDisplay )
90113 }
91-
92- deinit {
93- activeMonitorObserver? . invalidate ( )
94- NSCursor . removeObserver ( self , forKeyPath: " current " )
114+
115+ func update( _ event: NSEvent ) -> Bool {
116+ if ( NSCursor . currentSystem != nil ) {
117+ currentCursor = NSCursor . currentSystem!
118+ }
119+
120+ let screenBounds = currentMonitor? . frame
121+ // Apply a small transform to the y axis to avoid it going out of bounds along the top axis
122+ let mousePosition = event. locationInWindow. applying ( CGAffineTransform ( translationX: 0.0 , y: - 0.00001 ) )
123+ if ( screenBounds? . contains ( mousePosition) != true ) {
124+ resetCurrentMonitor ( mousePosition)
125+ }
126+
127+ return !cursorImageView. isHidden
95128 }
96129}
97130
98- extension Date {
99- var millisecondsSince1970 : Int64 {
100- Int64 ( ( self . timeIntervalSince1970 * 1000.0 ) . rounded ( ) )
131+ extension Data {
132+ public var sha256 : String {
133+ get {
134+ return hexStringFromData ( input: digest ( input: self as NSData ) )
135+ }
101136 }
102-
103- init ( milliseconds: Int64 ) {
104- self = Date ( timeIntervalSince1970: TimeInterval ( milliseconds) / 1000 )
137+
138+ private func digest( input : NSData ) -> NSData {
139+ let digestLength = Int ( CC_SHA256_DIGEST_LENGTH)
140+ var hash = [ UInt8] ( repeating: 0 , count: digestLength)
141+ CC_SHA256 ( input. bytes, UInt32 ( input. length) , & hash)
142+ return NSData ( bytes: hash, length: digestLength)
105143 }
106- }
107144
145+ private func hexStringFromData( input: NSData ) -> String {
146+ var bytes = [ UInt8] ( repeating: 0 , count: input. length)
147+ input. getBytes ( & bytes, length: input. length)
148+
149+ var hexString = " "
150+ for byte in bytes {
151+ hexString += String ( format: " %02x " , UInt8 ( byte) )
152+ }
153+
154+ return hexString
155+ }
156+ }
0 commit comments