Skip to content

feat: disable IME extract UI and centralize singleLine keyboard behavior#1014

Open
jeremybernstein wants to merge 1 commit intoutkarshdalal:masterfrom
jeremybernstein:jb/fix-textfield-ime
Open

feat: disable IME extract UI and centralize singleLine keyboard behavior#1014
jeremybernstein wants to merge 1 commit intoutkarshdalal:masterfrom
jeremybernstein:jb/fix-textfield-ime

Conversation

@jeremybernstein
Copy link
Copy Markdown
Contributor

@jeremybernstein jeremybernstein commented Mar 25, 2026

Description

adds NoExtractOutlinedTextField wrapper that sets IME_FLAG_NO_EXTRACT_UI, opting out of the IME's fullscreen extract editing mode. centralizes ImeAction.Done + clearFocus() for singleLine fields, removing per-callsite boilerplate. migrates all OutlinedTextField usages across the app (11 files).

closes #1013.

Recording

before:
https://github.com/user-attachments/assets/c195692d-3120-44bb-8221-a6fc94844d3e

after:
https://github.com/user-attachments/assets/b96dcd62-6adc-4b44-a26c-66b3969b2695

Test plan

  • Open any container config dialog, type in a text field, delete mid-word — no extract UI overlay
  • Single-line fields show "Done" IME action and dismiss keyboard on tap
  • Multi-line field (game feedback) still works normally without Done action

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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Introduces NoExtractOutlinedTextField, a composable wrapper that disables the IME extract UI and centralizes single-line IME handling; replaces existing OutlinedTextField usages across dialogs, settings, and login screens.

Changes

Cohort / File(s) Summary
New Component
app/src/main/java/app/gamenative/ui/component/NoExtractOutlinedTextField.kt
Added NoExtractOutlinedTextField which intercepts platform text input to OR EditorInfo.IME_FLAG_NO_EXTRACT_UI, maps ImeAction.DefaultImeAction.Done for single-line fields, and auto-clears focus on Done.
Dialog components
app/src/main/java/app/gamenative/ui/component/dialog/Box64PresetsDialog.kt, .../ControllerBindingDialog.kt, .../EnvironmentTab.kt, .../FEXCorePresetsDialog.kt, .../GameFeedbackDialog.kt, .../GeneralTab.kt, .../TouchGestureSettingsDialog.kt
Replaced OutlinedTextField with NoExtractOutlinedTextField, added singleLine = true where applicable; preserved value bindings, labels, icons/menus, and existing handlers.
Settings UI
app/src/main/java/app/gamenative/ui/component/settings/SettingsTextField.kt
Switched action slot input to NoExtractOutlinedTextField and set singleLine = true; kept modifier, focusRequester, value, and onChange logic.
Login screens
app/src/main/java/app/gamenative/ui/screen/login/TwoFactorAuthScreen.kt, app/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.kt
Migrated credential and 2FA inputs from OutlinedTextField to NoExtractOutlinedTextField; maintained filtering, visualTransformation, focus/keyboard handling (minor IME action adjustment in 2FA).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant IME
    participant Interceptor as InterceptPlatformTextInput
    participant TextField as NoExtractOutlinedTextField
    participant Focus as FocusManager

    User->>IME: type text / press IME action
    IME->>Interceptor: PlatformTextInputMethodRequest (EditorInfo)
    Interceptor-->>IME: modify EditorInfo (OR IME_FLAG_NO_EXTRACT_UI) and forward
    Interceptor->>TextField: deliver input events / committed text
    TextField->>User: render updated text
    alt IME action == Done (singleLine)
        TextField->>Focus: clearFocus()
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through code with ears alert and bright,
Turned fullscreen IME off, kept single-line light.
No extract UI, no stale-text fright,
Focus clears on Done — the typing feels right! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: disabling IME extract UI and centralizing singleLine keyboard behavior.
Linked Issues check ✅ Passed The PR fully addresses both objectives from issue #1013: implements NoExtractOutlinedTextField wrapper that sets IME_FLAG_NO_EXTRACT_UI and centralizes ImeAction.Done + clearFocus() behavior for singleLine fields.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: new wrapper component, migrations to use it with singleLine behavior, and necessary import updates across 11 files.
Description check ✅ Passed The PR description includes all required sections: a clear description of changes with issue reference, before/after video recordings, test plan, and all checklist items completed.

