# MiddleDrag MiddleDrag is a native macOS application that provides three-finger trackpad gestures for middle-click and middle-drag functionality. It solves the problem of Mac trackpads lacking a middle mouse button, which is essential for panning in design tools (Figma, Photoshop), navigating 3D software (Blender, CAD applications), opening browser tabs in the background, and other workflows that rely on middle-mouse input. The application works by using Apple's private MultitouchSupport framework to intercept raw touch data before the system gesture recognizer processes it. This allows three-finger gestures to generate synthetic middle-mouse events while leaving Mission Control and other system gestures intact. The architecture consists of a core layer for touch processing and mouse event synthesis, a manager layer for coordinating business logic, and a UI layer for the menu bar interface. ## Core Components ### MultitouchManager - Main Coordinator The `MultitouchManager` is the central coordinator that manages multitouch monitoring, gesture recognition, and event suppression. It provides the main interface for starting/stopping gesture recognition and handling configuration updates. ```swift import Foundation // Access the shared MultitouchManager instance let manager = MultitouchManager.shared // Start monitoring for three-finger gestures manager.start() // Configure gesture settings var config = GestureConfiguration() config.sensitivity = 1.5 // Drag sensitivity (0.5 - 2.0) config.smoothingFactor = 0.3 // Movement smoothing (0.0 - 1.0) config.tapThreshold = 0.15 // Max tap duration in seconds config.middleDragEnabled = true // Enable middle-drag gestures config.tapToClickEnabled = true // Enable tap-to-click gestures manager.updateConfiguration(config) // Toggle enabled state (useful for menu bar toggle) manager.toggleEnabled() // Check current state print("Monitoring: \(manager.isMonitoring)") print("Enabled: \(manager.isEnabled)") // Restart after sleep/wake manager.restart() // Stop monitoring completely manager.stop() ``` ### GestureConfiguration - Settings Structure `GestureConfiguration` defines all adjustable parameters for gesture detection and mouse behavior, including sensitivity, timing thresholds, palm rejection options, and feature toggles. ```swift import Foundation // Create a configuration with custom settings var config = GestureConfiguration() // Basic gesture settings config.sensitivity = 1.0 // Drag speed multiplier (0.5 - 2.0) config.smoothingFactor = 0.3 // EMA smoothing (0 = none, 1 = max) config.tapThreshold = 0.15 // Max seconds for tap recognition config.maxTapHoldDuration = 0.5 // Max hold time before tap cancels config.moveThreshold = 0.015 // Movement threshold for tap vs drag // Feature toggles config.middleDragEnabled = true // Allow middle-drag gestures config.tapToClickEnabled = true // Allow tap-to-click gestures // Palm rejection - Exclusion zone (bottom of trackpad) config.exclusionZoneEnabled = true config.exclusionZoneSize = 0.15 // Bottom 15% of trackpad // Palm rejection - Modifier key requirement config.requireModifierKey = true config.modifierKeyType = .shift // .shift, .control, .option, .command // Palm rejection - Contact size filter config.contactSizeFilterEnabled = true config.maxContactSize = 1.5 // Skip touches larger than this // Window filters config.minimumWindowSizeFilterEnabled = true config.minimumWindowWidth = 100 // Ignore windows smaller than 100px config.minimumWindowHeight = 100 config.ignoreDesktop = true // Skip gestures over desktop // Title bar passthrough (for native window dragging) config.passThroughTitleBar = true config.titleBarHeight = 28 // Title bar height in pixels // Relift during drag config.allowReliftDuringDrag = true // Continue drag with 2 fingers // Apply configuration to manager MultitouchManager.shared.updateConfiguration(config) ``` ### GestureRecognizer - Touch Processing The `GestureRecognizer` class processes raw touch data from the multitouch device and detects three-finger tap and drag gestures. It uses a state machine to track gesture progression. ```swift import Foundation // Create a gesture recognizer with custom configuration let recognizer = GestureRecognizer() recognizer.configuration = GestureConfiguration() // Implement the delegate protocol to receive gesture events class MyGestureHandler: GestureRecognizerDelegate { func gestureRecognizerDidStart(_ recognizer: GestureRecognizer, at position: MTPoint) { print("Gesture started at: (\(position.x), \(position.y))") } func gestureRecognizerDidTap(_ recognizer: GestureRecognizer) { print("Three-finger tap detected!") // Perform middle-click action } func gestureRecognizerDidBeginDragging(_ recognizer: GestureRecognizer) { print("Drag gesture started") // Begin middle-drag operation } func gestureRecognizerDidUpdateDragging(_ recognizer: GestureRecognizer, with data: GestureData) { // Calculate movement delta let delta = data.frameDelta(from: recognizer.configuration) print("Drag delta: (\(delta.x), \(delta.y))") } func gestureRecognizerDidEndDragging(_ recognizer: GestureRecognizer) { print("Drag gesture ended") } func gestureRecognizerDidCancel(_ recognizer: GestureRecognizer) { print("Gesture cancelled (e.g., from possibleTap state)") } func gestureRecognizerDidCancelDragging(_ recognizer: GestureRecognizer) { print("Drag cancelled (e.g., 4th finger added for Mission Control)") } } // Set up the delegate let handler = MyGestureHandler() recognizer.delegate = handler // Process touch data (called from DeviceMonitor callback) // touches: UnsafeMutableRawPointer to MTTouch array // count: number of touches // timestamp: frame timestamp // modifierFlags: current modifier key state recognizer.processTouches(touches, count: touchCount, timestamp: timestamp, modifierFlags: modifierFlags) // Reset state manually if needed recognizer.reset() ``` ### MouseEventGenerator - Event Synthesis The `MouseEventGenerator` class creates and posts synthetic middle-mouse events using Core Graphics APIs. It handles click events, drag operations with cursor association, and automatic stuck-drag detection. ```swift import Foundation import CoreGraphics // Create a mouse event generator let mouseGenerator = MouseEventGenerator() // Configure smoothing and movement settings mouseGenerator.smoothingFactor = 0.3 // EMA smoothing (0-1) mouseGenerator.minimumMovementThreshold = 0.5 // Min pixels to register movement mouseGenerator.stuckDragTimeout = 10.0 // Auto-release after 10s inactivity // Perform a middle-click at current cursor position mouseGenerator.performClick() // Start a middle-drag operation // The cursor will be disassociated from mouse movement for smooth dragging let startPosition = MouseEventGenerator.currentMouseLocation mouseGenerator.startDrag(at: startPosition) // Update drag with delta movement (called each frame) // deltaX/deltaY are in pixels mouseGenerator.updateDrag(deltaX: 10.0, deltaY: -5.0) // End the drag normally mouseGenerator.endDrag() // Cancel an active drag (e.g., user added 4th finger) mouseGenerator.cancelDrag() // Force release a stuck middle-drag state // Use when system state may be out of sync with tracking mouseGenerator.forceMiddleMouseUp() // Get current mouse location in Quartz coordinates let currentPos = MouseEventGenerator.currentMouseLocation print("Cursor at: (\(currentPos.x), \(currentPos.y))") ``` ### PreferencesManager - Settings Persistence `PreferencesManager` handles loading and saving user preferences to UserDefaults. It provides a thread-safe interface for managing all configuration options. ```swift import Foundation // Access the shared PreferencesManager let prefsManager = PreferencesManager.shared // Load current preferences var prefs = prefsManager.loadPreferences() // Modify preferences prefs.launchAtLogin = true prefs.dragSensitivity = 1.2 prefs.tapThreshold = 0.15 prefs.smoothingFactor = 0.3 prefs.middleDragEnabled = true prefs.tapToClickEnabled = true // Palm rejection settings prefs.exclusionZoneEnabled = false prefs.exclusionZoneSize = 0.15 prefs.requireModifierKey = false prefs.modifierKeyType = .shift prefs.contactSizeFilterEnabled = false prefs.maxContactSize = 1.5 // Window filter settings prefs.minimumWindowSizeFilterEnabled = false prefs.minimumWindowWidth = 100 prefs.minimumWindowHeight = 100 prefs.ignoreDesktop = false // Title bar passthrough prefs.passThroughTitleBar = false prefs.titleBarHeight = 28 // Hotkey bindings (Carbon key codes and modifiers) prefs.toggleHotKey = HotKeyBinding( keyCode: UInt32(kVK_ANSI_E), // 'E' key carbonModifiers: UInt32(cmdKey | optionKey) // Cmd+Option ) // Save preferences prefsManager.savePreferences(prefs) // Convert preferences to GestureConfiguration for use with MultitouchManager let gestureConfig = prefs.gestureConfig MultitouchManager.shared.updateConfiguration(gestureConfig) // Track one-time prompts if !prefsManager.hasShownGestureConfigurationPrompt { // Show first-run configuration dialog prefsManager.markGestureConfigurationPromptShown() } ``` ### DeviceMonitor - Trackpad Monitoring `DeviceMonitor` interfaces with Apple's private MultitouchSupport framework to receive raw touch data from trackpads. It manages device lifecycle and callback registration. ```swift import Foundation // Create a device monitor let deviceMonitor = DeviceMonitor() // Implement the delegate protocol class MyDeviceHandler: DeviceMonitorDelegate { func deviceMonitor( _ monitor: DeviceMonitor, didReceiveTouches touches: UnsafeMutableRawPointer, count: Int32, timestamp: Double ) { // Access raw touch data let touchArray = touches.bindMemory(to: MTTouch.self, capacity: Int(count)) for i in 0..