From 1af177f30d848d27fff6807611a89eaa2bb51fea Mon Sep 17 00:00:00 2001 From: jsahn Date: Sat, 3 Oct 2026 12:23:33 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8B=B8=ED=8A=B8=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EB=93=A4=EC=96=B4=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EB=A7=89=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 틸트 비트는 macOS에 4~7번 버튼으로도 전달되어 앱이 휠 클릭처럼 처리하거나, 휠을 옆으로 밀 때 휠 버튼이 같이 눌리는 경우가 있었다. 이벤트 탭에서 4~7번 버튼과 틸트 직후 0.15초 안의 휠 클릭을 버리고, 버린 버튼의 드래그·떼기도 함께 버린다. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 ++ razer-tilt.swift | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 393a69d..054d277 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Razer Pro Click V2 (vendor 5426, product 209)의 휠 좌우 틸트를 각각 `Co 마우스 휠의 세로 스크롤 방향도 반전합니다. 트랙패드와 Magic Mouse처럼 제스처로 스크롤하는 장치와 가로 스크롤은 바꾸지 않습니다. +틸트할 때 함께 들어오는 원래 버튼 이벤트(4~7번 버튼, 틸트 직후의 휠 클릭)는 막습니다. 측면 버튼도 4·5번 버튼이라 원래 동작 대신 뒤로/앞으로 가기로 동작합니다. + ## 설치 / 업데이트 ```sh diff --git a/razer-tilt.swift b/razer-tilt.swift index e912eef..8f652a8 100644 --- a/razer-tilt.swift +++ b/razer-tilt.swift @@ -151,6 +151,39 @@ let accessTimer: Timer? = accessGranted() ? nil : { // Invert vertical mouse-wheel scrolling. Trackpads and Magic Mouse send gesture phases; // wheels do not, even when their high-resolution scrolling is marked continuous. var scrollTap: CFMachPort? +// Mouse buttons whose down event was swallowed, so the matching drag/up is swallowed too. +var suppressedButtons = Set() +// CG button numbers (0-based) of the tilt bits 0x08/0x10/0x20/0x40, i.e. buttons 4-7. +let tiltButtonNumbers: ClosedRange = 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. let invertedScrollMarker: Int64 = 0x52544D31 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) } return Unmanaged.passUnretained(event) } + if shouldSuppress(type, event) { return nil } guard type == .scrollWheel, event.getIntegerValueField(.eventSourceUserData) != invertedScrollMarker else { return Unmanaged.passUnretained(event) } @@ -189,7 +223,8 @@ let scrollCallback: CGEventTapCallBack = { proxy, type, event, _ in } if accessGranted() { 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) if let tap = scrollTap { CFRunLoopAddSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0), .commonModes)