feat: Improve pointer exclusivity handling#1152
feat: Improve pointer exclusivity handling#1152Unnvaldr wants to merge 2 commits intoutkarshdalal:masterfrom
Conversation
📝 WalkthroughWalkthroughRefactors pointer-capture management by extracting InputCaptureManager, moves lifecycle and focus-based capture logic out of TouchpadView, and updates XServerScreen to use the new manager for delayed restore, menu-driven release/restore, and a keyboard shortcut to toggle pointer capture. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant XServerScreen
participant TouchpadView
participant InputCaptureManager
participant AndroidOS
User->>XServerScreen: Press Ctrl+Shift+Alt+Z (ACTION_UP)
XServerScreen->>TouchpadView: getInputCaptureManager()
TouchpadView->>InputCaptureManager: togglePointerCapture()
InputCaptureManager->>AndroidOS: requestPointerCapture() / releasePointerCapture()
AndroidOS-->>InputCaptureManager: capture state updated
User->>XServerScreen: Show Quick Menu
XServerScreen->>TouchpadView: inputCaptureManager.disablePointerCapture()
InputCaptureManager->>AndroidOS: releasePointerCapture()
AndroidOS-->>InputCaptureManager: released
User->>XServerScreen: Dismiss Quick Menu
XServerScreen->>TouchpadView: post { inputCaptureManager.refreshPointerCapture() }
InputCaptureManager->>AndroidOS: requestPointerCapture() (if needed)
AndroidOS-->>InputCaptureManager: capture state updated
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/com/winlator/widget/TouchpadView.java">
<violation number="1" location="app/src/main/java/com/winlator/widget/TouchpadView.java:180">
P2: Focus-loss release clears the request flag, preventing automatic pointer recapture when window focus returns.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 1125-1128: The current branch sends physical-device events to
Keyboard.onKeyEvent which loses modifier/shifted-character handling; change the
routing so both virtual and physical devices call Keyboard.onVirtualKeyEvent
until onKeyEvent is feature-parity-ready. In the block that checks
it.event.device?.isVirtual and sets handled via keyboard?.onVirtualKeyEvent(...)
or keyboard?.onKeyEvent(...), remove the else branch and invoke
keyboard?.onVirtualKeyEvent(it.event) for all cases (preserving the handled
assignment) so physical keyboard input continues to include modifier/keysym
data.
- Around line 1101-1108: The special-key chord handling leaks the Z key event
and pointer recapture; update the chord interception to consume both ACTION_DOWN
and ACTION_UP for KeyEvent.KEYCODE_Z (not only ACTION_UP) so the guest never
sees the unmatched press/release, and set handled=true for both. Also change the
direct pointer-capture call in tryCapturePointer() (and any direct
requestPointerCapture() usages) to route through the TouchpadView helper APIs
(e.g., PluviaApp.touchpadView?.togglePointerExclusive() / a new
touchpadView.requestPointerCaptureHelper()) so pointer capture is
granted/revoked only via TouchpadView and cannot immediately reacquire after the
user toggles it off. Ensure you update the matching event branch names
(areAllSpecialKeysUp / chord logic) to reflect handling both actions.
In `@app/src/main/java/com/winlator/widget/TouchpadView.java`:
- Around line 173-180: The current releasePointerExclusive() method
unconditionally clears pointerCaptureRequested, which conflates a transient
suspension with a permanent opt-out and prevents determinePointerExclusivity()
from restoring capture after temporary releases; change the logic by introducing
a new suspendPointerExclusive() method for transient cases (used by
quick-menu/focus-loss paths) that calls releasePointerCapture() but does NOT set
pointerCaptureRequested = false, and reserve releasePointerExclusive() to
perform the permanent opt-out by releasing capture and setting
pointerCaptureRequested = false so determinePointerExclusivity() can correctly
recapture after suspensions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5232eb80-b0a3-4f87-b455-f34aa310b9e9
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.ktapp/src/main/java/com/winlator/widget/TouchpadView.java
| determinePointerExclusivity(); | ||
| } | ||
|
|
||
| public void determinePointerExclusivity() { | ||
| if (hasFocus()) { | ||
| if (!hasPointerCapture() && pointerCaptureRequested) | ||
| { | ||
| requestPointerExclusive(); | ||
| } | ||
| } else { | ||
| releasePointerExclusive(); | ||
| } | ||
| } | ||
|
|
||
| public void togglePointerExclusive() { | ||
| if (!capturePointerOnExternalMouse) return; | ||
| if (hasPointerCapture()) { | ||
| releasePointerExclusive(); | ||
| } else { | ||
| requestPointerExclusive(); | ||
| } | ||
| } | ||
|
|
||
| public void releasePointerExclusive() { | ||
| if (!capturePointerOnExternalMouse) return; | ||
| if (!hasPointerCapture() || !pointerCaptureRequested) { | ||
| Log.v("TouchpadView", "Pointer capture: Pointer capture not detected, skipped"); | ||
| return; | ||
| } | ||
| releasePointerCapture(); | ||
| pointerCaptureRequested = false; | ||
| Log.v("TouchpadView", String.format("Pointer capture: Pointer capture release (state=%s).", hasPointerCapture())); | ||
| } | ||
|
|
||
| @SuppressLint("LogNotTimber") | ||
| public void requestPointerExclusive() { | ||
| if (!capturePointerOnExternalMouse) return; | ||
| if (!hasFocus() && !requestFocus()) { | ||
| Log.w("TouchpadView", "Pointer capture: Unable to request pointer capture, view is unfocused and cannot regain focus!"); | ||
| return; | ||
| } | ||
| if (hasPointerCapture() && !pointerCaptureRequested) { | ||
| Log.v("TouchpadView", "Pointer capture: Pointer capture already requested, skipped"); | ||
| return; | ||
| } | ||
| requestPointerCapture(); | ||
| pointerCaptureRequested = true; | ||
| Log.v("TouchpadView", String.format("Pointer capture: Pointer capture request (state=%s).", hasPointerCapture())); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onAttachedToWindow() { | ||
| super.onAttachedToWindow(); | ||
| requestPointerExclusive(); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onDetachedFromWindow() { | ||
| super.onDetachedFromWindow(); | ||
| releasePointerExclusive(); |
There was a problem hiding this comment.
why not add a boolean toggle to tryCapturePointer instead?
There was a problem hiding this comment.
It was based on the previous contributors work, where logic was split between Winlator's and GameNative's classes.
With new changes, we can move to either class. Preferably, all pointer capture logic moved to the new manager, just the host class would change.
Let me know what might be more desireable considering vaster scope.
cb7f036 to
9d4bb3e
Compare
Extract pointer capture logic to a separate manager, introduce state based pointer capture, recapture on overlay menu dismissal
9d4bb3e to
f7573a7
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (2)
app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java (1)
45-55:⚠️ Potential issue | 🟠 MajorDon't clear
pointerCaptureRequestedduring transient refreshes.
refreshPointerCapture()is now the restore path used after quick-menu dismissal and focus regain. ClearingpointerCaptureRequestedon the!shouldCapture && hasCapturebranch turns a temporary release into a permanent opt-out, so the laterrefreshPointerCapture()call has nothing left to restore.Suggested fix
public void refreshPointerCapture() { boolean shouldCapture = targetView.hasFocus() && pointerCaptureRequested; boolean hasCapture = targetView.hasPointerCapture(); if (shouldCapture && !hasCapture) { enablePointerCapture(); - setPointerCaptureRequested(true); } else if (!shouldCapture && hasCapture) { disablePointerCapture(); - setPointerCaptureRequested(false); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java` around lines 45 - 55, refreshPointerCapture currently clears the persistent flag pointerCaptureRequested by calling setPointerCaptureRequested(false) when temporarily releasing capture, which turns transient releases into permanent opt-outs; change refreshPointerCapture so the branch that calls disablePointerCapture() does NOT clear pointerCaptureRequested (remove or guard the setPointerCaptureRequested(false) call) so that transient refreshes can be restored later by subsequent refreshPointerCapture calls; leave enablePointerCapture(), setPointerCaptureRequested(true) behavior unchanged.app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)
1225-1254:⚠️ Potential issue | 🟠 MajorConsume
Ctrl+Shift+Alt+Zon both key-down and key-up.Right now only the
ACTION_UPhalf of the chord is intercepted. TheACTION_DOWNstill falls through to the guest keyboard handler, so games see a strayZpress whenever the user toggles pointer capture.Suggested fix
- val areAllSpecialKeysUp = it.event.isCtrlPressed and it.event.isShiftPressed and it.event.isAltPressed && it.event.action == KeyEvent.ACTION_UP - if (areAllSpecialKeysUp) { - // Handing special key combination - when (it.event.keyCode) { - KeyEvent.KEYCODE_Z -> { - // Toggles pointer exclusivity when in game view - PluviaApp.touchpadView?.inputCaptureManager?.togglePointerCapture() - handled = true - } - else -> { - handled = false - } - } + val isPointerCaptureChord = + it.event.keyCode == KeyEvent.KEYCODE_Z && + it.event.isCtrlPressed && + it.event.isShiftPressed && + it.event.isAltPressed + if (isPointerCaptureChord) { + if (it.event.action == KeyEvent.ACTION_DOWN && it.event.repeatCount == 0) { + PluviaApp.touchpadView?.inputCaptureManager?.togglePointerCapture() + } + handled = true }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around lines 1225 - 1254, The code only detects the Ctrl+Shift+Alt+Z chord on ACTION_UP (variable areAllSpecialKeysUp), letting the ACTION_DOWN fall through to keyboard handlers; modify the chord detection logic to detect the same chord on both ACTION_DOWN and ACTION_UP (or check event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP) and when the chord is matched call PluviaApp.touchpadView?.inputCaptureManager?.togglePointerCapture() and set handled = true for both actions so the guest handlers (keyboard?.onKeyEvent / keyboard?.onVirtualKeyEvent) never receive the stray 'Z' press.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 1225-1254: The code only detects the Ctrl+Shift+Alt+Z chord on
ACTION_UP (variable areAllSpecialKeysUp), letting the ACTION_DOWN fall through
to keyboard handlers; modify the chord detection logic to detect the same chord
on both ACTION_DOWN and ACTION_UP (or check event.action == KeyEvent.ACTION_DOWN
|| event.action == KeyEvent.ACTION_UP) and when the chord is matched call
PluviaApp.touchpadView?.inputCaptureManager?.togglePointerCapture() and set
handled = true for both actions so the guest handlers (keyboard?.onKeyEvent /
keyboard?.onVirtualKeyEvent) never receive the stray 'Z' press.
In `@app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java`:
- Around line 45-55: refreshPointerCapture currently clears the persistent flag
pointerCaptureRequested by calling setPointerCaptureRequested(false) when
temporarily releasing capture, which turns transient releases into permanent
opt-outs; change refreshPointerCapture so the branch that calls
disablePointerCapture() does NOT clear pointerCaptureRequested (remove or guard
the setPointerCaptureRequested(false) call) so that transient refreshes can be
restored later by subsequent refreshPointerCapture calls; leave
enablePointerCapture(), setPointerCaptureRequested(true) behavior unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d1aab3f4-7684-4c3f-bc3a-374aaadcf021
📒 Files selected for processing (3)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.ktapp/src/main/java/com/winlator/inputcontrols/InputCaptureManager.javaapp/src/main/java/com/winlator/widget/TouchpadView.java
There was a problem hiding this comment.
2 issues found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:1225">
P2: Ctrl+Shift+Alt+Z shortcut is consumed only on key-up, allowing key-down to propagate and potentially leaving guest input in an inconsistent state.</violation>
</file>
<file name="app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java">
<violation number="1" location="app/src/main/java/com/winlator/inputcontrols/InputCaptureManager.java:85">
P2: Inverted pointer-capture skip condition causes inconsistent state handling and redundant capture requests.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Description
Current implementation of the pointer capture does not take into account, that implicit requesting on every external mouse event is not desireable, since it locks your mouse to the GameNative's window on any type of a mouse event, not just mouse presses.
I have rebuilt it in a way, so that the exclusivity is requested on window focus regain (which can be reached through many means, but most importantly, explicit mouse click on the window), but it only happens if the exclusivity was lost through other actions than usage of special key combination (see below).
Also clean-up of this subsystem was moved to attached/detached lifecycle events.
Additionaly, the user is now able to request/release exclusivity via special key combination (Ctrl + Shift + Alt + , in this case it was binded to Z key). Especially helpful in the multi-window mode.
Recording
I will create a showcase in the oncoming days.
Checklist
#code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.CONTRIBUTING.md.Summary by cubic
Make pointer exclusivity predictable and user-controlled. Pointer capture no longer auto-locks on external mouse events;
InputCaptureManagernow owns state and lifecycle, and users can toggle with Ctrl+Shift+Alt+Z.New Features
Bug Fixes
InputCaptureManagerfor state-based control and focus handling.Written for commit f7573a7. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes