Try Live
Add Docs
Rankings
Pricing
Docs
Install
Theme
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
MiddleDrag
https://github.com/nullpointerdepressivedisorder/middledrag
Admin
MiddleDrag is a native macOS application that adds three-finger trackpad gestures for middle-click
...
Tokens:
7,166
Snippets:
74
Trust Score:
7.8
Update:
3 weeks ago
Context
Skills
Chat
Benchmark
60.7
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# 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..<Int(count) { let touch = touchArray[i] let position = touch.normalizedVector.position let state = TouchState(rawValue: touch.state) // Only process active touches (state 3 = touching, state 4 = active) if touch.state == 3 || touch.state == 4 { print("Finger \(touch.fingerID): (\(position.x), \(position.y))") } } } } // Set up delegate and start monitoring let handler = MyDeviceHandler() deviceMonitor.delegate = handler // Start monitoring (returns false if no trackpad found) if deviceMonitor.start() { print("Device monitoring started") } else { print("No multitouch device found") } // Stop monitoring when done deviceMonitor.stop() ``` ### Touch Data Models The touch data structures represent raw touch information from the MultitouchSupport framework, including position, velocity, pressure, and touch state. ```swift import Foundation // MTPoint - 2D point with utility methods let point1 = MTPoint(x: 0.5, y: 0.5) let point2 = MTPoint(x: 0.7, y: 0.3) // Calculate distance between points let distance = point1.distance(to: point2) print("Distance: \(distance)") // Calculate midpoint let mid = point1.midpoint(with: point2) print("Midpoint: (\(mid.x), \(mid.y))") // MTVector - position and velocity let vector = MTVector( position: MTPoint(x: 0.5, y: 0.5), velocity: MTPoint(x: 0.1, y: -0.05) ) // MTTouch - raw touch data from framework // (Received from DeviceMonitor callback, not constructed manually) // Key fields: // - fingerID: Int32 - unique finger identifier // - state: UInt32 - touch state (see TouchState enum) // - normalizedVector: MTVector - position/velocity in 0-1 coordinates // - zTotal: Float - contact pressure/size (used for palm rejection) // - angle: Float - touch angle // - majorAxis/minorAxis: Float - contact ellipse dimensions // TouchState enum values // .notTracking (0) - finger not being tracked // .starting (1) - touch beginning // .hovering (2) - finger hovering // .touching (3) - finger just made contact // .active (4) - finger actively touching // .lifting (5) - finger lifting off // .lingering (6) - brief state after lift // .outOfRange (7) - finger gone // Check if a touch state should be tracked let state = TouchState.active print("Is touching: \(state.isTouching)") // true print("Should track: \(state.shouldTrack)") // true // TrackedFinger - enhanced finger tracking let finger = TrackedFinger( id: 1, position: MTPoint(x: 0.5, y: 0.5), velocity: MTPoint(x: 0.0, y: 0.0), pressure: 0.8, timestamp: CACurrentMediaTime(), state: 4 // active ) print("Finger active: \(finger.isActive)") ``` ### UserPreferences and HotKeyBinding `UserPreferences` is a Codable structure for persistent settings, and `HotKeyBinding` represents keyboard shortcut configurations. ```swift import Foundation import Carbon.HIToolbox // Create default user preferences var prefs = UserPreferences() // All preference fields with their defaults: prefs.launchAtLogin = false prefs.dragSensitivity = 1.0 // 0.5 - 2.0 prefs.tapThreshold = 0.15 // seconds prefs.maxTapHoldDuration = 0.5 // seconds prefs.smoothingFactor = 0.3 // 0.0 - 1.0 prefs.middleDragEnabled = true prefs.tapToClickEnabled = true prefs.exclusionZoneEnabled = false prefs.exclusionZoneSize = 0.15 // 0.0 - 1.0 (bottom portion) prefs.requireModifierKey = false prefs.modifierKeyType = .shift prefs.contactSizeFilterEnabled = false prefs.maxContactSize = 1.5 prefs.minimumWindowSizeFilterEnabled = false prefs.minimumWindowWidth = 100.0 prefs.minimumWindowHeight = 100.0 prefs.ignoreDesktop = false prefs.passThroughTitleBar = false prefs.titleBarHeight = 28.0 prefs.allowReliftDuringDrag = false // Configure hotkey bindings // Toggle gesture recognition (default: Cmd+Shift+E) prefs.toggleHotKey = HotKeyBinding( keyCode: UInt32(kVK_ANSI_E), carbonModifiers: UInt32(cmdKey) | UInt32(shiftKey) ) // Open menu bar (default: Cmd+Shift+M) prefs.menuBarHotKey = HotKeyBinding( keyCode: UInt32(kVK_ANSI_M), carbonModifiers: UInt32(cmdKey) | UInt32(shiftKey) ) // Get human-readable hotkey string print("Toggle hotkey: \(prefs.toggleHotKey.displayString)") // Output: "⌘ Command⇧ ShiftE" // Convert UserPreferences to GestureConfiguration let gestureConfig = prefs.gestureConfig MultitouchManager.shared.updateConfiguration(gestureConfig) // Encode/decode for persistence let encoder = JSONEncoder() let data = try encoder.encode(prefs) let decoder = JSONDecoder() let loadedPrefs = try decoder.decode(UserPreferences.self, from: data) ``` ### MultitouchFramework - Private API Bindings `MultitouchFramework` provides Swift bindings to Apple's private MultitouchSupport framework for accessing raw trackpad data. ```swift import Foundation // Access the shared framework helper let framework = MultitouchFramework.shared // Check if multitouch hardware is available if framework.isAvailable { print("Multitouch framework available") } else { print("No multitouch hardware detected") } // Get the default multitouch device (built-in trackpad) if let device = framework.getDefaultDevice() { print("Got default device: \(device)") // Low-level API usage (typically handled by DeviceMonitor): // MTDeviceStart(device, 0) // Start the device // MTDeviceStop(device) // Stop the device // MTDeviceIsRunning(device) // Check if running // Register callback (use DeviceMonitor for proper lifecycle management) // MTRegisterContactFrameCallback(device, callback) // MTUnregisterContactFrameCallback(device, callback) } // Get list of all multitouch devices if let deviceList = MTDeviceCreateList() { let count = CFArrayGetCount(deviceList) print("Found \(count) multitouch device(s)") } ``` ### Notification Names for State Changes MiddleDrag posts notifications for important state changes that UI components can observe. ```swift import Foundation // Notification posted when device polling times out // (no trackpad found within 5 minutes) NotificationCenter.default.addObserver( forName: .middleDragPollingTimedOut, object: nil, queue: .main ) { _ in print("Device polling timed out - no trackpad found") // Update UI to show disabled state } // Notification posted when a device connects during polling NotificationCenter.default.addObserver( forName: .middleDragDeviceConnected, object: nil, queue: .main ) { _ in print("Trackpad connected!") // Update UI to show enabled state } ``` ## Summary MiddleDrag is designed for users who need middle-mouse functionality on Mac trackpads, particularly professionals working with design tools, 3D/CAD software, and development environments. The main use cases include panning canvas views in applications like Figma, Photoshop, and Blender; opening browser links in background tabs; closing editor tabs in IDEs; and navigating large documents without modifier keys. The application integrates seamlessly with macOS through its menu bar interface and respects system gestures like Mission Control. Key integration patterns include using `MultitouchManager.shared` as the central coordinator, configuring behavior through `GestureConfiguration`, persisting settings via `PreferencesManager`, and observing notifications for state changes. The modular architecture with clear separation between touch processing (`GestureRecognizer`), event synthesis (`MouseEventGenerator`), device monitoring (`DeviceMonitor`), and UI management (`MenuBarController`) makes the codebase maintainable and testable.