mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Keyboard: add the keys the Android IME does not have
The emulated keyboard reaches games now, and the first thing it was used for was a game's debug menu -- which opened on Space and then could not be navigated, because a soft keyboard is built for typing text and has no arrows, no Escape and no function row. For that job those are not optional extras, they are the whole interaction. A row of them floats above the IME whenever the keyboard is up: Esc, Tab, the four arrows, Enter, and an Fn toggle for F1-F12. It scrolls sideways so nothing is cut off on a narrow screen, and it appears and disappears with the keyboard, so there is nothing to place in the touch layout and nothing to find. This adds to the IME rather than replacing it. Prediction, swipe and non-Latin input still come from whichever keyboard the user chose; only the keys that keyboard cannot express come from here. Taps go through SoftKeyboard.tap, the same paced queue the IME's own keys use, so a press is held long enough for the guest to sample it. The keys use tap gesture detection rather than clickable(): this sits beside a focused IME sink, and anything focusable here can take focus off it and drop the keyboard mid-use. The arrows are KEYCODE_DPAD_*, which the handler already maps to Qt's arrow codes, so nothing was needed on the native side.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package com.armsx2.input
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
|
||||
/**
|
||||
* The keys the Android IME does not have, floated above it.
|
||||
*
|
||||
* The emulated keyboard works, but a soft keyboard is built for typing text and has no
|
||||
* arrows, no Escape and no function row -- so the first thing it was used for, a game's
|
||||
* debug menu, opened and then could not be navigated. Those keys are not optional extras
|
||||
* for that job; they are the whole interaction.
|
||||
*
|
||||
* This does not replace the IME. Prediction, swipe and non-Latin input all still come from
|
||||
* whichever keyboard the user has chosen, and this only adds what that keyboard cannot
|
||||
* express. It appears and disappears with [SoftKeyboard.visible], so there is nothing to
|
||||
* place in the touch layout and nothing to discover.
|
||||
*
|
||||
* Taps go through [SoftKeyboard.tap], the same paced queue the IME's own keys use, so a
|
||||
* press is held long enough for the guest to sample it (see KEY_STEP_MS -- a press and
|
||||
* release issued back to back can land entirely between two guest polls and be missed).
|
||||
*
|
||||
* Gestures rather than clickable(): this sits next to a focused IME sink, and anything
|
||||
* focusable here can take focus off it and drop the keyboard mid-use. detectTapGestures
|
||||
* never touches focus.
|
||||
*/
|
||||
private data class ExtraKey(val label: String, val code: Int, val wide: Boolean = false)
|
||||
|
||||
private val BASE_KEYS = listOf(
|
||||
ExtraKey("Esc", KeyEvent.KEYCODE_ESCAPE),
|
||||
ExtraKey("Tab", KeyEvent.KEYCODE_TAB),
|
||||
ExtraKey("←", KeyEvent.KEYCODE_DPAD_LEFT),
|
||||
ExtraKey("↑", KeyEvent.KEYCODE_DPAD_UP),
|
||||
ExtraKey("↓", KeyEvent.KEYCODE_DPAD_DOWN),
|
||||
ExtraKey("→", KeyEvent.KEYCODE_DPAD_RIGHT),
|
||||
ExtraKey("⏎", KeyEvent.KEYCODE_ENTER),
|
||||
)
|
||||
|
||||
// KEYCODE_F1..F12 are contiguous, the same way the handler's qt mapping assumes.
|
||||
private val FN_KEYS = (0..11).map { ExtraKey("F${it + 1}", KeyEvent.KEYCODE_F1 + it) }
|
||||
|
||||
@Composable
|
||||
fun BoxScope.KeyboardExtraKeys() {
|
||||
if (!SoftKeyboard.visible.value) return
|
||||
|
||||
var showFn by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.92f),
|
||||
shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
// imePadding lifts it clear of the keyboard; navigationBarsPadding keeps it off
|
||||
// the gesture bar on the frames where the IME is animating out.
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
for (key in if (showFn) FN_KEYS else BASE_KEYS) {
|
||||
KeyCap(key.label) { SoftKeyboard.tap(key.code) }
|
||||
}
|
||||
|
||||
KeyCap(if (showFn) "abc" else "Fn", accent = true) { showFn = !showFn }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyCap(label: String, accent: Boolean = false, onTap: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.sizeIn(minWidth = 44.dp)
|
||||
.heightIn(min = 40.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
if (accent) MaterialTheme.colorScheme.primaryContainer
|
||||
else MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
.pointerInput(label) { detectTapGestures { onTap() } }
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
color = if (accent) MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.armsx2.ui
|
||||
|
||||
import com.armsx2.input.KeyboardExtraKeys
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -130,6 +131,12 @@ object WindowImpl {
|
||||
com.armsx2.ui.touch.TouchControlsOverlay()
|
||||
}
|
||||
|
||||
// The keys the IME does not have (arrows, Esc, Tab, function row), shown only
|
||||
// while the emulated keyboard is up. Outside the density override above: this
|
||||
// is normal UI and should scale with the UI scale setting, unlike the touch
|
||||
// controls, whose size comes from the user's own layout.
|
||||
KeyboardExtraKeys()
|
||||
|
||||
if (showLibrary.value && MainActivityRuntime.eState.value == EmuState.RUNNING && !overlayVisible.value) {
|
||||
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.56f))) {
|
||||
com.armsx2.navigation.AppNavigation()
|
||||
|
||||
Reference in New Issue
Block a user