TrackPoint Navigation Mode provides joystick-style cursor control by holding arrow keys (without initial swipe movement). When activated, finger movement in any direction moves the cursor proportionally, with speed scaling based on distance from the activation point. Supports all 8 directions including diagonals for fluid text navigation.
| File | Class/Function | Purpose |
|---|---|---|
src/main/kotlin/tribixbite/cleverkeys/Pointers.kt |
handleTrackPointRepeat() |
Main cursor movement logic |
src/main/kotlin/tribixbite/cleverkeys/Pointers.kt |
FLAG_P_TRACKPOINT_MODE |
Mode state flag (value: 64) |
src/main/kotlin/tribixbite/cleverkeys/VibratorCompat.kt |
CLOCK_TICK pattern |
Distinct haptic on activation |
src/main/kotlin/tribixbite/cleverkeys/Config.kt |
Haptic toggle | TrackPoint haptic setting |
User Input (arrow key touch + hold without movement)
|
v
+------------------+
| onTouchDown() | -- Records initial touch position
+------------------+
|
v (no initial movement + hold time exceeded)
+------------------+
| TrackPoint Mode | -- FLAG_P_TRACKPOINT_MODE activated
| Activation | -- CLOCK_TICK haptic feedback
+------------------+
|
v
+------------------+
| handleTrackPoint | -- Repeating handler tracks position
| Repeat() | -- Calculates direction and distance
+------------------+
|
v
+------------------+
| Cursor Movement | -- Sends DPAD_* key events
| Handler | -- Supports all 8 directions
+------------------+
- Activation: Hold arrow key without initial swipe movement
- Detection: After
LONGPRESS_TIMEOUTwith <30px movement, mode activates - Tracking: Repeat handler reads current finger position
- Direction: Calculate angle from activation point to current position
- Speed: Distance from center determines repeat rate
- Output: Appropriate DPAD_* key events sent to input connection
| Key | Type | Default | Description |
|---|---|---|---|
trackpoint_haptic_enabled |
Boolean | true | Enable CLOCK_TICK on mode activation |
short_gesture_min_distance |
Int | 15 | Pixels before short swipe detection (30px for nav keys) |
// State flag constant
private const val FLAG_P_TRACKPOINT_MODE = 64
// Timer identifier for repeat handler
private val trackpointWhat = Any()
// Main TrackPoint handler - called repeatedly while mode active
private fun handleTrackPointRepeat(ptr: Pointer) {
val dx = ptr.currentX - ptr.activationX
val dy = ptr.currentY - ptr.activationY
// Calculate 8-direction from delta
val direction = getTrackPointDirection(dx, dy)
// Send cursor movement(s)
sendCursorMovement(direction)
}
// Get direction enum from delta coordinates
private fun getTrackPointDirection(dx: Float, dy: Float): TrackPointDirection
// Send arrow key event(s) for direction
private fun sendCursorMovement(direction: TrackPointDirection)
// Trigger activation haptic
private fun triggerTrackPointHaptic()enum class TrackPointDirection {
NORTH, // Up only
NORTHEAST, // Up + Right
EAST, // Right only
SOUTHEAST, // Down + Right
SOUTH, // Down only
SOUTHWEST, // Down + Left
WEST, // Left only
NORTHWEST // Up + Left
}TrackPoint mode activates when:
- Arrow key touched
- Finger moved <30px from initial position (nav keys use 30px, others use 15px)
- Hold time exceeds
LONGPRESS_TIMEOUT(600ms)
If finger moves >30px before timeout, it's a short swipe (single cursor move).
// Nav keys have increased tolerance to allow hold activation
val isNavKey = key.kind in listOf(Kind.Arrow_Up, Kind.Arrow_Down, Kind.Arrow_Left, Kind.Arrow_Right)
val movementThreshold = if (isNavKey) 30 else 15
if (distance < movementThreshold && holdTime > LONGPRESS_TIMEOUT) {
activateTrackPointMode(ptr)
}Direction determined by angle from activation point:
private fun getTrackPointDirection(dx: Float, dy: Float): TrackPointDirection {
val angle = atan2(dy.toDouble(), dx.toDouble())
val degrees = Math.toDegrees(angle)
// Map angle to 8 sectors (45 degrees each)
return when {
degrees in -22.5..22.5 -> EAST
degrees in 22.5..67.5 -> SOUTHEAST
degrees in 67.5..112.5 -> SOUTH
degrees in 112.5..157.5 -> SOUTHWEST
degrees > 157.5 || degrees < -157.5 -> WEST
degrees in -157.5..-112.5 -> NORTHWEST
degrees in -112.5..-67.5 -> NORTH
degrees in -67.5..-22.5 -> NORTHEAST
else -> EAST // fallback
}
}Diagonal directions send two key events:
private fun sendCursorMovement(direction: TrackPointDirection) {
when (direction) {
NORTH -> sendArrowKey(KEYCODE_DPAD_UP)
NORTHEAST -> {
sendArrowKey(KEYCODE_DPAD_UP)
sendArrowKey(KEYCODE_DPAD_RIGHT)
}
EAST -> sendArrowKey(KEYCODE_DPAD_RIGHT)
SOUTHEAST -> {
sendArrowKey(KEYCODE_DPAD_DOWN)
sendArrowKey(KEYCODE_DPAD_RIGHT)
}
SOUTH -> sendArrowKey(KEYCODE_DPAD_DOWN)
SOUTHWEST -> {
sendArrowKey(KEYCODE_DPAD_DOWN)
sendArrowKey(KEYCODE_DPAD_LEFT)
}
WEST -> sendArrowKey(KEYCODE_DPAD_LEFT)
NORTHWEST -> {
sendArrowKey(KEYCODE_DPAD_UP)
sendArrowKey(KEYCODE_DPAD_LEFT)
}
}
}Cursor movement speed scales with finger distance from activation center:
val distance = sqrt(dx * dx + dy * dy)
val repeatDelay = when {
distance < 50 -> 200 // Slow (5 moves/sec)
distance < 100 -> 100 // Medium (10 moves/sec)
distance < 150 -> 50 // Fast (20 moves/sec)
else -> 25 // Very fast (40 moves/sec)
}
handler.postDelayed(trackpointRunnable, repeatDelay)Distinct CLOCK_TICK pattern on activation:
private fun triggerTrackPointHaptic() {
if (config.trackpointHapticEnabled) {
VibratorCompat.vibrate(HapticEvent.CLOCK_TICK)
}
}CLOCK_TICK provides a subtle, distinct feel different from normal key press vibration.
Arrow keys are excluded from short gesture path collection to prevent accidental gesture triggering:
private fun shouldCollectGesturePath(key: KeyValue): Boolean {
// Nav keys excluded - they have TrackPoint mode instead
if (key.kind in listOf(Kind.Arrow_Up, Kind.Arrow_Down, Kind.Arrow_Left, Kind.Arrow_Right)) {
return false
}
return true
}- No navigation target: Cursor movement commands sent but have no effect
- Haptic unavailable: Mode still functions, just without feedback
- Rapid re-activation: Debounced to prevent haptic spam (min 200ms between activations)