✏️ 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.

No issues found across 11 files

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.

🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/ui/component/settings/SettingsTextField.kt (1)

42-49: Consider parameterizing singleLine for flexibility.

The hardcoded singleLine = true works for most use cases (button labels, typical settings). However, this component is also used for environment variable values (per SettingsEnvVars.kt), which could theoretically contain longer or multi-part values.

Given the 76.dp width constraint already implies compact single-line usage, this is acceptable. If multi-line env var support is needed later, consider exposing singleLine as a parameter.

🤖 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/component/settings/SettingsTextField.kt`
around lines 42 - 49, The NoExtractOutlinedTextField usage hardcodes singleLine
= true; make this configurable by adding a singleLine: Boolean parameter to the
SettingsTextField composable (default true) and forward it into
NoExtractOutlinedTextField, then update call sites (e.g., SettingsEnvVars usage)
to pass singleLine = false where multi-line values may be needed; ensure default
preserves current behavior.
app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt (1)

69-91: Consider adding ImeAction.Next to navigate between width and height fields.

Both fields use singleLine = true without explicit imeAction, so pressing "Done" on the width field will clear focus instead of moving to the height field. Users can still tap the height field manually, but adding ImeAction.Next with focus navigation would improve the input flow.

♻️ Optional: Add focus navigation between width and height
+val heightFocusRequester = remember { FocusRequester() }
+
 NoExtractOutlinedTextField(
-    modifier = Modifier.width(128.dp),
+    modifier = Modifier.width(128.dp).focusRequester(widthFocusRequester),
     value = state.customScreenWidth.value,
     onValueChange = { state.customScreenWidth.value = it },
-    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+    keyboardOptions = KeyboardOptions(
+        keyboardType = KeyboardType.Number,
+        imeAction = ImeAction.Next,
+    ),
+    keyboardActions = KeyboardActions(
+        onNext = { heightFocusRequester.requestFocus() },
+    ),
     label = { Text(text = stringResource(R.string.width)) },
     singleLine = true,
 )
 // ... separator ...
 NoExtractOutlinedTextField(
-    modifier = Modifier.width(128.dp),
+    modifier = Modifier.width(128.dp).focusRequester(heightFocusRequester),
     value = state.customScreenHeight.value,
     // ... rest remains the same with singleLine = true for Done behavior
 )
🤖 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/component/dialog/GeneralTab.kt` around
lines 69 - 91, The width and height NoExtractOutlinedTextField inputs
(NoExtractOutlinedTextField using state.customScreenWidth.value and
state.customScreenHeight.value) lack an explicit IME action, so pressing "Done"
on the width field clears focus instead of moving to the height field; update
the first field's KeyboardOptions to include imeAction = ImeAction.Next and add
KeyboardActions (or a FocusRequester/LocalFocusManager) to move focus to the
height field on Next, and set the height field's imeAction appropriately (e.g.,
ImeAction.Done) so keyboard navigation flows from the width field to the height
field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt`:
- Around line 69-91: The width and height NoExtractOutlinedTextField inputs
(NoExtractOutlinedTextField using state.customScreenWidth.value and
state.customScreenHeight.value) lack an explicit IME action, so pressing "Done"
on the width field clears focus instead of moving to the height field; update
the first field's KeyboardOptions to include imeAction = ImeAction.Next and add
KeyboardActions (or a FocusRequester/LocalFocusManager) to move focus to the
height field on Next, and set the height field's imeAction appropriately (e.g.,
ImeAction.Done) so keyboard navigation flows from the width field to the height
field.

In `@app/src/main/java/app/gamenative/ui/component/settings/SettingsTextField.kt`:
- Around line 42-49: The NoExtractOutlinedTextField usage hardcodes singleLine =
true; make this configurable by adding a singleLine: Boolean parameter to the
SettingsTextField composable (default true) and forward it into
NoExtractOutlinedTextField, then update call sites (e.g., SettingsEnvVars usage)
to pass singleLine = false where multi-line values may be needed; ensure default
preserves current behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9daec99-4575-41e4-ab4f-3fa8b6c81e0a

📥 Commits

Reviewing files that changed from the base of the PR and between c7da303 and a9471d6.

📒 Files selected for processing (11)
  • app/src/main/java/app/gamenative/ui/component/NoExtractOutlinedTextField.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/Box64PresetsDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/ControllerBindingDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/EnvironmentTab.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/FEXCorePresetsDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameFeedbackDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/TouchGestureSettingsDialog.kt
  • app/src/main/java/app/gamenative/ui/component/settings/SettingsTextField.kt
  • app/src/main/java/app/gamenative/ui/screen/login/TwoFactorAuthScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/login/UserLoginScreen.kt

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

jeremybernstein commented Mar 25, 2026

🧹 Nitpick comments (2)

@coderabbitai

#1: ok
#2: YAGNI, not changing

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Tip

For best results, initiate chat on the files or code changes.

@jeremybernstein: Understood on both!

  1. GeneralTab.kt — glad the ImeAction.Next suggestion is useful; sounds good.
  2. SettingsTextField.kt — noted, YAGNI makes sense here. I'll remember not to suggest making singleLine configurable in SettingsTextField in future reviews.

✏️ Learnings added
Learnt from: jeremybernstein
URL: https://github.com/utkarshdalal/GameNative/pull/1014

Timestamp: 2026-03-25T10:11:17.504Z
Learning: In `app/src/main/java/app/gamenative/ui/component/settings/SettingsTextField.kt`, the `singleLine = true` hardcoded in the `NoExtractOutlinedTextField` usage is intentional (YAGNI). Do not suggest adding a `singleLine` parameter to `SettingsTextField` or making it configurable in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: unbelievableflavour
Repo: utkarshdalal/GameNative PR: 811
File: app/src/main/java/app/gamenative/ui/util/WindowSize.kt:38-44
Timestamp: 2026-03-11T11:17:48.473Z
Learning: In `app/src/main/java/app/gamenative/ui/util/WindowSize.kt`, `shouldShowGamepadUI()` reads `PrefManager.showGamepadHints` as a plain (non-reactive) property. This is intentional: when the user toggles the setting in the Settings screen and navigates back, the composable recomposes and picks up the updated value. Real-time reactivity (e.g., via StateFlow/collectAsState) is not required here. Do not flag this as an issue in future reviews.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 1005
File: app/src/main/java/app/gamenative/ui/component/dialog/PhysicalControllerConfigSection.kt:706-738
Timestamp: 2026-03-24T12:55:58.820Z
Learning: In `app/src/main/java/app/gamenative/ui/component/dialog/PhysicalControllerConfigSection.kt`, the `needsCopy` heuristic in `copyElementsIfNeeded` (around lines 706-738) treats any destination layout lacking `GAMEPAD_*` bindings as safe to overwrite with source elements, even if it contains non-GAMEPAD custom bindings. This imperfect heuristic is a pre-existing limitation acknowledged by the maintainer and intentionally out of scope. Do not flag this overwrite-without-merge behavior as an issue in future reviews of this file.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 1005
File: app/src/main/java/app/gamenative/ui/component/dialog/ControllerPresetManager.kt:54-166
Timestamp: 2026-03-24T12:55:21.360Z
Learning: In `app/src/main/java/app/gamenative/ui/component/dialog/ControllerPresetManager.kt`, the three hardcoded factory presets (`PRESET_DEFAULT`, `PRESET_FPS`, `PRESET_PLATFORMER`) with embedded English names are a known, pre-existing limitation. The lack of centralization with profile assets and the absence of localized display names are acknowledged but intentionally out of scope. Do not flag the hardcoded English preset names or the duplication between factory presets and seeded profile assets as issues in future reviews of this file.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 801
File: app/src/main/java/app/gamenative/utils/BestConfigService.kt:45-51
Timestamp: 2026-03-10T21:46:14.981Z
Learning: In `app/src/main/java/app/gamenative/utils/BestConfigService.kt`, the singleton mutable state pattern (`lastMissingComponents` / `consumeLastMissingComponents()`) — where `parseConfigToContainerData()` writes a global and callers read it via `consume*()` — is a pre-existing design. Refactoring it to return missing components as part of the parse result has a large blast radius and is intentionally deferred. Do not flag this pattern as an issue in future reviews of this file.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 801
File: app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt:174-188
Timestamp: 2026-03-10T22:12:03.164Z
Learning: In `app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`, the `applyConfigForContainer` function calls `installMissingComponentsForConfig` before `BestConfigService.parseConfigToContainerData`. This install-before-parse ordering is pre-existing behavior, not introduced by any recent PR. Do not flag this ordering as an issue in future reviews of this file.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 0
File: :0-0
Timestamp: 2026-03-10T22:13:54.453Z
Learning: In `app/src/main/java/app/gamenative/utils/BestConfigService.kt`, around lines 403-418, missing Box64/FEXCore preset failures (via `Box86_64PresetManager.getPreset("box64", ...)` and `FEXCorePresetManager.getPreset(...)`) are intentionally appended to the same `missingComponents` list as installable components, even though `resolveMissingManifestInstallRequests()` cannot install presets. This is a known limitation deferred for future work — the long-term direction is to serialize non-default presets into config files and auto-install them. Do not flag this mixing of installable vs. non-installable blockers in `missingComponents` as an issue in future reviews of this file.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 0
File: :0-0
Timestamp: 2026-03-18T16:30:26.610Z
Learning: In `app/src/main/java/app/gamenative/ui/PluviaMain.kt`, the three sites that call `MainActivity.setPendingLaunchRequest`, toggle `shownPendingLaunchSnackbar`, and conditionally call `SnackbarManager.show` (cold-start pending branch, ExternalGameLaunch pending branch, and non-terminal SteamDisconnected branch) are intentionally kept separate — the surrounding logic differs enough that extracting a shared helper would add more complexity than it removes. Do not suggest refactoring these into a helper in future reviews.

Learnt from: unbelievableflavour
Repo: utkarshdalal/GameNative PR: 810
File: app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt:391-417
Timestamp: 2026-03-18T08:52:17.299Z
Learning: In the `app/src/main/java/app/gamenative/ui/screen/library/appscreen/` AppScreen files (e.g., `BaseAppScreen.kt`, `CustomGameAppScreen.kt`), using `CoroutineScope(Dispatchers.IO).launch` inside `AppMenuOption` click handlers (such as for `FetchSteamGridDBImages` and `CreateShortcut`) is an established, intentional pattern. Do not flag this as a lifecycle-safety issue in future reviews of these files.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 0
File: :0-0
Timestamp: 2026-03-18T16:30:26.610Z
Learning: In `app/src/main/java/app/gamenative/MainActivity.kt`, `peekPendingLaunchRequest()` is an intentional non-destructive, synchronized read of `pendingLaunchRequest`. The peek + separate consume pattern (also present at line 436) is pre-existing and accepted; the worst-case outcome of a race is an unnecessary snackbar, which is benign. Do not flag `peekPendingLaunchRequest()` as a TOCTOU race issue in future reviews.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 801
File: app/src/main/java/app/gamenative/ui/util/ContainerConfigTransfer.kt:105-143
Timestamp: 2026-03-11T07:31:50.139Z
Learning: In `app/src/main/java/app/gamenative/ui/util/ContainerConfigTransfer.kt`, the force-apply path inside `BaseAppScreen.showMissingComponentsDialog`'s `onApplyAnyway` callback (re-parsing with `forceApply = true`, resolving manifest installs, and writing the container) is intentional and user-initiated. It only runs when the user explicitly taps "Apply anyway" in the missing-components dialog. Do not flag this as contradicting the reject-on-missing-components goal; the default is rejection and this is an explicit user override.

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.

Disable IME extract UI for Compose TextFields

1 participant