├── .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 |
--------------------------------------------------------------------------------