Skip to content

Feature/trackpoint virtual touch mode - #53

Merged
stormpanda merged 9 commits into
mainfrom
feature/trackpoint-virtual-touch-mode
Jun 12, 2026
Merged

Feature/trackpoint virtual touch mode#53
stormpanda merged 9 commits into
mainfrom
feature/trackpoint-virtual-touch-mode

Conversation

@stormpanda

Copy link
Copy Markdown
Owner

No description provided.

- add TrackpointMode enum (PHYSICAL_MOUSE, VIRTUAL_TOUCH) in MacroPadLayout.kt
- extend PadAction.TrackpointMove to include trackpoint mode parameter
- add enableTouch flag to PadProfile data model and update state synchronization in MacroPadState.kt
- implement dropdown selector and explainer descriptions in PadButtonEditDialog.kt
- define strings for tracking modes and descriptions in English and German strings.xml
- implement absolute cursor coordinate tracking and virtual touch event injection in MacroPadHitTestEngine.kt
- manage TouchInjector lifecycle dynamically in MacroPadViewModel.kt and BackgroundMacroPadOverlay.kt
- update device availability checks for virtual touch in ButtonListItem.kt, MacroPadEditor.kt, and PadCanvas.kt
- add unit test suite in MacroPadHitTestEngineTest.kt covering coordinate calculations, swipes, and boundaries
- update macropad FEATURE.md functional requirements and schemas
…irection reversal jumps

- Clamp physical touch coordinates to display bounds [0.0, 1.0] in TouchInjector to prevent coordinate wrapping and negative coordinate jumps.
- Implement direction-change detection in MacroPadHitTestEngine using drag delta signs.
- Snap unclamped cursor coordinates immediately to clamped internally-tracked positions on direction changes to eliminate lag and dead zones when moving back from edges.
- Reset direction sign trackers on press, release, and resets.
- Update MacroPadHitTestEngineTest unit tests to verify clamped coordinate injection and direction-change snapping.
- Synchronize FEATURE.md documentation to reflect the updated edge clamping and direction snapping requirements.
…t relative mouse catch-up

- Update coordinate clamping in TouchInjector.kt to the safe overrun range of [-0.5f, 1.5f].
- Resolve issue where simulated relative cursors became caught and could not reach screen boundaries.
- Update MacroPadHitTestEngineTest assertions to verify clamped [-0.5f, 1.5f] coordinates.
…ing mode

- Add ' (WIP)' suffix to 'macropad_trackpoint_mode_touch' string resource.
- Add ' (WIP)' suffix to 'macropad_trackpoint_mode_touch_desc' string resource.
- Update both English and German locale configurations.
- update VIEWPORT_ZOOM_MAX to 10f in MirrorViewportController
- update mirror zoom range in README.md and mirror FEATURE.md
- update manual verification checklist to assert 10.0x zoom limit

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.

