Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-04-11

Swift Code Library: Detecting mouse clicks, single double or triple.

2015.05.07: Alert! it seems that it can and does happen that the clickcount for a draggingEnded event is sometimes 1. Hence the function that handles a single-click should also check if it is in fact a draggingEnded.

2015.04.13: Completely replaced the previous code with a new and better implementation.

When handling mouse-clicks in an application that must differentiate between single and double-clicks (and even triple-clicks) is a job that just begs for code reuse. As you may known, when the application receives a "triple-click-mouse-up" event, it has already received a single-click-mouse-up event and a double-click-mouse-up event. Any double-click-mouse-up or triple-click-mouse-up event may have to cancel the operation of the preceding event.

In this post I have included the source code for BRMouseUpView, a child class of NSView that replaces the mouseUp function with a "filteredMouseUp" function. The filteredMouseUp function will only be called for the last mouseUp event in a sequence. Be aware that this also means that any mouseclick action is called with a small delay. This delay depends on the "Double-Click speed" setting of the user in his "Mouse" Control Panel. This may result in an unacceptable user experience. If you use this code, please make sure that you application remains usable even when the user sets the double-click speed to its slowest value.

The code:

import Foundation
import Cocoa
import Dispatch


/// This class replaces the "mouseUp" function with a "filteredMouseUp" function. The filteredMouseUp function is only called for the last mouseUp even in a sequence of mouseUp events. A sequence of mouseUp events is defined by the "doubleClickInterval" (which is set by the user in the mouse control panel) and the maximum number of mouseUp clicks in a sequence as defined by the variable "maxClickCount".
///
/// Note: A mouseUp event for the end of a dragging sequence has a clickCount of 0, this mouseUp call is not filtered out!
///
/// Note2: By default the maxClickCount is set to 2, limiting the detection to drag-end-clicks, single-clicks and double-clicks. Set this variable to your desired maximum sequence length. Note that filteredMouseUp is never called for an event with a clickcount higher than this number.
//
// A suggested use of the filteredMouseUp is as follows:
//
// override func filteredMouseUp(theEvent: NSEvent) {
//     switch theEvent.clickCount {
//     case 0: draggingEnded(theEvent)
//     case 1: singleMouseClick(theEvent)
//     case 2: doubleMouseCLick(theEvent)
//     etc...
//     }
// }

class BRMouseUpView: NSView {
    
    
    // The double click time as defined by the user in the system preferences panel.
    
    final let systemDoubleClickDelay = NSEvent.doubleClickInterval()

    
    // The last mouse up event
    
    private final var lastMouseUpEvent: NSEvent!
    
    
    // Speed up the user experience by defining the maximum number of clicks in a sequence.
    // "filteredMouseUp" will not receive events with a higher clickCount than defined in this variable.
    
    final var maxClickCount = 2
    
    
    // This function will execute the last mouseUp in a sequence of mouseUp events
    
    private func delayedMouseUpAction(event: NSEvent) {
        

        // Check if a newer event is present in the lastMouseUpEvent. Done by comparing the clickcount.
        
        if event.clickCount >= lastMouseUpEvent.clickCount {
            
            
            // It is the same, no new mouseUp was received since this function was pushed on the queue
            
            filteredMouseUp(event)
        }
        
        // else: no else, if the old clickcount is lower than the lastMouseUpEvent clickcount, this call should be ignored.
    }
    
    
    // Handles the mouseUp event. Should not be overriden (there should be no need anymore to override this implementation).
    
    final override func mouseUp(theEvent: NSEvent) {
        
        dispatch_async(dispatch_get_main_queue(), {
            
            [unowned self] in
            
            
            // Special case: end of dragging, always call the filteredMouseUp for this case
            
            if theEvent.clickCount == 0 {
                self.filteredMouseUp(theEvent)
                return
            }
            

            // Store the latest event, this is used to discard older events
            
            self.lastMouseUpEvent = theEvent

            
            // Speed up the user experience for the max click count
            
            if theEvent.clickCount == self.maxClickCount {
                
                self.filteredMouseUp(theEvent)
                return
                
            } else if theEvent.clickCount < self.maxClickCount {
            
                // Dispatch this event, it will be canceled if another mouseUp event occurs within the doubleClickInterval.
                
                let executionTime = dispatch_time(DISPATCH_TIME_NOW, Int64(1_000_000_000 * self.systemDoubleClickDelay))
                dispatch_after(executionTime, dispatch_get_main_queue(), {
                    [unowned self] in self.delayedMouseUpAction(theEvent)
                })
            }
        })
    }

    
    /// Handles a double mouse click on the mouse-up event. To be overriden by a child class

    func filteredMouseUp(theEvent: NSEvent) {
        // Remove or replace as necessary
        log.atLevelWarning(id: 0, source: "BRMouseHandlingView.handleMouseUpEvent", message: "Childview from BRMouseHandlingView should implement handleMouseUpEvent")
    }

}

PS: I use SwifterLog as the logging framework, if you use something else, please adjust the logging calls where needed.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment