Hide the mouse in a Mac application after not moving it

1.4k views Asked by At

I am currently making a slideshow application for Mac using Swift. In this application I want to hide the mouse while the slideshow is running and the mouse has not been moved for a while, pretty much like it is done in Quick Time Player for example.

Now I have tried a bunch of ways using NSCursor.hide() and NSCursor.unhide() as well as NSCursor.setHiddenUntilMouseMoves(), but non of it quite worked.

Firstly I failed to make it call the mouseMoved function in my main ViewController and secondly NSCursor.setHiddenUntilMouseMoves() doesn't always seem to work even though I have not touched my trackpad at all. It is right after the code that changes the image in the slideshow and I see the image being changed, but using the debugger it doesn't stop on that line of code when the cursor isn't hidden.

Would someone be able to show me a general way how to get this to work? I'm pretty sure this is not such an exotic thing to do and there is much easier ways to do it than what I am trying.

The following is what I've tried:

import Cocoa

class DiashowViewController: NSViewController {

    enum DiashowState {
        case playing
        case paused
        case stopped
    }

    var files: [URL]?
    var diaTimer = Timer()
    var diashowState: DiashowState = .stopped

    var mouseTimer = Timer()

    @IBOutlet weak var diaView: NSImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.

        }
    }

    func playDiashow() {
        if diashowState == .paused {
            diaTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.changeDia), userInfo: nil, repeats: true)
            diashowState = .playing
            NSCursor.setHiddenUntilMouseMoves(true)
        }
    }

    func playDiashow(withFiles files: [URL]) {
        stopDiashow()

        self.files = files
        diashowState = .paused

        playDiashow()
        changeDia()
    }

    func pauseDiashow() {
        if diashowState == .playing {
            diaTimer.invalidate()
            diashowState = .paused
        }
    }

    override func mouseMoved(with event: NSEvent) {
        print("MOUSE MOVED")
    }

    func stopDiashow() {
        pauseDiashow()
        diaView.image = nil
        files = nil
        diashowState = .stopped
    }

    func changeDia() {
        if diashowState == .playing {
            let i = Int(arc4random_uniform(UInt32(files!.count)))
            let thisDiaURL = files![i]
            let thisDia = NSImage(contentsOf: thisDiaURL)
            thisDia?.size = NSSize(width: (thisDia?.representations.first?.pixelsWide)!, height: (thisDia?.representations.first?.pixelsHigh)!)
            diaView.image = thisDia
            NSCursor.setHiddenUntilMouseMoves(true)
            print("HIDE MOUSE")
        }
    }

}

Thanks a lot in advance!

1

There are 1 answers

6
Warren Burton On BEST ANSWER

To receive mouseMoved events you need to add a NSTrackingArea to the view and as you have probably discovered the setHiddenUntilMouseMoves setting is single shot and needs to be reasserted after a mouse move in that state.

Rather than trying to unravel your code i made a demo project where I setup a window with a view and a button. The view changes color from red to green to show state.

enter image description here

class ViewController: NSViewController {

    @IBOutlet weak var xview: NSView!

    override func viewDidLoad() {
        super.viewDidLoad()
        xview.wantsLayer = true
    }

    //1.
    var isPresentingSlideshow = false
    @IBAction func toggle(_ sender: Any) {

        if(isPresentingSlideshow) {
            isPresentingSlideshow = false
            xview.layer?.backgroundColor = NSColor.green.cgColor
            teardownTracking()
        }
        else {
            isPresentingSlideshow = true
            xview.layer?.backgroundColor = NSColor.red.cgColor
            setupTracking()
        }
    }

    //2.
    var trackingArea:NSTrackingArea?
    func setupTracking() {
        let area = NSTrackingArea(rect: xview.bounds, options: [.activeAlways,.mouseEnteredAndExited,.mouseMoved,.inVisibleRect]  , owner: self, userInfo: nil)
        xview.addTrackingArea(area)
        trackingArea = area
    }

    //3.
    func teardownTracking() {
        if let trackingArea = trackingArea {
            xview.removeTrackingArea(trackingArea)
            self.trackingArea = nil
            NSCursor.setHiddenUntilMouseMoves(false)
        }
    }

    //4.
    var cursorHideState = false
    override func mouseMoved(with event: NSEvent) {
        super.mouseMoved(with: event)

        if !cursorHideState {
            cursorHideState = true
            //5.
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                [weak self] in
                if let _ = self?.trackingArea {
                    NSCursor.setHiddenUntilMouseMoves(true)
                    self?.cursorHideState = false
                }
            }
        }
    }

}

What going on here is.

  1. Simple toggle action to flip between playing/not-playing state and reflect that in the color state.
  2. Adds a NSTrackingArea to the view. The owner is this view controller so it will receive the mouseMoved: event. The option .mouseMoved is required to set this up.
  3. Removes the tracking area from the view when the slide show isn't playing and sets setHiddenUntilMouseMoves to false.
  4. The mouseMoved: handler
  5. The setHiddenUntilMouseMoves is set true after two seconds as long as the tracking area exists and its not already waiting. Note the weak reference to self prevents a possible retain cycle here.

This isn't perfect as you might find that the cursor hides once after leaving your window but should get you going in the right direction.