Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

919 lines
31 KiB
Swift

import AppKit
import ApplicationServices
import Foundation
import IOKit.pwr_mgt
import OSLog
import SwiftUI
import MacToolsPluginKit
@MainActor
final class PhysicalCleanModeSession: NSObject, NSWindowDelegate {
private static let nullAssertionID = IOPMAssertionID(0)
private static let rightMouseHoldDurationSeconds = 3
private struct NotificationObservation {
let center: NotificationCenter
let token: NSObjectProtocol
}
enum EndReason {
case userRequested
case emergency(String)
}
private enum SessionError: LocalizedError {
case eventTapUnavailable
case runLoopSourceUnavailable
case missingScreens
case overlayWindowCreationFailed
case idleLockPreventionFailed
var errorDescription: String? {
localizedDescription(localization: PluginLocalization(bundle: .main))
}
func localizedDescription(localization: PluginLocalization) -> String {
switch self {
case .eventTapUnavailable:
return localization.string(
"session.error.eventTapUnavailable",
defaultValue: "无法创建输入拦截器,请确认已授予辅助功能权限。"
)
case .runLoopSourceUnavailable:
return localization.string(
"session.error.runLoopSourceUnavailable",
defaultValue: "无法初始化输入拦截运行循环。"
)
case .missingScreens:
return localization.string(
"session.error.missingScreens",
defaultValue: "未检测到可用屏幕,无法进入清洁模式。"
)
case .overlayWindowCreationFailed:
return localization.string(
"session.error.overlayWindowCreationFailed",
defaultValue: "无法创建屏幕覆盖窗口,已取消进入清洁模式。"
)
case .idleLockPreventionFailed:
return localization.string(
"session.error.idleLockPreventionFailed",
defaultValue: "无法启用防空闲锁屏保护,已取消进入清洁模式。"
)
}
}
}
private enum EventTapExitReason {
case shortcut
case rightMouseHold
case disabledByTimeout
case disabledByUserInput
}
private enum RightMouseButtonEvent {
case down
case up
}
private enum EventTapInterruptionReason: CustomStringConvertible {
case disabledByTimeout
case disabledByUserInput
var description: String {
switch self {
case .disabledByTimeout:
return "disabledByTimeout"
case .disabledByUserInput:
return "disabledByUserInput"
}
}
var exitReason: EventTapExitReason {
switch self {
case .disabledByTimeout:
return .disabledByTimeout
case .disabledByUserInput:
return .disabledByUserInput
}
}
}
private final class OverlayWindow: NSWindow {
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
}
@MainActor
private final class OverlayHintModel: ObservableObject {
@Published var keyboardExitHintText = ""
@Published var rightMouseExitHintText = ""
}
private struct OverlayWatermarkView: View {
@ObservedObject var model: OverlayHintModel
var body: some View {
ZStack(alignment: .bottomTrailing) {
Color.clear
VStack(alignment: .trailing, spacing: 4) {
Text(model.keyboardExitHintText)
.lineLimit(1)
.minimumScaleFactor(0.8)
Text(model.rightMouseExitHintText)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.font(.system(size: 18, weight: .medium, design: .rounded))
.foregroundStyle(Color.white.opacity(0.78))
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, 18)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color.white.opacity(0.07))
)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.white.opacity(0.14), lineWidth: 1)
)
.shadow(color: Color.black.opacity(0.65), radius: 18, y: 4)
.padding(.trailing, 32)
.padding(.bottom, 28)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}
private final class EventTapContext {
let exitBinding: ShortcutBinding
private let lock = NSLock()
private let onExit: (EventTapExitReason) -> Void
private let onInterruption: (EventTapInterruptionReason, Bool) -> Void
private let onRightMouseButtonEvent: (RightMouseButtonEvent) -> Void
private var hasTriggeredExit = false
private var eventTap: CFMachPort?
init(
exitBinding: ShortcutBinding,
onExit: @escaping (EventTapExitReason) -> Void,
onInterruption: @escaping (EventTapInterruptionReason, Bool) -> Void,
onRightMouseButtonEvent: @escaping (RightMouseButtonEvent) -> Void
) {
self.exitBinding = exitBinding
self.onExit = onExit
self.onInterruption = onInterruption
self.onRightMouseButtonEvent = onRightMouseButtonEvent
}
func requestExit(_ reason: EventTapExitReason) {
let shouldNotify: Bool
lock.lock()
shouldNotify = !hasTriggeredExit
if shouldNotify {
hasTriggeredExit = true
}
lock.unlock()
guard shouldNotify else {
return
}
onExit(reason)
}
func setEventTap(_ eventTap: CFMachPort) {
lock.lock()
self.eventTap = eventTap
lock.unlock()
}
func notifyRightMouseButtonEvent(_ event: RightMouseButtonEvent) {
let shouldNotify: Bool
lock.lock()
shouldNotify = !hasTriggeredExit
lock.unlock()
guard shouldNotify else {
return
}
onRightMouseButtonEvent(event)
}
func recoverTap(_ reason: EventTapInterruptionReason) {
var didRecover = false
var exitReason: EventTapExitReason?
lock.lock()
if hasTriggeredExit {
lock.unlock()
return
}
if let eventTap {
CGEvent.tapEnable(tap: eventTap, enable: true)
didRecover = CGEvent.tapIsEnabled(tap: eventTap)
if !didRecover {
hasTriggeredExit = true
exitReason = reason.exitReason
}
} else {
hasTriggeredExit = true
exitReason = reason.exitReason
}
lock.unlock()
onInterruption(reason, didRecover)
if let exitReason {
onExit(exitReason)
}
}
}
let exitBinding: ShortcutBinding
private let onEnd: (EndReason) -> Void
private let localization: PluginLocalization
private let logger = PhysicalCleanModeLog.session
private let sessionIdentifier = String(UUID().uuidString.prefix(8))
private let overlayHintModel = OverlayHintModel()
private var overlayWindows: [OverlayWindow] = []
private var eventTap: CFMachPort?
private var eventTapRunLoopSource: CFRunLoopSource?
private var eventTapContextPointer: UnsafeMutableRawPointer?
private var notificationObservers: [NotificationObservation] = []
private var previousPresentationOptions: NSApplication.PresentationOptions?
private var cursorHidden = false
private var idleSleepAssertionID = IOPMAssertionID(0)
private var isStopping = false
private var rightMouseHoldTimer: Timer?
private var rightMouseHoldRemainingSeconds: Int?
private var tapDisableTimestamps: [Date] = []
private var keyboardExitHintText: String {
localization.format(
"overlay.exit.keyboardFormat",
defaultValue: "按 %@ 退出",
Self.displayTokens(for: exitBinding).joined(separator: " + ")
)
}
private var rightMouseExitHintText: String {
let remainingSeconds = rightMouseHoldRemainingSeconds ?? Self.rightMouseHoldDurationSeconds
return localization.format(
"overlay.exit.rightMouseFormat",
defaultValue: "或长按鼠标右键 %ds 退出",
remainingSeconds
)
}
init(
exitBinding: ShortcutBinding,
onEnd: @escaping (EndReason) -> Void,
localization: PluginLocalization = PluginLocalization(bundle: .main)
) {
self.exitBinding = exitBinding
self.onEnd = onEnd
self.localization = localization
super.init()
overlayHintModel.keyboardExitHintText = keyboardExitHintText
overlayHintModel.rightMouseExitHintText = rightMouseExitHintText
}
func start() throws {
guard eventTap == nil, overlayWindows.isEmpty else {
return
}
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] starting session")
}
let tapContext = EventTapContext(
exitBinding: exitBinding,
onExit: { [weak self] reason in
Task { @MainActor in
self?.handleEventTapExit(reason)
}
},
onInterruption: { [weak self] reason, didRecover in
Task { @MainActor in
self?.handleEventTapInterruption(reason, didRecover: didRecover)
}
},
onRightMouseButtonEvent: { [weak self] event in
Task { @MainActor in
self?.handleRightMouseButtonEvent(event)
}
}
)
let contextPointer = UnsafeMutableRawPointer(Unmanaged.passRetained(tapContext).toOpaque())
let tap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: Self.interceptedEventMask,
callback: Self.eventTapCallback,
userInfo: contextPointer
)
guard let tap else {
logger.error("[\(self.sessionIdentifier, privacy: .public)] failed to create event tap")
Unmanaged<EventTapContext>.fromOpaque(contextPointer).release()
throw SessionError.eventTapUnavailable
}
guard let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) else {
logger.error("[\(self.sessionIdentifier, privacy: .public)] failed to create event tap run loop source")
CFMachPortInvalidate(tap)
Unmanaged<EventTapContext>.fromOpaque(contextPointer).release()
throw SessionError.runLoopSourceUnavailable
}
eventTap = tap
eventTapRunLoopSource = runLoopSource
eventTapContextPointer = contextPointer
tapContext.setEventTap(tap)
CGEvent.tapEnable(tap: tap, enable: false)
do {
previousPresentationOptions = NSApp.presentationOptions
NSApp.activate(ignoringOtherApps: true)
NSApp.presentationOptions = [
.hideDock,
.hideMenuBar,
.disableProcessSwitching
]
try enableIdleLockPrevention()
try rebuildOverlayWindows()
installObservers()
hideCursor()
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, CFRunLoopMode.commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] session start completed windows=\(self.overlayWindows.count, privacy: .public)")
}
} catch {
logger.error("[\(self.sessionIdentifier, privacy: .public)] startup failed: \(error.localizedDescription, privacy: .public)")
tearDown(shouldNotify: false, endReason: .userRequested)
throw localizedStartupError(from: error)
}
}
func requestStop(reason: EndReason) {
tearDown(shouldNotify: true, endReason: reason)
}
func requestEmergencyExit(message: String) {
logger.error("[\(self.sessionIdentifier, privacy: .public)] emergency exit requested message=\(message, privacy: .public)")
requestStop(reason: .emergency(message))
}
func windowWillClose(_ notification: Notification) {
guard !isStopping else {
return
}
logger.error("[\(self.sessionIdentifier, privacy: .public)] overlay window will close unexpectedly")
requestEmergencyExit(message: localization.string(
"session.error.overlayWindowUnexpectedlyClosed",
defaultValue: "清洁模式覆盖窗口意外关闭,已恢复系统输入。"
))
}
private func installObservers() {
let appCenter = NotificationCenter.default
let workspaceCenter = NSWorkspace.shared.notificationCenter
notificationObservers.append(
NotificationObservation(
center: appCenter,
token: appCenter.addObserver(
forName: NSApplication.didChangeScreenParametersNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
self?.handleScreenParametersChanged()
}
}
)
)
notificationObservers.append(
NotificationObservation(
center: appCenter,
token: appCenter.addObserver(
forName: NSApplication.willTerminateNotification,
object: NSApp,
queue: .main
) { [weak self] _ in
Task { @MainActor in
self?.requestStop(reason: .userRequested)
}
}
)
)
notificationObservers.append(
NotificationObservation(
center: workspaceCenter,
token: workspaceCenter.addObserver(
forName: NSWorkspace.sessionDidResignActiveNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
guard let self else { return }
self.handleWorkspaceInterruption(
message: self.localization.string(
"session.error.workspaceInactiveEmergencyExit",
defaultValue: "当前会话已锁定或切出,已自动退出清洁模式。"
)
)
}
}
)
)
notificationObservers.append(
NotificationObservation(
center: workspaceCenter,
token: workspaceCenter.addObserver(
forName: NSWorkspace.screensDidSleepNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
guard let self else { return }
self.handleWorkspaceInterruption(
message: self.localization.string(
"session.error.screensDidSleepEmergencyExit",
defaultValue: "屏幕已进入睡眠,已自动退出清洁模式。"
)
)
}
}
)
)
notificationObservers.append(
NotificationObservation(
center: workspaceCenter,
token: workspaceCenter.addObserver(
forName: NSWorkspace.willSleepNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
guard let self else { return }
self.handleWorkspaceInterruption(
message: self.localization.string(
"session.error.systemWillSleepEmergencyExit",
defaultValue: "系统即将进入睡眠,已自动退出清洁模式。"
)
)
}
}
)
)
}
private func removeObservers() {
for observer in notificationObservers {
observer.center.removeObserver(observer.token)
}
notificationObservers.removeAll()
}
private func handleScreenParametersChanged() {
guard !isStopping else {
return
}
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] screen parameters changed, rebuilding overlay windows")
}
do {
try rebuildOverlayWindows()
} catch {
requestEmergencyExit(message: localizedDescription(from: error))
}
}
private func handleWorkspaceInterruption(message: String) {
guard !isStopping else {
return
}
requestEmergencyExit(message: message)
}
private func handleRightMouseButtonEvent(_ event: RightMouseButtonEvent) {
guard !isStopping else {
return
}
switch event {
case .down:
startRightMouseHoldCountdown()
case .up:
cancelRightMouseHoldCountdown(resetHintText: true)
}
}
private func rebuildOverlayWindows() throws {
guard !NSScreen.screens.isEmpty else {
throw SessionError.missingScreens
}
let level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.screenSaverWindow)))
let windows = try createOverlayWindows(level: level)
guard let focusWindow = windows.first else {
throw SessionError.overlayWindowCreationFailed
}
focusWindow.makeKeyAndOrderFront(nil)
let previousWindows = overlayWindows
overlayWindows = windows
closeOverlayWindows(previousWindows)
}
private func createOverlayWindows(level: NSWindow.Level) throws -> [OverlayWindow] {
var windows: [OverlayWindow] = []
for screen in NSScreen.screens {
let window = OverlayWindow(
contentRect: screen.frame,
styleMask: .borderless,
backing: .buffered,
defer: false,
screen: screen
)
window.delegate = self
window.level = level
window.collectionBehavior = [
.canJoinAllSpaces,
.fullScreenAuxiliary,
.stationary,
.ignoresCycle
]
window.backgroundColor = .black
window.isOpaque = true
window.hasShadow = false
window.ignoresMouseEvents = true
window.isMovable = false
window.isReleasedWhenClosed = false
window.setFrame(screen.frame, display: true)
window.contentView = NSHostingView(
rootView: OverlayWatermarkView(model: overlayHintModel)
)
window.orderFrontRegardless()
windows.append(window)
}
guard !windows.isEmpty else {
throw SessionError.overlayWindowCreationFailed
}
return windows
}
private func enableIdleLockPrevention() throws {
guard idleSleepAssertionID == Self.nullAssertionID else {
return
}
var assertionID = Self.nullAssertionID
let result = IOPMAssertionCreateWithName(
kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"MacTools Physical Clean Mode" as CFString,
&assertionID
)
guard result == kIOReturnSuccess else {
logger.error(
"[\(self.sessionIdentifier, privacy: .public)] failed to create idle-sleep assertion result=\(result, privacy: .public)"
)
throw SessionError.idleLockPreventionFailed
}
idleSleepAssertionID = assertionID
}
private func releaseIdleLockPrevention() {
guard idleSleepAssertionID != Self.nullAssertionID else {
return
}
let assertionID = idleSleepAssertionID
idleSleepAssertionID = Self.nullAssertionID
let result = IOPMAssertionRelease(assertionID)
if result != kIOReturnSuccess {
logger.error(
"[\(self.sessionIdentifier, privacy: .public)] failed to release idle-sleep assertion result=\(result, privacy: .public)"
)
}
}
private func closeOverlayWindows(_ windows: [OverlayWindow]? = nil) {
let windowsToClose = windows ?? overlayWindows
if windows == nil {
overlayWindows.removeAll()
}
for window in windowsToClose {
window.delegate = nil
window.close()
}
}
private func hideCursor() {
guard !cursorHidden else {
return
}
NSCursor.hide()
cursorHidden = true
}
private func showCursorIfNeeded() {
guard cursorHidden else {
return
}
NSCursor.unhide()
cursorHidden = false
}
private func startRightMouseHoldCountdown() {
guard rightMouseHoldTimer == nil else {
return
}
rightMouseHoldRemainingSeconds = Self.rightMouseHoldDurationSeconds
updateOverlayHintText()
let timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
Task { @MainActor in
self?.handleRightMouseHoldTimerTick()
}
}
rightMouseHoldTimer = timer
RunLoop.main.add(timer, forMode: .common)
}
private func cancelRightMouseHoldCountdown(resetHintText: Bool) {
rightMouseHoldTimer?.invalidate()
rightMouseHoldTimer = nil
rightMouseHoldRemainingSeconds = nil
if resetHintText {
updateOverlayHintText()
}
}
private func handleRightMouseHoldTimerTick() {
guard let remainingSeconds = rightMouseHoldRemainingSeconds else {
cancelRightMouseHoldCountdown(resetHintText: false)
return
}
if remainingSeconds > 1 {
rightMouseHoldRemainingSeconds = remainingSeconds - 1
updateOverlayHintText()
return
}
cancelRightMouseHoldCountdown(resetHintText: false)
handleEventTapExit(.rightMouseHold)
}
private func updateOverlayHintText() {
overlayHintModel.keyboardExitHintText = keyboardExitHintText
overlayHintModel.rightMouseExitHintText = rightMouseExitHintText
}
private func tearDown(shouldNotify: Bool, endReason: EndReason) {
guard !isStopping else {
return
}
if case .emergency = endReason {
logger.error("[\(self.sessionIdentifier, privacy: .public)] tearing down session reason=\(String(describing: endReason), privacy: .public)")
} else if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] tearing down session reason=\(String(describing: endReason), privacy: .public)")
}
isStopping = true
cancelRightMouseHoldCountdown(resetHintText: false)
removeObservers()
closeOverlayWindows()
if let eventTap {
CGEvent.tapEnable(tap: eventTap, enable: false)
}
if let eventTapRunLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetMain(), eventTapRunLoopSource, CFRunLoopMode.commonModes)
}
if let eventTap {
CFMachPortInvalidate(eventTap)
}
eventTap = nil
eventTapRunLoopSource = nil
if let eventTapContextPointer {
Unmanaged<EventTapContext>.fromOpaque(eventTapContextPointer).release()
self.eventTapContextPointer = nil
}
showCursorIfNeeded()
releaseIdleLockPrevention()
if let previousPresentationOptions {
NSApp.presentationOptions = previousPresentationOptions
self.previousPresentationOptions = nil
}
isStopping = false
if shouldNotify {
onEnd(endReason)
}
}
private func handleEventTapExit(_ reason: EventTapExitReason) {
switch reason {
case .shortcut:
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] event tap requested exit reason=shortcut")
}
requestStop(reason: .userRequested)
case .rightMouseHold:
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] event tap requested exit reason=rightMouseHold")
}
requestStop(reason: .userRequested)
case .disabledByTimeout:
logger.error("[\(self.sessionIdentifier, privacy: .public)] event tap could not recover from timeout interruption")
requestEmergencyExit(message: localization.string(
"session.error.eventTapTimeoutEmergencyExit",
defaultValue: "输入拦截重启失败,已自动退出清洁模式。"
))
case .disabledByUserInput:
logger.error("[\(self.sessionIdentifier, privacy: .public)] event tap could not recover from user-input interruption")
requestEmergencyExit(message: localization.string(
"session.error.eventTapUserInputEmergencyExit",
defaultValue: "输入拦截被系统停用且无法恢复,已自动退出清洁模式。"
))
}
}
private func handleEventTapInterruption(
_ reason: EventTapInterruptionReason,
didRecover: Bool
) {
if PhysicalCleanModeLog.isVerboseLoggingEnabled {
logger.debug("[\(self.sessionIdentifier, privacy: .public)] event tap interruption reason=\(reason.description, privacy: .public) recovered=\(didRecover, privacy: .public)")
}
guard didRecover else {
return
}
let now = Date()
tapDisableTimestamps.append(now)
tapDisableTimestamps.removeAll { now.timeIntervalSince($0) > 2 }
if tapDisableTimestamps.count >= 3 {
requestEmergencyExit(message: localization.string(
"session.error.eventTapRepeatedlyDisabledEmergencyExit",
defaultValue: "输入拦截被系统连续停用,已自动退出清洁模式。"
))
}
}
private func localizedStartupError(from error: Error) -> Error {
guard let sessionError = error as? SessionError else {
return error
}
return NSError(
domain: "PhysicalCleanModeSession",
code: 1,
userInfo: [
NSLocalizedDescriptionKey: sessionError.localizedDescription(localization: localization)
]
)
}
private func localizedDescription(from error: Error) -> String {
guard let sessionError = error as? SessionError else {
return error.localizedDescription
}
return sessionError.localizedDescription(localization: localization)
}
private nonisolated static let interceptedEventMask: CGEventMask = {
let eventTypes: [CGEventType] = [
.keyDown,
.keyUp,
.flagsChanged,
.mouseMoved,
.leftMouseDown,
.leftMouseUp,
.leftMouseDragged,
.rightMouseDown,
.rightMouseUp,
.rightMouseDragged,
.otherMouseDown,
.otherMouseUp,
.otherMouseDragged,
.scrollWheel
]
return eventTypes.reduce(CGEventMask(0)) { partialResult, eventType in
partialResult | (CGEventMask(1) << UInt64(eventType.rawValue))
}
}()
private static func displayTokens(for binding: ShortcutBinding) -> [String] {
ShortcutFormatter.displayTokens(for: binding)
}
private nonisolated static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
guard let userInfo else {
return Unmanaged.passUnretained(event)
}
let context = Unmanaged<EventTapContext>.fromOpaque(userInfo).takeUnretainedValue()
switch type {
case .tapDisabledByTimeout:
context.recoverTap(.disabledByTimeout)
return Unmanaged.passUnretained(event)
case .tapDisabledByUserInput:
context.recoverTap(.disabledByUserInput)
return Unmanaged.passUnretained(event)
case .rightMouseDown:
context.notifyRightMouseButtonEvent(.down)
return nil
case .rightMouseUp:
context.notifyRightMouseButtonEvent(.up)
return nil
case .keyDown:
let keyCode = UInt16(event.getIntegerValueField(.keyboardEventKeycode))
let modifiers = ShortcutModifiers.from(event.flags)
if keyCode == context.exitBinding.keyCode, modifiers == context.exitBinding.modifiers {
context.requestExit(.shortcut)
}
return nil
case .keyUp,
.flagsChanged,
.mouseMoved,
.leftMouseDown,
.leftMouseUp,
.leftMouseDragged,
.rightMouseDragged,
.otherMouseDown,
.otherMouseUp,
.otherMouseDragged,
.scrollWheel:
return nil
default:
return nil
}
}
}