スペースバーを押したときだけ手のひらツールにする
- Apple
- 開発
「スペースバーが押されているあいだだけ手のひらツールに変化1させるのが大変だった」という話を耳にしたので自分も挑戦してみた。もっときれいな方法や間違いがあったら教えてください。
import Cocoa
class CanvasView: NSView
{
var cursorTrackingArea: NSTrackingArea!
private func _commonInit()
{
layerContentsRedrawPolicy = .onSetNeedsDisplay
cursorTrackingArea = NSTrackingArea(rect: .zero, options: [.activeInKeyWindow, .cursorUpdate, .inVisibleRect], owner: self, userInfo: nil)
addTrackingArea(cursorTrackingArea)
}
override init(frame: NSRect)
{
super.init(frame: frame)
_commonInit()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
_commonInit()
}
deinit
{
removeTrackingArea(cursorTrackingArea)
}
//MARK: - View Flags
override var wantsUpdateLayer: Bool { return true }
override var isOpaque: Bool { return true }
override var canBecomeKeyView: Bool { return true }
override var acceptsFirstResponder: Bool { return true }
//MARK: - Drawing
override func updateLayer()
{
layer?.backgroundColor = NSColor.textBackgroundColor.cgColor
}
//MARK: - Hit Test
override func hitTest(_ pointInSuperview: NSPoint) -> NSView?
{
if effectiveTool.interceptsDescendantEvents {
guard let point = superview?.convert(pointInSuperview, to: self) else {
return nil
}
return (isMousePoint(point, in: bounds) ? self : nil)
} else {
return super.hitTest(pointInSuperview)
}
}
//MARK: - Mouse Events
override func mouseDown(with event: NSEvent)
{
window?.makeFirstResponder(self)
switch effectiveTool {
case .hand:
window?.trackEvents(matching: [.leftMouseDragged, .leftMouseUp], timeout: 0.5, mode: .eventTracking, handler: { (event, stopPtr) in
//check the button state just in case.
guard ((NSEvent.pressedMouseButtons & (1 << 0)) != 0) else {
stopPtr.pointee = true
return
}
guard let event = event else {
return
}
switch event.type {
case .leftMouseUp:
stopPtr.pointee = true
default:
//do some work for scroll here.
break
}
})
default:
break
}
}
//MARK: - Keyboard Events
override func keyDown(with event: NSEvent)
{
if (event.keyCode == 49/*space*/) {
isSpaceBarPressed = true
} else {
isSpaceBarPressed = false
super.keyDown(with: event)
}
}
override func keyUp(with event: NSEvent)
{
if (event.keyCode == 49/*space*/) {
isSpaceBarPressed = false
} else {
super.keyUp(with: event)
}
}
//MARK: - Cursor Events
override func cursorUpdate(with event: NSEvent)
{
effectiveTool.cursor.set()
}
//MARK: - Tool
public enum Tool
{
case select
case hand
var cursor: NSCursor
{
switch self {
case .select:
return .arrow
case .hand:
return .openHand
}
}
var interceptsDescendantEvents: Bool
{
switch self {
case .hand:
return true
default:
return false
}
}
}
/// Tool selected by user.
public var tool = Tool.select
{
didSet { updateEffectiveTool() }
}
/// Actual effective tool which generally equals to `tool` but changes on some conditions.
public private(set) var effectiveTool = Tool.select
{
didSet
{
if (oldValue != effectiveTool) {
window?.invalidateCursorRects(for: self)
}
}
}
private func updateEffectiveTool()
{
if isSpaceBarPressed {
effectiveTool = .hand
} else {
effectiveTool = tool
}
}
//MARK: - Keyboard Flags
private var isSpaceBarPressed = false
{
didSet { updateEffectiveTool() }
}
}
以下、解説。
ツールの切り替え
スペースバーが押されているあいだだけ手のひらツールに変化させ、それ以外にはユーザの選択したツールが有効になるのがよくあるパターンなので再現してみる。
tool
(ユーザが選択するツール)とeffectiveTool
(実際に有効なツール)を別プロパティとして用意。keyDown(with:)
とkeyUp(with:)
のタイミングでisSpaceBarPressed
(スペースバーが押されているかのフラグ)を操作。- フラグが変更されたらそれらをもとに
effectiveTool
を更新。
キーボードイベントが受け取れるようにビューがレスポンダチェーンに存在する必要があるはず。
カーソルの変更
Cocoa におけるカーソルの変更は独特なので注意。ビューには cursor
みたいなプロパティがなくて直接「現在のカーソル」を NSCursor.set()
で切り替えるのだが、好きなタイミングでセットしてもほかの要因ですぐに上書きされてしまう。
ビューの cursorUpdate(with:)
をオーバーライドして、状態やポイントされている位置に応じた NSCursor
をセットする処理を実装しておきそれが適切なタイミングで呼ばれるようにするのが基本だ。
init(...)
でNSTrackingArea
を追加しておく。カーソルの更新が目的なので.cursorUpdate
オプションは必須。さらに.inVisibleRect
を指定すればサイズが変わるたびに追加しなおす必要がなくなる。- するとマウスが触れたり離れたりしたときに
cursorUpdate(with:)
が呼ばれるようになるので、そこでeffectiveTool
に応じたNSCursor
をセット。 effectiveTool
が変更されたときにウインドウのinvalidateCursorRects(for:)
を呼ぶようにすれば、そこでもcursorUpdate(with:)
が呼ばれる2。
その他の工夫
mouseDragged(with:)
を使ってドラッグイベントを捉えようとするとカーソルがウインドウの外に出たときに変化してしまうのでtrackEvents(matching:timeout:mode:handler:)
を使って余計なイベントが割り込んでこないようにしてみた。下手な実装をするとループから抜け出せないことがあるので注意3。hitTest(_:)
をオーバーライドして、手のひらツールのときは子孫ビューにマウスイベントが行かないようにする。
Share
リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。