├── .gitignore ├── FULL APP Trackpad Mapper.zip ├── Resources ├── trackpad_mapper.icns └── trackpad_status_icon.icns ├── src ├── PreferenceViewController.swift ├── main.swift ├── PreferenceUIView.swift ├── Settings.swift ├── MainMenu.swift ├── MultitouchSupport.h └── trackpad_mapper_util.c ├── Info.plist ├── LICENSE.txt ├── settings.def.h ├── makefile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.svg 3 | icons_script* 4 | *.png 5 | settings.h 6 | .DS_Store 7 | build/ 8 | -------------------------------------------------------------------------------- /FULL APP Trackpad Mapper.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lokxii/Mac-trackpad-mapper/HEAD/FULL APP Trackpad Mapper.zip -------------------------------------------------------------------------------- /Resources/trackpad_mapper.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lokxii/Mac-trackpad-mapper/HEAD/Resources/trackpad_mapper.icns -------------------------------------------------------------------------------- /Resources/trackpad_status_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lokxii/Mac-trackpad-mapper/HEAD/Resources/trackpad_status_icon.icns -------------------------------------------------------------------------------- /src/PreferenceViewController.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | 4 | class PreferenceViewController: NSViewController { 5 | var mainMenu: MainMenu? = nil 6 | 7 | required init(coder: NSCoder) { 8 | super.init(coder: coder)! 9 | } 10 | 11 | init(mainMenu: MainMenu) { 12 | super.init(nibName: nil, bundle: nil) 13 | self.mainMenu = mainMenu 14 | } 15 | 16 | override func loadView() { 17 | title = "Preference" 18 | view = NSView(frame: NSMakeRect(0.0, 0.0, 300, 300)) 19 | 20 | let ui = NSHostingView(rootView: PreferenceUIView(mainMenu: mainMenu!)) 21 | ui.translatesAutoresizingMaskIntoConstraints = false 22 | 23 | self.view.addSubview(ui) 24 | 25 | NSLayoutConstraint.activate([ 26 | ui.centerXAnchor.constraint(equalTo: view.centerXAnchor), 27 | ui.centerYAnchor.constraint(equalTo: view.centerYAnchor), 28 | ]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | trackpad_mapper 9 | CFBundleIdentifier 10 | com.trackpad-mapper 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Trackpad Mapper 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 0.0.1 19 | CFBundleVersion 20 | 0.0.1 21 | CFBundleIconFile 22 | trackpad_mapper 23 | LSApplicationCategoryType 24 | public.app-category.utilities 25 | LSUIElement 26 | 27 | NSPrincipalClass 28 | NSApplication 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Austin Siu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | import SwiftUI 4 | import ApplicationServices 5 | 6 | func alert(msg: String = "") { 7 | let alert = NSAlert.init() 8 | alert.messageText = msg 9 | alert.addButton(withTitle: "OK") 10 | alert.runModal() 11 | } 12 | 13 | var settings: Settings = Settings() 14 | 15 | func main() { 16 | let _ = NSApplication.shared 17 | NSApp.setActivationPolicy(.regular) 18 | NSApp.activate(ignoringOtherApps: true) 19 | 20 | // Check accessibility 21 | let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString 22 | let options = [checkOptPrompt: true] as CFDictionary? 23 | 24 | if (!AXIsProcessTrustedWithOptions(options)) { 25 | return 26 | } 27 | 28 | // create status bar item 29 | let statusItem = NSStatusBar.system.statusItem( 30 | withLength: NSStatusItem.variableLength) 31 | statusItem.button?.image = Bundle.main.image(forResource: "trackpad_status_icon")! 32 | statusItem.button?.image!.isTemplate = true 33 | 34 | // building menu 35 | let menu = MainMenu() 36 | 37 | // Hook menu to status bar item 38 | statusItem.menu = menu 39 | 40 | // Atart app 41 | NSApp.run() 42 | } 43 | 44 | main() 45 | -------------------------------------------------------------------------------- /settings.def.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Do not remove this. This variable is initialized in main program 4 | extern CGSize screenSize; 5 | // Emit mouse event or wrap cursor. Default is warp cursor 6 | bool emitMouseEvent = false; 7 | 8 | // Helper function: lower and upper has to be without 0 to 1 inclusive 9 | static inline double rangeRatio(double n, double lower, double upper) { 10 | if (n < lower || n > upper) { 11 | return -1; 12 | } 13 | return (n - lower) / (upper - lower); 14 | } 15 | 16 | // Compulsory: Modify this function to change how relative position of trackpad is mapped to normalized screen coordinates. Return negative number for invalid finger position 17 | static inline MTPoint map(double normx, double normy) { 18 | // whole trackpad to whole screen 19 | MTPoint point = { 20 | .x = normx, 21 | .y = normy, 22 | }; 23 | //scaling the points up to the screen size 24 | point.x *= screenSize.width; 25 | point.y *= screenSize.height; 26 | return point; 27 | } 28 | 29 | 30 | // Jitter / smoothing settings for absolute cursor mode 31 | static const double JITTER_THRESHOLD = 4.0; // in screen pixels (try 4–10) 32 | 33 | // Smoothing factor for cursor motion (0..1). 34 | // If set to 0, it will be clamped to 0.1 in trackpad_mapper_util.c to avoid the cursor getting stuck. 35 | static const double JITTER_ALPHA = 0.6; 36 | 37 | // Disable custom cursor movement whenever more than one finger is on the pad 38 | static const bool DISABLE_CURSOR_ON_MULTITOUCH = true; 39 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | LIBS=-F/System/Library/PrivateFrameworks -framework MultitouchSupport \ 2 | -framework CoreFoundation \ 3 | -framework Carbon 4 | 5 | BUILD_DIR=build 6 | SOURCE_DIR=src 7 | UTIL=trackpad_mapper_util 8 | TARGET=trackpad_mapper 9 | APP="Trackpad Mapper.app" 10 | 11 | all: 12 | if [[ ! -e ${BUILD_DIR} ]]; then mkdir -p ${BUILD_DIR}/bin; fi 13 | make util 14 | make app 15 | make bundle 16 | 17 | release: 18 | if [[ ! -e ${BUILD_DIR} ]]; then mkdir -p ${BUILD_DIR}/bin; fi 19 | make util_release 20 | make app_release 21 | make bundle 22 | 23 | settings.h: 24 | @if [[ ! -e settings.h ]]; then cp settings.def.h settings.h; fi 25 | 26 | util: 27 | make settings.h 28 | gcc ${LIBS} ${SOURCE_DIR}/${UTIL}.c -o ${BUILD_DIR}/bin/${UTIL} -g 29 | 30 | util_release: 31 | make settings.h 32 | gcc ${LIBS} ${SOURCE_DIR}/${UTIL}.c -o ${BUILD_DIR}/bin/${UTIL} -O3 33 | 34 | app: 35 | swiftc ${SOURCE_DIR}/*.swift -o ${BUILD_DIR}/bin/${TARGET} -g 36 | 37 | app_release: 38 | swiftc ${SOURCE_DIR}/*.swift -o ${BUILD_DIR}/bin/${TARGET} -O 39 | 40 | bundle: 41 | @if [[ ! -e ${BUILD_DIR}/${APP} ]]; then \ 42 | echo Creating app bundle; \ 43 | mkdir -p ${BUILD_DIR}/${APP}/Contents/MacOS; \ 44 | echo done; \ 45 | fi 46 | @cp -R Resources ${BUILD_DIR}/${APP}/Contents/Resources 47 | @cp Info.plist ${BUILD_DIR}/${APP}/Contents 48 | @cp ${BUILD_DIR}/bin/${UTIL} ${BUILD_DIR}/${APP}/Contents/MacOS 49 | @cp ${BUILD_DIR}/bin/${TARGET} ${BUILD_DIR}/${APP}/Contents/MacOS 50 | 51 | install: 52 | cp -R ${BUILD_DIR}/${APP} /Applications 53 | 54 | install_util_update: 55 | cp ${BUILD_DIR}/bin/${UTIL} /Applications/${APP}/Contents/MacOS 56 | -------------------------------------------------------------------------------- /src/PreferenceUIView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PreferenceUIView: View { 4 | @State private var useHeader: Bool = false 5 | @State private var trackpadRange: String = "0,0,1,1" 6 | @State private var screenRange: String = "0,0,1,1" 7 | @State private var emitMouseEvent: Bool = false 8 | 9 | var mainMenu: MainMenu 10 | 11 | var isValid: Bool { 12 | return Settings.Range.stringIsValid(string: trackpadRange, 13 | name: "Trackpad region") && 14 | Settings.Range.stringIsValid(string: screenRange, 15 | name: "Screen region") 16 | } 17 | 18 | var body: some View { 19 | VStack (alignment: .leading) { 20 | Toggle("Use settings in header file (settings.h)", isOn: $useHeader) 21 | .toggleStyle(.checkbox) 22 | if (!useHeader) { 23 | Form { 24 | TextField("Trackpad region:", text: $trackpadRange) 25 | TextField("Screen region:", text: $screenRange) 26 | } 27 | Toggle("Emit mouse events", isOn: $emitMouseEvent) 28 | .toggleStyle(.checkbox) 29 | } 30 | } 31 | Button (action: { 32 | if isValid && !useHeader { 33 | settings.trackpadRange = Settings.Range(from: trackpadRange) 34 | settings.screenRange = Settings.Range(from: screenRange) 35 | } 36 | settings.emitMouseEvent = emitMouseEvent 37 | settings.useHeader = useHeader 38 | 39 | if mainMenu.process != nil { 40 | mainMenu.stopProcess(nil) 41 | mainMenu.startProcess(nil) 42 | } 43 | }) { 44 | Text("Apply").padding() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Settings.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | struct Settings: Codable { 4 | struct Range: Codable { 5 | var low: NSPoint 6 | var up: NSPoint 7 | 8 | init(low: NSPoint, up: NSPoint) { 9 | self.low = low 10 | self.up = up 11 | } 12 | 13 | init?(from string: String) { 14 | if Settings.Range.stringIsValid(string: string) { 15 | let compoenents = string.components(separatedBy: ",") 16 | .map { CGFloat(Float($0)!) } 17 | low = NSPoint(x: compoenents[0], y: compoenents[1]) 18 | up = NSPoint(x: compoenents[2], y: compoenents[3]) 19 | } else { 20 | return nil 21 | } 22 | } 23 | 24 | func toString() -> String { 25 | return "\(low.x),\(low.y),\(up.x),\(up.y)" 26 | } 27 | 28 | static func stringIsValid(string: String, name: String = "") -> Bool { 29 | let components = string.components(separatedBy: ",") 30 | .map({ (s: String) -> Bool in 31 | let f = Float(s) 32 | return f != nil && (0.0...1.0).contains(f!) 33 | }) 34 | let isValid = components.count == 4 && 35 | components.reduce(true) { $0 && $1 } 36 | if !isValid && name != "" { 37 | alert(msg: name + " range not valid") 38 | } 39 | return isValid 40 | } 41 | } 42 | 43 | var useHeader: Bool = false 44 | var trackpadRange: Range? = nil 45 | var screenRange: Range? = nil 46 | var emitMouseEvent: Bool = false 47 | 48 | init(trackpad: Range? = nil, screen: Range? = nil) { 49 | trackpadRange = trackpad 50 | screenRange = screen 51 | } 52 | 53 | func toArgs() -> [String] { 54 | var args: [String] = [] 55 | if useHeader { 56 | if let trackpadRange = trackpadRange { 57 | args.append("-i") 58 | args.append(trackpadRange.toString()) 59 | } 60 | if let screenRange = screenRange { 61 | args.append("-o") 62 | args.append(screenRange.toString()) 63 | } 64 | if emitMouseEvent { 65 | args.append("-e") 66 | } 67 | } 68 | return args 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MainMenu.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class MainMenu: NSMenu { 4 | var process: Process? = nil 5 | var versionItem: NSMenuItem? = nil 6 | var startItem: NSMenuItem? = nil 7 | var stopItem: NSMenuItem? = nil 8 | var preferenceItem: NSMenuItem? = nil 9 | var quitItem: NSMenuItem? = nil 10 | 11 | let toggleTrackingItemIndex = 2 12 | let preferenceItemIndex = 3 13 | 14 | var preferenceWindow: NSWindow? = nil 15 | 16 | required init(coder: NSCoder) { 17 | super.init(coder: coder) 18 | preferenceWindow = NSWindow( 19 | contentViewController: PreferenceViewController(mainMenu: self)) 20 | } 21 | 22 | public init() { 23 | super.init(title: "") 24 | 25 | // Version 26 | versionItem = NSMenuItem( 27 | title: "Version 0.0.1", 28 | action: nil, 29 | keyEquivalent: "") 30 | 31 | // Start process 32 | startItem = NSMenuItem( 33 | title: "Start absolute tracking", 34 | action: #selector(MainMenu.startProcess(_:)), 35 | keyEquivalent: "s") 36 | startItem!.target = self 37 | 38 | // Stop process 39 | stopItem = NSMenuItem( 40 | title: "Stop absolute tracking", 41 | action: #selector(MainMenu.stopProcess(_:)), 42 | keyEquivalent: "s") 43 | stopItem!.target = self 44 | 45 | // Preference 46 | preferenceItem = NSMenuItem( 47 | title: "Preference", 48 | action: #selector(MainMenu.openPreference(_:)), 49 | keyEquivalent: ",") 50 | preferenceItem!.target = self 51 | 52 | // Quit app 53 | quitItem = NSMenuItem( 54 | title: "Quit trackpad mapper", 55 | action: #selector(MainMenu.terminate(_:)), 56 | keyEquivalent: "q") 57 | quitItem!.target = self 58 | 59 | addItem(versionItem!) 60 | addItem(NSMenuItem.separator()) 61 | addItem(startItem!) 62 | addItem(preferenceItem!) 63 | addItem(NSMenuItem.separator()) 64 | addItem(quitItem!) 65 | 66 | preferenceWindow = NSWindow( 67 | contentViewController: PreferenceViewController(mainMenu: self)) 68 | } 69 | 70 | @objc 71 | public func startProcess(_: Any?) { 72 | if process == nil { 73 | var processUrl = Bundle.main.bundleURL 74 | processUrl.appendPathComponent("Contents/MacOS/trackpad_mapper_util") 75 | do { 76 | process = try Process.run( 77 | processUrl, 78 | arguments: settings.toArgs(), 79 | terminationHandler: nil) 80 | 81 | items[toggleTrackingItemIndex] = stopItem! 82 | } catch { 83 | alert(msg: "Cannot spawn process") 84 | } 85 | } 86 | } 87 | 88 | @objc 89 | public func stopProcess(_: Any?) { 90 | if let process = process { 91 | process.terminate() 92 | self.process = nil 93 | 94 | items[toggleTrackingItemIndex] = startItem! 95 | } 96 | } 97 | 98 | @objc 99 | public func terminate(_: Any?) { 100 | if let process = process { 101 | process.terminate() 102 | } 103 | NSApp.terminate(nil) 104 | } 105 | 106 | @objc 107 | public func openPreference(_: Any?) { 108 | preferenceWindow!.makeKeyAndOrderFront(nil) 109 | preferenceWindow!.orderedIndex = 0 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/MultitouchSupport.h: -------------------------------------------------------------------------------- 1 | /* 2 | * MultitouchSupport.h 3 | * TouchSynthesis 4 | * 5 | * Created by Nathan Vander Wilt on 1/13/10. 6 | * Copyright 2010 Calf Trail Software, LLC. All rights reserved. 7 | * 8 | */ 9 | 10 | #include 11 | 12 | typedef struct { 13 | float x; 14 | float y; 15 | } MTPoint; 16 | 17 | typedef struct { 18 | MTPoint position; 19 | MTPoint velocity; 20 | } MTVector; 21 | 22 | enum { 23 | MTTouchStateNotTracking = 0, 24 | MTTouchStateStartInRange = 1, 25 | MTTouchStateHoverInRange = 2, 26 | MTTouchStateMakeTouch = 3, 27 | MTTouchStateTouching = 4, 28 | MTTouchStateBreakTouch = 5, 29 | MTTouchStateLingerInRange = 6, 30 | MTTouchStateOutOfRange = 7 31 | }; 32 | typedef uint32_t MTTouchState; 33 | 34 | typedef struct { 35 | int32_t frame; 36 | double timestamp; 37 | int32_t pathIndex; // "P" (~transducerIndex) 38 | MTTouchState state; 39 | int32_t fingerID; // "F" (~identity) 40 | int32_t handID; // "H" (always 1) 41 | MTVector normalizedVector; 42 | float zTotal; // "ZTot" (~quality, multiple of 1/8 between 0 and 1) 43 | float pressure; // Always whole number 44 | float angle; 45 | float majorAxis; 46 | float minorAxis; 47 | MTVector absoluteVector; // "mm" 48 | int32_t field14; 49 | int32_t field15; 50 | float zDensity; // "ZDen" (~density) 51 | } MTTouch; 52 | 53 | //typedef const void* MTDeviceRef; 54 | typedef CFTypeRef MTDeviceRef; 55 | 56 | double MTAbsoluteTimeGetCurrent(); 57 | bool MTDeviceIsAvailable(); // true if can create default device 58 | 59 | CFArrayRef MTDeviceCreateList(); // creates for driver types 0, 1, 4, 2, 3 60 | MTDeviceRef MTDeviceCreateDefault(); 61 | MTDeviceRef MTDeviceCreateFromDeviceID(int64_t); 62 | // MTDeviceRef MTDeviceCreateFromService(io_service_t); 63 | MTDeviceRef MTDeviceCreateFromGUID(uuid_t); // GUID's compared by pointer, not value! 64 | void MTDeviceRelease(MTDeviceRef); 65 | 66 | CFRunLoopSourceRef MTDeviceCreateMultitouchRunLoopSource(MTDeviceRef); 67 | OSStatus MTDeviceScheduleOnRunLoop(MTDeviceRef, CFRunLoopRef, CFStringRef); 68 | 69 | OSStatus MTDeviceStart(MTDeviceRef, int); 70 | OSStatus MTDeviceStop(MTDeviceRef); 71 | bool MTDeviceIsRunning(MTDeviceRef); 72 | 73 | 74 | bool MTDeviceIsValid(MTDeviceRef); 75 | bool MTDeviceIsBuiltIn(MTDeviceRef) __attribute__ ((weak_import)); // no 10.5 76 | bool MTDeviceIsOpaqueSurface(MTDeviceRef); 77 | // io_service_t MTDeviceGetService(MTDeviceRef); 78 | OSStatus MTDeviceGetSensorSurfaceDimensions(MTDeviceRef, int*, int*); 79 | OSStatus MTDeviceGetFamilyID(MTDeviceRef, int*); 80 | OSStatus MTDeviceGetDeviceID(MTDeviceRef, uint64_t*) __attribute__ ((weak_import)); // no 10.5 81 | OSStatus MTDeviceGetDriverType(MTDeviceRef, int*); 82 | OSStatus MTDeviceGetActualType(MTDeviceRef, int*); 83 | OSStatus MTDeviceGetGUID(MTDeviceRef, uuid_t*); 84 | 85 | typedef void (*MTFrameCallbackFunction)(MTDeviceRef device, 86 | MTTouch touches[], size_t numTouches, 87 | double timestamp, size_t frame); 88 | void MTRegisterContactFrameCallback(MTDeviceRef, MTFrameCallbackFunction); 89 | 90 | typedef void (*MTFrameCallbackRefconFunction)(MTDeviceRef device, 91 | MTTouch touches[], size_t numTouches, 92 | double timestamp, size_t frame, void* refcon); 93 | void MTRegisterContactFrameCallbackWithRefcon(MTDeviceRef, MTFrameCallbackRefconFunction, void* refcon); 94 | void MTUnregisterContactFrameCallback(MTDeviceRef, MTFrameCallbackRefconFunction); 95 | 96 | 97 | typedef void (*MTPathCallbackFunction)(MTDeviceRef device, long pathID, long state, MTTouch* touch); 98 | MTPathCallbackFunction MTPathPrintCallback; 99 | void MTRegisterPathCallback(MTDeviceRef, MTPathCallbackFunction); 100 | 101 | /* 102 | // callbacks never called (need different flags?) 103 | typedef void (*MTImageCallbackFunction)(MTDeviceRef, void*, void*); 104 | MTImageCallbackFunction MTImagePrintCallback; 105 | void MTRegisterMultitouchImageCallback(MTDeviceRef, MTImageCallbackFunction); 106 | */ 107 | 108 | /* 109 | // these log error 110 | void MTVibratorRunForDuration(MTDeviceRef,long); 111 | void MTVibratorStop(MTDeviceRef); 112 | */ 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trackpad mapper for Mac 2 | 3 | This utility maps finger position on trackpad to curosr coordinate on screen. 4 | 5 | - [x] Lightweight 6 | - [x] Does not create window 7 | - [x] Highly configurable 8 | - [x] Status bar app for easy toggling absolute tracking 9 | 10 | I personally use this to play Osu! (& Osu!lazer) on Mac with trackpad. 11 | 12 | [Here](https://github.com/lokxii/Linux-Trackpad-Mapper) is the linux version with similar features. 13 | 14 | ## Usage 15 | 16 | Open the app, there is a trackpad icon shown on the status bar. By default, 17 | mapping was disabled. To toggle mapping, click the icon and choose 'Start 18 | absolute tracking' or 'Stop absolute tracking' 19 | 20 | ## Settings 21 | 22 | In the preference window, there are three items: 23 | 24 | 1. - [ ] Use settings in header file (settings.h) 25 | 2. Trackpad region: [Region] 26 | 3. Screen region: [Region] 27 | 28 | If you want to use your custom code to map coordinates, enable the first item. 29 | (See Modifying rule in header file) 30 | 31 | The syntax of [Region] is `lowx,lowy,upx,upy`, where all numbers are floats in 32 | range 0 to 1 inclusively. These numbers are passed as command line arguments to 33 | `trackpad_mapper_util`. Click `Apply` to update the settings and remember to 34 | restart absolute tracking. 35 | 36 | ## Modifying rule in header file 37 | 38 | The default settings are stored in `settings.def.h`. You may want to make a 39 | copy of it and rename it to `settings.h`. This is where all local settings are 40 | stored. 41 | 42 | ### Mapping 43 | 44 | There is a function `map` in `settings.h` that converts normalized finger 45 | position on trackpad to absolute coordinate of cursor on Screen. 46 | `MTPoint map(double, double)` must be provided in `settings.h` as it will be 47 | called in `trackpad_mapper_util.c`. 48 | 49 | ```C 50 | MTPoint map(double normx, double normy) { 51 | // whole trackpad to whole screen 52 | MTPoint point = { 53 | normx * screenSize.width, normy * screenSize.height 54 | }; 55 | return point; 56 | } 57 | ``` 58 | 59 | Use the function `rangeRatio()` to map custom arrangments. 60 | 61 | ```C 62 | .x = rangeRatio(normx, lowx, highx), 63 | .y = rangeRatio(normy, lowy, highy), 64 | ``` 65 | 66 | Set the `lowx` & `highx` to a number between 0 & 1, much like a percentage. To map the middle of the trackpad to the whole screen (for x dimension), set `lowx` to `.25` & `highx` to `.75`. 67 | 68 | Example code to map to top right quarter of trackpad to whole screen 69 | 70 | ```C 71 | MTPoint map(double normx, double normy) { 72 | // top right quarter of the area of trackpad to whole screen 73 | MTPoint point = { 74 | //the right half (.5 to 1) of the trackpad 75 | .x = rangeRatio(normx, .5, 1), 76 | //the top half (0 to 0.5) of the trackpad 77 | .y = rangeRatio(normy, 0, .5), 78 | }; 79 | point.x *= screenSize.width; 80 | point.y *= screenSize.height; 81 | return point; 82 | } 83 | ``` 84 | 85 | Remember to rebuild the util everytime you changed `map`. 86 | 87 | ### Screen Size 88 | 89 | `extern CGSize screenSize` declared in `settings.def.h` will be initialized in 90 | the main program. Do not remove the declaration. 91 | 92 | The main program makes sure the mapped cursor coordinate is within `screenSize` 93 | to make the dock appear. 94 | 95 | ### Emitting mouse events 96 | 97 | It is possible choose between emitting mouse event or wrapping cursor coordinate 98 | (without mouse event). Some programs requires emitting mouse events but enabling 99 | it will cause the cursor not to magnify. Set `bool emitMouseEvent = true` to 100 | emit mouse event. The default value is `false`. 101 | 102 | ## Building 103 | 104 | Building the app requires Xcode version >= 11. 105 | 106 | To compile, run 107 | ```sh 108 | make release 109 | ``` 110 | 111 | The app is located in `build/`. 112 | 113 | You may want to copy the app to the `/Applications/` folder or run 114 | ```sh 115 | make install 116 | ``` 117 | to do it for you. 118 | 119 | ## Rebuilding Util 120 | 121 | After you modified `map`, run 122 | ```sh 123 | make util_release install_util_update 124 | ``` 125 | This will compile and update the util binary in the app bundle inside `/Applications/` 126 | 127 | ## TODO List 128 | 129 | - [x] Create an app that can toggle absolute tracking 130 | - [ ] Better way to change mapping rule 131 | 132 | ## Reference 133 | Where I got the MultitouchSupport.h header 134 | https://gist.github.com/rmhsilva/61cc45587ed34707da34818a76476e11 135 | -------------------------------------------------------------------------------- /src/trackpad_mapper_util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "MultitouchSupport.h" 5 | #include "../settings.h" 6 | 7 | #define try(...) \ 8 | if ((__VA_ARGS__) == -1) { \ 9 | fprintf(stderr, "`%s` failed", #__VA_ARGS__); \ 10 | exit(1); \ 11 | } 12 | 13 | typedef struct { 14 | float lowx, lowy, upx, upy; 15 | } Range; 16 | 17 | typedef struct { 18 | bool useArg; 19 | Range trackpadRange; 20 | Range screenRange; 21 | bool emitMouseEvent; 22 | } Settings; 23 | 24 | Settings settings = { false, { 0, 0, 1, 1 }, { 0, 0, 1, 1, }, false }; 25 | CGSize screenSize; 26 | 27 | int mouseEventNumber = 0; 28 | pthread_mutex_t mouseEventNumber_mutex; 29 | #define MAGIC_NUMBER 12345 30 | 31 | double _rangeRatio(double n, double lower, double upper) { 32 | if (n < lower || n > upper) { 33 | return -1; 34 | } 35 | return (n - lower) / (upper - lower); 36 | } 37 | 38 | double _reverseRangeRatio(double n, double lower, double upper) { 39 | if (n < 0) { 40 | return n; 41 | } 42 | return n * (upper - lower) + lower; 43 | } 44 | 45 | MTPoint _map(double normx, double normy) { 46 | MTPoint point = { 47 | .x = _rangeRatio( 48 | normx, settings.trackpadRange.lowx, settings.trackpadRange.upx), 49 | .y = _rangeRatio( 50 | normy, settings.trackpadRange.lowy, settings.trackpadRange.upy), 51 | }; 52 | 53 | point.x = _reverseRangeRatio( 54 | point.x, settings.screenRange.lowx, settings.screenRange.upx); 55 | point.y = _reverseRangeRatio( 56 | point.y, settings.screenRange.lowy, settings.screenRange.upy); 57 | 58 | point.x *= screenSize.width; 59 | point.y *= screenSize.height; 60 | return point; 61 | } 62 | 63 | void moveCursor(double x, double y) { 64 | // Simple stabiliser: ignore very small moves and smooth bigger ones 65 | static double lastX = -1.0; 66 | static double lastY = -1.0; 67 | 68 | // Minimum movement (in screen pixels) before we move the cursor (change in settings.def.h / settings.h) 69 | const double threshold = JITTER_THRESHOLD; 70 | double alpha = JITTER_ALPHA; // from settings 71 | 72 | // Prevent alpha from being exactly 0 to avoid cursor freeze. 73 | // If the user sets JITTER_ALPHA = 0 in settings, we clamp it to 0.1. 74 | if (alpha <= 0.0) { 75 | alpha = 0.1; 76 | } else if (alpha > 1.0) { 77 | alpha = 1.0; 78 | } 79 | 80 | if (lastX >= 0.0 && lastY >= 0.0) { 81 | double dx = x - lastX; 82 | double dy = y - lastY; 83 | double dist2 = dx * dx + dy * dy; 84 | 85 | // If the movement is tiny, ignore it completely 86 | if (dist2 < threshold * threshold) { 87 | return; 88 | } 89 | 90 | // Low-pass filter: move part-way toward the new point 91 | x = lastX + alpha * dx; 92 | y = lastY + alpha * dy; 93 | } 94 | 95 | lastX = x; 96 | lastY = y; 97 | 98 | CGPoint point = (CGPoint){ 99 | .x = x < 0 ? 0 : x >= screenSize.width ? screenSize.width - 1 : x, 100 | .y = y < 0 ? 0 : y >= screenSize.height ? screenSize.height - 1 : y, 101 | }; 102 | 103 | if (settings.useArg && settings.emitMouseEvent || 104 | !settings.useArg && emitMouseEvent) 105 | { 106 | CGEventRef event = CGEventCreateMouseEvent( 107 | NULL, 108 | kCGEventMouseMoved, 109 | point, 110 | kCGMouseButtonLeft); 111 | CGEventSetIntegerValueField(event, kCGEventSourceUserData, MAGIC_NUMBER); 112 | CGEventSetIntegerValueField(event, kCGMouseEventSubtype, 3); 113 | 114 | try(pthread_mutex_lock(&mouseEventNumber_mutex)); 115 | CGEventSetIntegerValueField(event, kCGMouseEventNumber, mouseEventNumber); 116 | try(pthread_mutex_unlock(&mouseEventNumber_mutex)); 117 | 118 | CGEventPost(kCGHIDEventTap, event); 119 | } else { 120 | CGWarpMouseCursorPosition(point); 121 | } 122 | } 123 | 124 | // moving cursor here increases sensitivity to finger 125 | // detecting gesture: 126 | // Beginning of a gesture may start with one finger or more than one fingers. 127 | // Simply checking how many fingers touched is not enough. 128 | // Discard coordinates of the first callback call for each cursor movement 129 | // and wait for the second call to make sure it is not a gesture. 130 | int trackpadCallback( 131 | MTDeviceRef device, 132 | MTTouch *data, 133 | size_t nFingers, 134 | double timestamp, 135 | size_t frame) 136 | { 137 | #define GESTURE_PHASE_NONE 0 138 | #define GESTURE_PHASE_MAYSTART 1 139 | #define GESTURE_PHASE_BEGAN 2 140 | #define GESTURE_TIMEOUT 0.02 141 | 142 | static MTPoint fingerPosition = { 0, 0 }, 143 | oldFingerPosition = { 0, 0 }; 144 | static int32_t oldPathIndex = -1; 145 | static double oldTimeStamp = 0, 146 | startTrackTimeStamp = 0; 147 | static size_t oldFingerCount = 1; 148 | static int gesturePhase = GESTURE_PHASE_NONE; 149 | // FIXME: how many fingers can magic trackpad detect? 150 | static bool gesturePaths[20] = { 0 }; 151 | 152 | if (nFingers == 0) { 153 | // all fingers lifted, clearing gesture fingers 154 | for (int i = 0; i < 20; i++) { 155 | gesturePaths[i] = false; 156 | } 157 | gesturePhase = GESTURE_PHASE_NONE; 158 | oldFingerCount = nFingers; 159 | startTrackTimeStamp = 0; 160 | return 0; 161 | } 162 | 163 | 164 | 165 | if (!startTrackTimeStamp) { 166 | startTrackTimeStamp = timestamp; 167 | } 168 | 169 | if (oldFingerCount != 1 && nFingers == 1 && !gesturePhase) { 170 | gesturePhase = GESTURE_PHASE_MAYSTART; 171 | oldFingerCount = nFingers; 172 | return 0; 173 | }; 174 | 175 | if (nFingers == 1 && timestamp - startTrackTimeStamp < GESTURE_TIMEOUT) { 176 | return 0; 177 | } 178 | 179 | if (nFingers != 1 && ( 180 | timestamp - startTrackTimeStamp < GESTURE_TIMEOUT || 181 | gesturePhase != GESTURE_PHASE_NONE)) 182 | { 183 | gesturePhase = GESTURE_PHASE_BEGAN; 184 | for (int i = 0; i < nFingers; i++) { 185 | gesturePaths[data[i].pathIndex] = true; 186 | } 187 | if (!(DISABLE_CURSOR_ON_MULTITOUCH && nFingers > 1)) { 188 | moveCursor(fingerPosition.x, fingerPosition.y); 189 | } 190 | oldFingerCount = nFingers; 191 | return 0; 192 | }; 193 | 194 | // keeping one finger on trackpad when lifting up fingers 195 | // at the end of gesture 196 | if (gesturePhase == GESTURE_PHASE_BEGAN) { 197 | for (int i = 0; i < nFingers; i++) { 198 | if (gesturePaths[data[i].pathIndex]) { 199 | if (!(DISABLE_CURSOR_ON_MULTITOUCH && nFingers > 1)) { 200 | moveCursor(fingerPosition.x, fingerPosition.y); 201 | } 202 | return 0; 203 | } 204 | } 205 | } 206 | 207 | gesturePhase = GESTURE_PHASE_NONE; 208 | 209 | // remembers currently using which finger 210 | MTTouch *f = &data[0]; 211 | for (int i = 0; i < nFingers; i++){ 212 | if (data[i].pathIndex == oldPathIndex) { 213 | f = &data[i]; 214 | break; 215 | } 216 | } 217 | 218 | oldFingerPosition = fingerPosition; 219 | // use settings.h if no command line arguments are given 220 | fingerPosition = (settings.useArg ? _map : map)( 221 | f->normalizedVector.position.x, 1 - f->normalizedVector.position.y); 222 | MTPoint velocity = f->normalizedVector.velocity; 223 | 224 | if (fingerPosition.x < 0 || fingerPosition.y < 0) { 225 | // Only lock cursor when finger starts path on dead zone 226 | if (f->pathIndex == oldPathIndex) { 227 | if (fingerPosition.x < 0) { 228 | fingerPosition.x = oldFingerPosition.x + 229 | velocity.x * (timestamp - oldTimeStamp) * 1000; 230 | } 231 | if (fingerPosition.y < 0) { 232 | fingerPosition.y = oldFingerPosition.y - 233 | velocity.y * (timestamp - oldTimeStamp) * 1000; 234 | } 235 | 236 | } else { 237 | fingerPosition = oldFingerPosition; 238 | } 239 | } else { 240 | oldPathIndex = f->pathIndex; 241 | } 242 | 243 | if (!(DISABLE_CURSOR_ON_MULTITOUCH && nFingers > 1)) { 244 | moveCursor(fingerPosition.x, fingerPosition.y); 245 | } 246 | 247 | 248 | oldTimeStamp = timestamp; 249 | return 0; 250 | } 251 | 252 | bool check_privileges(void) { 253 | bool result; 254 | const void *keys[] = { kAXTrustedCheckOptionPrompt }; 255 | const void *values[] = { kCFBooleanTrue }; 256 | 257 | CFDictionaryRef options; 258 | options = CFDictionaryCreate( 259 | kCFAllocatorDefault, 260 | keys, values, sizeof(keys) / sizeof(*keys), 261 | &kCFCopyStringDictionaryKeyCallBacks, 262 | &kCFTypeDictionaryValueCallBacks); 263 | 264 | result = AXIsProcessTrustedWithOptions(options); 265 | CFRelease(options); 266 | 267 | return result; 268 | } 269 | 270 | Range parseRange(char* s) { 271 | char* token[4 + 1]; 272 | int i = 0; 273 | for (;(token[i] = strsep(&s, ",")) != NULL && i < 4; i++) { } 274 | if (i != 4 || token[4] != NULL) { 275 | fputs("Range format: lowx,lowy,upx,upy and numbers in range [0, 1]", stderr); 276 | exit(1); 277 | } 278 | float num[4]; 279 | for (i = 0; i < 4; i++){ 280 | char* endptr; 281 | num[i] = strtof(token[i], &endptr); 282 | if (*endptr) { 283 | fprintf(stderr, "Invalid number %s\n", token[i]); 284 | exit(1); 285 | } 286 | } 287 | return (Range) { 288 | num[0], num[1], num[2], num[3] 289 | }; 290 | } 291 | 292 | void parseSettings(int argc, char** argv) { 293 | int opt; 294 | while ((opt = getopt(argc, argv, "i:o:e")) != -1) { 295 | switch (opt) { 296 | case 'i': 297 | settings.trackpadRange = parseRange(optarg); 298 | settings.useArg = true; 299 | break; 300 | case 'o': 301 | settings.screenRange = parseRange(optarg); 302 | settings.useArg = true; 303 | break; 304 | case 'e': 305 | settings.emitMouseEvent = true; 306 | settings.useArg = true; 307 | break; 308 | default: 309 | fprintf(stderr, "Usage: %s [-i lowx,lowy,upx,upy] [-o lowx,lowy,upx,upy] [-e]\n", argv[0]); 310 | exit(EXIT_FAILURE); 311 | } 312 | } 313 | } 314 | 315 | CGEventRef loggerCallback( 316 | CGEventTapProxy proxy, 317 | CGEventType type, 318 | CGEventRef event, 319 | void* context) 320 | { 321 | int magic_number = CGEventGetIntegerValueField(event, kCGEventSourceUserData); 322 | if (magic_number == MAGIC_NUMBER) { 323 | return event; 324 | } 325 | int eventNumber = CGEventGetIntegerValueField(event, kCGMouseEventNumber); 326 | try(pthread_mutex_lock(&mouseEventNumber_mutex)); 327 | mouseEventNumber = eventNumber; 328 | try(pthread_mutex_unlock(&mouseEventNumber_mutex)); 329 | return event; 330 | } 331 | 332 | int main(int argc, char** argv) { 333 | // if (!check_privileges()) { 334 | // printf("Requires accessbility privileges\n"); 335 | // return 1; 336 | // } 337 | parseSettings(argc, argv); 338 | screenSize = CGDisplayBounds(CGMainDisplayID()).size; 339 | 340 | try(pthread_mutex_init(&mouseEventNumber_mutex, NULL)); 341 | 342 | // start trackpad service 343 | // https://github.com/JitouchApp/Jitouch-project/blob/3b5018e4bc839426a6ce0917cea6df753d19da10/Application/Gesture.m#L2926-L2954 344 | CFArrayRef deviceList = MTDeviceCreateList(); 345 | for (CFIndex i = 0; i < CFArrayGetCount(deviceList); i++) { 346 | MTDeviceRef device = (MTDeviceRef)CFArrayGetValueAtIndex(deviceList, i); 347 | int familyId; 348 | MTDeviceGetFamilyID(device, &familyId); 349 | if ( 350 | familyId >= 98 351 | && familyId != 112 && familyId != 113 // Magic Mouse 1&2 / 3 352 | ) { 353 | MTRegisterContactFrameCallback(device, (MTFrameCallbackFunction)trackpadCallback); 354 | MTDeviceStart(device, 0); 355 | } 356 | } 357 | 358 | // simply an infinite loop waiting for app to quit 359 | CFRunLoopRun(); 360 | return 0; 361 | } 362 | --------------------------------------------------------------------------------