Skip to content

feat: Improve pointer exclusivity handling#1152

Draft
Unnvaldr wants to merge 1 commit intoutkarshdalal:masterfrom
Unnvaldr:feat-improve-pointer-exclusivity-handling
Draft

feat: Improve pointer exclusivity handling#1152
Unnvaldr wants to merge 1 commit intoutkarshdalal:masterfrom
Unnvaldr:feat-improve-pointer-exclusivity-handling

Conversation

@Unnvaldr
Copy link
Copy Markdown
Contributor

@Unnvaldr Unnvaldr commented Apr 8, 2026

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

  • If I have access to #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.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Make pointer exclusivity predictable and user-controlled. It no longer auto-locks on every external mouse event, and users can toggle lock with Ctrl+Shift+Alt+Z.

  • New Features

    • Add Ctrl+Shift+Alt+Z to toggle pointer exclusivity in the game view.
    • Quick menu: releases exclusivity on open and re-evaluates on close.
  • Bug Fixes

    • Stop implicit capture on any external mouse event; now request on window focus regain only if the user previously wanted exclusivity.
    • Respect user intent: if the user releases via the shortcut, we don’t auto-recapture on focus.
    • Manage capture via lifecycle: request on attach, release on detach; remove manual release on exit.

Written for commit cb7f036. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Keyboard shortcut (Ctrl+Shift+Alt+Z) added to toggle pointer exclusive mode for enhanced touchpad control during gameplay.
    • Pointer capture behavior now automatically adjusts based on quick menu visibility.
  • Bug Fixes

    • Enhanced pointer exclusivity state management and lifecycle handling to ensure more reliable and consistent cursor control behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

This PR refactors pointer capture lifecycle management in TouchpadView by introducing new public methods for controlling pointer exclusivity. XServerScreen is updated to leverage these methods when managing the quick menu overlay and to support keyboard-triggered pointer exclusivity toggling.

Changes

Cohort / File(s) Summary
Pointer Capture Lifecycle
app/src/main/java/com/winlator/widget/TouchpadView.java
Introduced determinePointerExclusivity(), togglePointerExclusive(), requestPointerExclusive(), and releasePointerExclusive() methods. Changed pointerCaptureRequested initialization to true and updated onWindowFocusChanged() to call the new determine method. Added capture/release hooks to view lifecycle via onAttachedToWindow() and onDetachedFromWindow().
Keyboard and Menu Integration
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Added pointer exclusivity toggling tied to quick menu overlay state: releasePointerExclusive() when menu shows and determinePointerExclusivity() when dismissed. Introduced keyboard shortcut (Ctrl+Shift+Alt+Z) to toggle exclusivity. Refactored keyboard event routing to check both virtual and non-virtual keyboard devices.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant XServerScreen
    participant TouchpadView
    participant Android as Android OS

    User->>XServerScreen: Press Ctrl+Shift+Alt+Z
    XServerScreen->>TouchpadView: togglePointerExclusive()
    TouchpadView->>Android: requestPointerCapture() or releasePointerCapture()
    Android-->>TouchpadView: Capture state updated
    TouchpadView-->>XServerScreen: Acknowledgment

    User->>XServerScreen: Dismiss Quick Menu
    XServerScreen->>TouchpadView: determinePointerExclusivity()
    TouchpadView->>Android: requestPointerCapture() (if focused and needed)
    Android-->>TouchpadView: Capture granted/confirmed
    
    User->>XServerScreen: Show Quick Menu via gameBack()
    XServerScreen->>TouchpadView: releasePointerExclusive()
    TouchpadView->>Android: releasePointerCapture()
    Android-->>TouchpadView: Capture released
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

  • PR #626 — Directly modifies TouchpadView's pointer-capture logic and pointerCaptureRequested field initialization, sharing the same refactored lifecycle approach.
  • PR #705 — Changes pointer-capture release behavior in XServerScreen.kt exit flow, contrasting with this PR's removal of explicit capture release during shutdown.
  • PR #166 — Modifies both TouchpadView and XServerScreen to add pointer/capture and input APIs, overlapping with the same classes and control flow changes.

Poem

🐰 A keyboard shortcut swift and true,
Ctrl+Shift+Alt+Z makes pointer break through,
TouchpadView dances through lifecycle's arc,
Capturing exclusivity from light to dark!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: improving pointer exclusivity handling, which is the core focus of the entire changeset.
Description check ✅ Passed The description covers what changed and why, discusses implementation details, notes the special key combination feature, and mentions lifecycle changes. However, the recording section is incomplete ('I will create a showcase in the oncoming days'), which is a required checklist item.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 55c0796 and cb7f036.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/com/winlator/widget/TouchpadView.java

Comment on lines +1101 to +1108
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?.togglePointerExclusive()
handled = true
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The Ctrl+Shift+Alt+Z toggle currently leaks input and won't stay off.

Only KEYCODE_Z ACTION_UP is intercepted here, so the guest still sees the Z press but never the matching release. Separately, tryCapturePointer() at Lines 665-670 still calls requestPointerCapture() directly, so the next mouse/touchpad motion can reacquire capture immediately after the user toggles it off. Consume the whole chord and route the remaining raw pointer-capture calls through TouchpadView's helper methods.

🤖 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 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.

Copy link
Copy Markdown
Contributor Author

@Unnvaldr Unnvaldr Apr 8, 2026

Choose a reason for hiding this comment

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

tryCapturePointer will be addressed in the oncoming days.

Comment on lines +150 to +209
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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not add a boolean toggle to tryCapturePointer instead?

@Unnvaldr Unnvaldr marked this pull request as draft April 11, 2026 23:39
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