틸트할 때 함께 들어오는 클릭 막기

틸트 비트는 macOS에 4~7번 버튼으로도 전달되어 앱이 휠 클릭처럼 처리하거나,
휠을 옆으로 밀 때 휠 버튼이 같이 눌리는 경우가 있었다. 이벤트 탭에서 4~7번
버튼과 틸트 직후 0.15초 안의 휠 클릭을 버리고, 버린 버튼의 드래그·떼기도
함께 버린다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-10-03 12:23:33 +09:00
co-authored by Claude Opus 5.5
parent d840bfd842
commit 1af177f30d
2 changed files with 38 additions and 1 deletions
+2
View File
@@ -4,6 +4,8 @@ Razer Pro Click V2 (vendor 5426, product 209)의 휠 좌우 틸트를 각각 `Co
마우스 휠의 세로 스크롤 방향도 반전합니다. 트랙패드와 Magic Mouse처럼 제스처로 스크롤하는 장치와 가로 스크롤은 바꾸지 않습니다. 마우스 휠의 세로 스크롤 방향도 반전합니다. 트랙패드와 Magic Mouse처럼 제스처로 스크롤하는 장치와 가로 스크롤은 바꾸지 않습니다.
틸트할 때 함께 들어오는 원래 버튼 이벤트(4~7번 버튼, 틸트 직후의 휠 클릭)는 막습니다. 측면 버튼도 4·5번 버튼이라 원래 동작 대신 뒤로/앞으로 가기로 동작합니다.
## 설치 / 업데이트 ## 설치 / 업데이트
```sh ```sh
+36 -1
View File
@@ -151,6 +151,39 @@ let accessTimer: Timer? = accessGranted() ? nil : {
// Invert vertical mouse-wheel scrolling. Trackpads and Magic Mouse send gesture phases; // Invert vertical mouse-wheel scrolling. Trackpads and Magic Mouse send gesture phases;
// wheels do not, even when their high-resolution scrolling is marked continuous. // wheels do not, even when their high-resolution scrolling is marked continuous.
var scrollTap: CFMachPort? var scrollTap: CFMachPort?
// Mouse buttons whose down event was swallowed, so the matching drag/up is swallowed too.
var suppressedButtons = Set<Int64>()
// CG button numbers (0-based) of the tilt bits 0x08/0x10/0x20/0x40, i.e. buttons 4-7.
let tiltButtonNumbers: ClosedRange<Int64> = 3...6
let middleButtonNumber: Int64 = 2
// A wheel click this close to a tilt is the wheel being pressed while it is pushed sideways.
let tiltClickWindow: TimeInterval = 0.15
func tiltActive() -> Bool {
if state.lastBits.values.contains(where: { $0 != 0 }) { return true }
let now = ProcessInfo.processInfo.systemUptime
return state.lastFired.values.contains { now - $0 < tiltClickWindow }
}
// Swallow the native button events behind a tilt: many apps treat buttons 4-7 like a
// middle click (e.g. open link in new tab), and tilting can also press the wheel button.
func shouldSuppress(_ type: CGEventType, _ event: CGEvent) -> Bool {
let button = event.getIntegerValueField(.mouseEventButtonNumber)
switch type {
case .otherMouseDown:
if tiltButtonNumbers.contains(button) || (button == middleButtonNumber && tiltActive()) {
suppressedButtons.insert(button)
return true
}
return false
case .otherMouseDragged:
return suppressedButtons.contains(button)
case .otherMouseUp:
return suppressedButtons.remove(button) != nil
default:
return false
}
}
// Marks the replacement events this app posts so the tap lets them through untouched. // Marks the replacement events this app posts so the tap lets them through untouched.
let invertedScrollMarker: Int64 = 0x52544D31 let invertedScrollMarker: Int64 = 0x52544D31
let scrollCallback: CGEventTapCallBack = { proxy, type, event, _ in let scrollCallback: CGEventTapCallBack = { proxy, type, event, _ in
@@ -158,6 +191,7 @@ let scrollCallback: CGEventTapCallBack = { proxy, type, event, _ in
if let tap = scrollTap { CGEvent.tapEnable(tap: tap, enable: true) } if let tap = scrollTap { CGEvent.tapEnable(tap: tap, enable: true) }
return Unmanaged.passUnretained(event) return Unmanaged.passUnretained(event)
} }
if shouldSuppress(type, event) { return nil }
guard type == .scrollWheel, guard type == .scrollWheel,
event.getIntegerValueField(.eventSourceUserData) != invertedScrollMarker event.getIntegerValueField(.eventSourceUserData) != invertedScrollMarker
else { return Unmanaged.passUnretained(event) } else { return Unmanaged.passUnretained(event) }
@@ -189,7 +223,8 @@ let scrollCallback: CGEventTapCallBack = { proxy, type, event, _ in
} }
if accessGranted() { if accessGranted() {
scrollTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, scrollTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap,
eventsOfInterest: CGEventMask(1 << CGEventType.scrollWheel.rawValue), eventsOfInterest: [CGEventType.scrollWheel, .otherMouseDown, .otherMouseDragged, .otherMouseUp]
.reduce(CGEventMask(0)) { $0 | CGEventMask(1 << $1.rawValue) },
callback: scrollCallback, userInfo: nil) callback: scrollCallback, userInfo: nil)
if let tap = scrollTap { if let tap = scrollTap {
CFRunLoopAddSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0), .commonModes) CFRunLoopAddSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0), .commonModes)