🍁

Swift: NSButtonへの右クリックを左クリック同様に扱う

2021/07/21に公開

NSButtonactiontargetを指定して、sendAction(on: NSEvent.EventTypeMask)を使うことで、クリックの種類を指定しながらボタンが押された時のイベントハンドリング可能です。

例★
var button = NSButton(title: "push me", target: self, action: #selector(pushed(_:)))
button.sendAction(on: [.rightMouseDown])

@objc func pushed(_ sender: Any?) { }

いや、そんなことありませんでした!

このようなやり方で右クリックに対応したところで、ボタンを押したときのハイライトはうまく機能しませんし、左クリックで押したときのハイライトもキャンセルできません。

実はNSButtonのデフォルトの挙動は少し特殊です。mouseDown(with:)はコールされますが、mouseUp(with:)およびmouseDragged(with:)はコールされません。そのくせ、rightMouseDown(with:)rightMouseUp(with:)rightMouseDragged(with:)はコールされます。この不思議な挙動はNSButtonを継承したボタンのクラスを作れば分かります。

MyButton
class MyButton: NSButton {
    
    override func mouseDown(with event: NSEvent) {
        print("🐸")
        super.mouseDown(with: event)
    }
    
    override func mouseDragged(with event: NSEvent) {
        print("🐙")
        super.mouseDragged(with: event)
    }
    
    override func mouseUp(with event: NSEvent) {
        print("🦑")
        super.mouseUp(with: event)
    }
    
    override func rightMouseDown(with event: NSEvent) {
        print("🐤")
        super.rightMouseDown(with: event)
    }
    
    override func rightMouseDragged(with event: NSEvent) {
        print("🐶")
        super.rightMouseDragged(with: event)
    }
    
    override func rightMouseUp(with event: NSEvent) {
        print("🐮")
        super.rightMouseUp(with: event)
    }
    
}

これを使ったとき、🐸🐤🐶🐮しか出力されません。

そこで、右クリックをした際も左クリックした時と同様な挙動にさせたい時はどうすればいいのか、Googleの海を彷徨ったところ。Appleの古いリファレンスに辿り着きました。
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingMouseEvents/HandlingMouseEvents.html#//apple_ref/doc/uid/10000060i-CH6-SW4
どうやら、AppleのコードではNSButtonのmouseDownの中で、ドラッグからマウスリリースまでの操作をwhileループで監視するような仕組みを使っているらしいのです。(実際にwhileループしているかは謎)

ということで、それを再現するようなカスタムボタンクラスを書いてみました。

MyButton
class MyButton: NSButton {
    
    private(set) var eventTypeMask: NSEvent.EventTypeMask = []
    
    override func sendAction(on mask: NSEvent.EventTypeMask) -> Int {
        eventTypeMask = mask
        return Int(mask.rawValue)
    }
    
    override func mouseDown(with event: NSEvent) {
        guard eventTypeMask.contains(.leftMouseDown) ||
                eventTypeMask.contains(.leftMouseUp)
        else { return }
        self.highlight(true)
        self.sendAction(self.action, to: self.target)

        var keepOn = true
        while keepOn {
            if let theEvent = self.window?.nextEvent(matching: [.leftMouseDragged, .leftMouseUp]) {
                switch theEvent.type {
                case .leftMouseDragged:
                    self.highlight(self.frame.contains(theEvent.locationInWindow))
                case .leftMouseUp:
                    keepOn = false
                    self.highlight(false)
                    if self.frame.contains(theEvent.locationInWindow) && eventTypeMask.contains(.leftMouseUp) {
                        self.sendAction(self.action, to: self.target)
                    }
                default:
                    break
                }
            }
        }
    }
    
    override func mouseDragged(with event: NSEvent) {
        // 何もさせない
    }
    
    override func mouseUp(with event: NSEvent) {
        // 何もさせない
    }
    
    override func rightMouseDown(with event: NSEvent) {
        guard eventTypeMask.contains(.rightMouseDown) ||
                eventTypeMask.contains(.rightMouseUp)
        else { return }
        self.highlight(true)
        self.sendAction(self.action, to: self.target)

        var keepOn = true
        while keepOn {
            if let theEvent = self.window?.nextEvent(matching: [.rightMouseDragged, .rightMouseUp]) {
                switch theEvent.type {
                case .rightMouseDragged:
                    self.highlight(self.frame.contains(theEvent.locationInWindow))
                case .rightMouseUp:
                    keepOn = false
                    self.highlight(false)
                    if self.frame.contains(theEvent.locationInWindow) && eventTypeMask.contains(.rightMouseUp) {
                        self.sendAction(self.action, to: self.target)
                    }
                default:
                    break
                }
            }
        }
    }
    
    override func rightMouseDragged(with event: NSEvent) {
        // 何もさせない
    }
    
    override func rightMouseUp(with event: NSEvent) {
        // 何もさせない
    }
    
}

これを上記例★のように使用した場合は、右クリックで押した時だけハイライトが反応し、押した瞬間にイベントが流れてくるようになります。イベントを受け取るためにはsendAction(on:)が必須になりますが、むしろコードで確実にハンドルするものを限定できるので嬉しいと思います。

ドラッグ中のイベントをハンドリングしたり、ホイールボタンのクリックも使いたいという場合は、上記コードに少し手を加えればできると思います。

Discussion