Comment on lines 61 to 63
@@ -63,17 +63,21 @@ object TouchInjector {
* @param normalizedY 0.0 (top) … 1.0 (bottom) of the logical display
Comment thread docs/features/macropad/FEATURE.md Outdated
Comment on lines +63 to +66
- `enableKeyboard = true` if any button has a `KeyboardKey` action.
- `enableGamepad = true` if any button has a `GamepadButton` action.
- `enableMouse = true` if any button has a `MouseButton`, `ScrollWheel`, or `TrackpointMove` action.
- `enableMouse = true` if any button has a `MouseButton`, `ScrollWheel`, or `TrackpointMove` (with `PHYSICAL_MOUSE` tracking mode) action.
- `enableTouch = true` if any button has a `TrackpointMove` (with `VIRTUAL_TOUCH` tracking mode) action.
Comment thread docs/features/macropad/FEATURE.md Outdated
- In use mode, dragging a finger on a trackpoint button translates relative motion into **REL_X / REL_Y mouse events** via `MouseInjector.moveMouse()`. Sensitivity is fixed at 3× the raw pixel delta (`MP_TRACKPOINT_SENSITIVITY = 3f`).
- In use mode, dragging a finger on a trackpoint button translates relative motion depending on the configured **Tracking Mode**:
- **Virtual Physical Mouse**: relative motion is translated into **REL_X / REL_Y mouse events** via `MouseInjector.moveMouse()`. Sensitivity is fixed at 3× the raw pixel delta (`MP_TRACKPOINT_SENSITIVITY = 3f`).
- **Virtual Touch**: relative motion is translated into absolute touches on the primary landscape display (`1920x1080`) via `TouchInjector.injectTouch()`. A virtual cursor position is tracked internally (initially at center `(0.5f, 0.5f)`) and persists across finger lifts, allowing subsequent swipes from the same position. The internally tracked position (`virtualCursorX` / `virtualCursorY`) is clamped to screen boundaries `[0.0f, 1.0f]` at all times. Touch events injected to the system are clamped to screen boundaries `[0.0f, 1.0f]` to prevent coordinate wrapping or jumps. Relative movements are tracked unclamped during dragging, but snap immediately to the internally tracked position as soon as the user changes drag direction (sign change in delta) to eliminate lag or dead zones when moving back from edges. There is no snap-back logic on release or overshoot limits.
Comment on lines 44 to 49
val ms = hasMacro || allButtons.any {
it.action is PadAction.MouseButton ||
it.action is PadAction.ScrollWheel ||
it.action is PadAction.TrackpointMove ||
(it.action is PadAction.TrackpointMove && (it.action as PadAction.TrackpointMove).mode == TrackpointMode.PHYSICAL_MOUSE) ||
it.action is PadAction.FullScreenMouse ||
it.action is PadAction.MirrorTouchProjection
Comment on lines +143 to +147
@@ -143,20 +144,23 @@
KeyInjector.stop()
GamepadInjector.stop()
MouseInjector.stop()
TouchInjector.stop()
Comment on lines 150 to 154
AppLog.d(TAG, "pill menu open → stopping gamepad/mouse injectors")
GamepadInjector.stop()
MouseInjector.stop()
TouchInjector.stop()
}
…ch injector safety

- remove MirrorTouchProjection from enableMouse profile flag derivation
- remove TouchInjector.stop() from BackgroundMacroPadOverlay modal gates to prevent lifecycle races
- update TouchInjector KDocs and macropad FEATURE.md to match safe overrun clamping [-0.5, 1.5]
- add withSyncedDeviceFlags synchronization rules unit test in MacroPadStateTest

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Comment on lines 312 to +323
fun releaseAll(buttons: List<PadButton>) {
val activeIds = _pressedIds.value
_pressedIds.value = emptySet()
pointerMap.clear()
lastTpPos = null
scrollStartY.clear()
virtualCursorX = virtualCursorX.coerceIn(0f, 1f)
virtualCursorY = virtualCursorY.coerceIn(0f, 1f)
unclampedCursorX = virtualCursorX
unclampedCursorY = virtualCursorY
lastDxSign = 0f
lastDySign = 0f
Comment on lines +183 to +184
val dxNormalized = (deltaX * MP_TRACKPOINT_SENSITIVITY) / 1920f
val dyNormalized = (deltaY * MP_TRACKPOINT_SENSITIVITY) / 1080f
Comment thread docs/features/macropad/FEATURE.md Outdated
- In use mode, dragging a finger on a trackpoint button translates relative motion into **REL_X / REL_Y mouse events** via `MouseInjector.moveMouse()`. Sensitivity is fixed at 3× the raw pixel delta (`MP_TRACKPOINT_SENSITIVITY = 3f`).
- In use mode, dragging a finger on a trackpoint button translates relative motion depending on the configured **Tracking Mode**:
- **Virtual Physical Mouse**: relative motion is translated into **REL_X / REL_Y mouse events** via `MouseInjector.moveMouse()`. Sensitivity is fixed at 3× the raw pixel delta (`MP_TRACKPOINT_SENSITIVITY = 3f`).
- **Virtual Touch**: relative motion is translated into absolute touches on the primary landscape display (`1920x1080`) via `TouchInjector.injectTouch()`. A virtual cursor position is tracked internally (initially at center `(0.5f, 0.5f)`) and persists across finger lifts, allowing subsequent swipes from the same position. The internally tracked position (`virtualCursorX` / `virtualCursorY`) is clamped to screen boundaries `[0.0f, 1.0f]` at all times. Touch events injected to the system are clamped to the safe overrun range of `[-0.5f, 1.5f]` to prevent coordinate wrapping or jumps while allowing the system cursor to catch up. Relative movements are tracked unclamped during dragging, but snap immediately to the internally tracked position as soon as the user changes drag direction (sign change in delta) to eliminate lag or dead zones when moving back from edges. There is no snap-back logic on release or overshoot limits.
…gic numbers

- update releaseAll() in MacroPadHitTestEngine to send UP event for active virtual-touch trackpoints on cancellation
- extract logical display dimensions 1920f and 1080f to private constants in MacroPadHitTestEngine
- resolve documentation inconsistency regarding overshoot limits in macropad FEATURE.md
- add releaseAll unit test for virtual touch trackpoint cancellation

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Comment on lines 44 to 49
val ms = hasMacro || allButtons.any {
it.action is PadAction.MouseButton ||
it.action is PadAction.ScrollWheel ||
it.action is PadAction.TrackpointMove ||
it.action is PadAction.FullScreenMouse ||
it.action is PadAction.MirrorTouchProjection
(it.action is PadAction.TrackpointMove && (it.action as PadAction.TrackpointMove).mode == TrackpointMode.PHYSICAL_MOUSE) ||
it.action is PadAction.FullScreenMouse
}
Comment on lines 128 to 134
fun stopInjectors() {
AppLog.i(TAG, "stopInjectors called")
KeyInjector.stop()
GamepadInjector.stop()
MouseInjector.stop()
TouchInjector.stop()
MacroPadState.resetPeek()
Comment on lines 139 to 144
AppLog.i(TAG, "onCleared → all injectors stopped")
KeyInjector.stop()
GamepadInjector.stop()
MouseInjector.stop()
TouchInjector.stop()
MacroPadState.resetPeek()
Comment on lines 171 to 176
AppLog.d(TAG, "BackgroundMacroPadOverlay disposed → all injectors stopped")
KeyInjector.stop()
GamepadInjector.stop()
MouseInjector.stop()
TouchInjector.stop()
MacroPadState.resetPeek()
…client tokens

- introduce synchronized activeClients registry in TouchInjector to manage concurrent client start/stop requests
- update all TouchInjector start and stop calls in overlays, presentations, ViewModels, and executors to pass unique client tokens
- remove redundant injectorStartedByUs flag from RecordingMirrorPresentation
- update KDoc for withSyncedDeviceFlags in MacroPadState to reflect enableTouch derivation
- sync technical implementation documentation in mirror and touchpad FEATURE.md files

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Comment on lines +69 to +76
/* Flush and stop on a daemon thread to avoid blocking the calling thread.
stop() can be invoked from Compose DisposableEffect.onDispose (main thread). */
Thread {
if (!ShellInputInjector.flushPendingTouches(TOUCH_STOP_FLUSH_TIMEOUT_MS)) {
AppLog.w(TAG, "stop() timed out while flushing touch release commands")
}
ShellInputInjector.stop()
}.also { it.isDaemon = true }.start()
Comment on lines +56 to +60
fun stop(token: String) {
if (!activeClients.contains(token)) {
AppLog.w(TAG, "stop() called for non-active client '$token'. Ignoring.")
return
}
Comment on lines +329 to +334
Both the Virtual Touchpad and Mirror Touch Projection use `TouchInjector` from the `input/` package. The same native binary (`touchinjector_arm64`) and device node (`/dev/input/event6`) are used by both features. To coordinate the native process lifetime across multiple concurrent callers (Mirror Touch Projection, relative trackpoints in MacroPad, macro executors), `TouchInjector` implements a thread-safe, client-aware reference-counted lifecycle. The native binary is started when the first client registers itself, and is terminated only after the last active client has unregistered.

**Lifecycle:**

- `LaunchedEffect(isTouchProjectionActive)` starts the injector when projection is enabled and stops it when disabled.
- `DisposableEffect(Unit)` stops the injector unconditionally when `MirrorScreen` leaves composition (mode switch).
- `LaunchedEffect(isTouchProjectionActive)` starts the injector with the `"MirrorPresentation"` token when projection is enabled, and stops it when disabled.
- `DisposableEffect(Unit)` stops the injector with the `"MirrorPresentation"` token when `MirrorScreen` leaves composition (mode switch).
Comment on lines +61 to +68
- Each profile has four independent boolean flags: `enableKeyboard`, `enableGamepad`, `enableMouse`, `enableTouch` (all default **`false`** — new profiles start with all injectors off).
- These flags are **not user-configurable** directly. They are automatically recomputed whenever the button list changes (add / edit / delete) by inspecting the action types of all buttons. The exact derivation rules are:
- If the profile contains any button with a `Macro` action, all four flags are force-enabled (`true`).
- Otherwise, each flag is set to `true` if any button matches the following actions:
- `enableKeyboard = true` if any button has a `KeyboardKey` or `FullScreenKeyboard` action.
- `enableGamepad = true` if any button has a `GamepadButton` action.
- `enableMouse = true` if any button has a `MouseButton`, `ScrollWheel`, `TrackpointMove` (with `PHYSICAL_MOUSE` tracking mode), or `FullScreenMouse` action. (Note: `MirrorTouchProjection` is explicitly excluded from this derivation because the screen mirror presentation manages its own touch injector lifecycle).
- `enableTouch = true` if any button has a `TrackpointMove` (with `VIRTUAL_TOUCH` tracking mode) action.
…r lifecycle stop

- synchronize on TouchInjector inside stop(token)'s flush thread to prevent stopping if new clients register during flush
- downgrade non-active client stop warning log to debug level to prevent noisy logs in normal safety-net flows
- update remaining no-arg TouchInjector.stop() documentation references in mirror and macropad FEATURE.md files

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.

@stormpanda
stormpanda merged commit f505540 into main Jun 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants