d6e438a541
build / build (push) Successful in 2m13s
Stale GL_UNPACK state in gl4es and a missing pre-release GPU sync caused diagonal-banded corruption in the 3D view. Synthetic key press/release must track the real controller button or touch down/up edges, not fire together in one call - fixes a typed movement key sticking forever, then a follow-up that had silently broken A-button jump and on-screen keyboard arrow keys. Also adds force-rebuild.sh for the known stageEngine stale-cache issue.
546 lines
25 KiB
Java
546 lines
25 KiB
Java
package de.ladkau.questshock;
|
|
|
|
import android.app.Activity;
|
|
import android.util.TypedValue;
|
|
import android.view.Gravity;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.Button;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import org.libsdl.app.SDLActivity;
|
|
|
|
/**
|
|
* The on-screen keyboard quad (see OverlayPanel for the shared off-screen-
|
|
* render/touch/cursor plumbing this builds on) - a hand-built, full
|
|
* US-layout key grid (there's no system IME to borrow once immersive) plus
|
|
* a text field echoing what's been sent. Every key that reaches the game
|
|
* (everything but Close/Shift/Ctrl/Alt, which are local UI actions - see
|
|
* handleKeyDown()/handleKeyUp() and handleCharKeyDown()/handleCharKeyUp())
|
|
* is driven by that key button's own ACTION_DOWN/ACTION_UP (see
|
|
* charKeyTouchListener()/controlKeyTouchListener()), which
|
|
* OverlayPanel.handleTouch() dispatches for exactly as long as the
|
|
* controller's trigger is physically held while aimed at it - so a key
|
|
* held on this keyboard behaves like a real key held on a physical one,
|
|
* not an instant tap. Non-printable keys go through an
|
|
* SDLActivity.onNativeKeyDown()/onNativeKeyUp() pair, the same one a
|
|
* physical Bluetooth keyboard's presses already drive; printable keys go
|
|
* through a synthesized SDL_TEXTINPUT/SDL_KEYUP pair pushed from native
|
|
* code (see nativeSendPrintableCharDown()/Up() in questshock_native.c) -
|
|
* except while a Ctrl/Alt "armed" modifier is active, when they go through
|
|
* the same onNativeKeyDown()/onNativeKeyUp() path instead, so the modifier
|
|
* carries across (see toggleModifier()). Shift is a purely local layer
|
|
* toggle (uppercase/symbols vs lowercase/numbers), never itself sent to the
|
|
* game.
|
|
*
|
|
* This is a fully independent overlay quad from MenuOverlay's small
|
|
* launcher - opened via that launcher's "Keyboard" button
|
|
* (nativeShowKeyboard()) but otherwise self-contained: once open, the
|
|
* controller's menu button (which only ever affects the menu launcher, see
|
|
* xr_input.c) doesn't hide it, so it stays up as a standing input panel
|
|
* while actually playing rather than a modal you open and close. The only
|
|
* way to hide it is its own title bar's Close button (buildTitleBar()),
|
|
* which also doubles as a drag handle for repositioning it (handled
|
|
* entirely on the native side - see xr_input.c - since it never reaches
|
|
* this class's own touch dispatch). contentAlpha() renders the whole panel
|
|
* semi-transparent so whatever's behind it stays visible while it's up.
|
|
*/
|
|
public class KeyboardOverlay extends OverlayPanel {
|
|
// Matches xr_session.c's KEYBOARD_WIDTH/KEYBOARD_HEIGHT swapchain size -
|
|
// fixed, not tied to any real display metric, since this tree is never
|
|
// shown in a real window.
|
|
private static final int WIDTH = 1024;
|
|
private static final int HEIGHT = 768;
|
|
|
|
private static final int TYPED_TEXT_HEIGHT = 100;
|
|
|
|
// Height of the title bar (buildTitleBar()) and width of its Close
|
|
// button - xr_input.c's TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION
|
|
// must be kept in sync with these (as fractions of WIDTH/HEIGHT above),
|
|
// since the drag-handle-vs-Close-button-vs-rest-of-the-keyboard
|
|
// decision happens entirely on the native side, before a hit ever
|
|
// reaches this class's own touch dispatch.
|
|
private static final int TITLE_BAR_HEIGHT = 90;
|
|
private static final int CLOSE_BUTTON_WIDTH = 160;
|
|
|
|
// ~70% opaque - readable but lets whatever's behind the panel (the game
|
|
// quad, or empty space) show through while it's left up during play.
|
|
private static final int PANEL_ALPHA = 180;
|
|
|
|
private static KeyboardOverlay instance;
|
|
|
|
private TextView typedTextView;
|
|
private final StringBuilder typedText = new StringBuilder();
|
|
|
|
// Shift is a local-only layer toggle (never itself sent to the game -
|
|
// see updateShiftVisual()); Ctrl/Alt are one-shot "armed" modifiers,
|
|
// sent to the game the moment they're pressed and released again as
|
|
// soon as the next key consumes them (see toggleModifier()/
|
|
// releaseArmedModifiers()) - there's no press-and-hold gesture with a
|
|
// laser pointer + trigger click, so armed-then-consumed stands in for
|
|
// holding the key down.
|
|
private boolean shiftActive = false;
|
|
private boolean ctrlArmed = false;
|
|
private boolean altArmed = false;
|
|
private Button shiftButton;
|
|
private Button ctrlButton;
|
|
private Button altButton;
|
|
|
|
// Every CharKey created by addRow(), so updateShiftVisual() can relabel
|
|
// all of them at once when Shift toggles. Assigned (not a field
|
|
// initializer) at the top of buildContent() - see OverlayPanel's
|
|
// constructor note on why a field a subclass's buildContent() depends
|
|
// on can't rely on normal field-initializer timing.
|
|
private List<CharKey> charKeys;
|
|
|
|
// A key whose printed character (and, while unmodified, whose game
|
|
// input) depends on the Shift layer - covers letters, digits, and
|
|
// punctuation uniformly (e.g. 'q'/'Q', '1'/'!', '-'/'_'). androidKeyCode
|
|
// is only used for the Ctrl/Alt-combo path (see handleCharKeyDown()) -
|
|
// the unmodified path goes through nativeSendPrintableCharDown()/Up()
|
|
// instead, which doesn't need an Android keycode at all.
|
|
private static final class CharKey {
|
|
final char base;
|
|
final char shifted;
|
|
final int androidKeyCode;
|
|
Button button;
|
|
|
|
CharKey(char base, char shifted, int androidKeyCode) {
|
|
this.base = base;
|
|
this.shifted = shifted;
|
|
this.androidKeyCode = androidKeyCode;
|
|
}
|
|
}
|
|
|
|
private static CharKey charKey(char base, char shifted, int androidKeyCode) {
|
|
return new CharKey(base, shifted, androidKeyCode);
|
|
}
|
|
|
|
// Space is a CharKey like any other (see addRow()), but a literal
|
|
// space character makes for an unreadable, blank-looking button - keep
|
|
// showing the word "Space" instead, same as before it became a CharKey.
|
|
private static String charKeyLabel(CharKey key, boolean shiftActive) {
|
|
if (key.base == ' ') {
|
|
return "Space";
|
|
}
|
|
return String.valueOf(shiftActive ? key.shifted : key.base);
|
|
}
|
|
|
|
private KeyboardOverlay(Activity activity) {
|
|
super(activity, WIDTH, HEIGHT);
|
|
}
|
|
|
|
@Override
|
|
protected int contentAlpha() {
|
|
return PANEL_ALPHA;
|
|
}
|
|
|
|
@Override
|
|
protected View buildContent() {
|
|
charKeys = new ArrayList<>();
|
|
|
|
LinearLayout panel = new LinearLayout(activity);
|
|
panel.setOrientation(LinearLayout.VERTICAL);
|
|
panel.setBackgroundColor(0xFF202020);
|
|
|
|
panel.addView(buildTitleBar(),
|
|
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, TITLE_BAR_HEIGHT));
|
|
|
|
typedTextView = new TextView(activity);
|
|
typedTextView.setTextColor(0xFFFFFFFF);
|
|
typedTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 28);
|
|
typedTextView.setBackgroundColor(0xFF303030);
|
|
typedTextView.setPadding(20, 20, 20, 20);
|
|
panel.addView(typedTextView, new LinearLayout.LayoutParams(
|
|
ViewGroup.LayoutParams.MATCH_PARENT, TYPED_TEXT_HEIGHT));
|
|
|
|
// Rows follow real keyboard geometry where it helps (Tab starting
|
|
// the QWERTY row, Enter ending the home row, Shift starting the
|
|
// bottom-letter row, a compact arrow cluster bottom-right) so the
|
|
// layout reads as a familiar keyboard rather than an arbitrary grid.
|
|
addRow(panel, "Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10",
|
|
"F11", "F12", "Back");
|
|
addRow(panel,
|
|
charKey('`', '~', KeyEvent.KEYCODE_GRAVE),
|
|
charKey('1', '!', KeyEvent.KEYCODE_1),
|
|
charKey('2', '@', KeyEvent.KEYCODE_2),
|
|
charKey('3', '#', KeyEvent.KEYCODE_3),
|
|
charKey('4', '$', KeyEvent.KEYCODE_4),
|
|
charKey('5', '%', KeyEvent.KEYCODE_5),
|
|
charKey('6', '^', KeyEvent.KEYCODE_6),
|
|
charKey('7', '&', KeyEvent.KEYCODE_7),
|
|
charKey('8', '*', KeyEvent.KEYCODE_8),
|
|
charKey('9', '(', KeyEvent.KEYCODE_9),
|
|
charKey('0', ')', KeyEvent.KEYCODE_0),
|
|
charKey('-', '_', KeyEvent.KEYCODE_MINUS),
|
|
charKey('=', '+', KeyEvent.KEYCODE_EQUALS));
|
|
addRow(panel, "Tab",
|
|
charKey('q', 'Q', KeyEvent.KEYCODE_Q), charKey('w', 'W', KeyEvent.KEYCODE_W),
|
|
charKey('e', 'E', KeyEvent.KEYCODE_E), charKey('r', 'R', KeyEvent.KEYCODE_R),
|
|
charKey('t', 'T', KeyEvent.KEYCODE_T), charKey('y', 'Y', KeyEvent.KEYCODE_Y),
|
|
charKey('u', 'U', KeyEvent.KEYCODE_U), charKey('i', 'I', KeyEvent.KEYCODE_I),
|
|
charKey('o', 'O', KeyEvent.KEYCODE_O), charKey('p', 'P', KeyEvent.KEYCODE_P),
|
|
charKey('[', '{', KeyEvent.KEYCODE_LEFT_BRACKET),
|
|
charKey(']', '}', KeyEvent.KEYCODE_RIGHT_BRACKET),
|
|
charKey('\\', '|', KeyEvent.KEYCODE_BACKSLASH));
|
|
addRow(panel, "Ctrl",
|
|
charKey('a', 'A', KeyEvent.KEYCODE_A), charKey('s', 'S', KeyEvent.KEYCODE_S),
|
|
charKey('d', 'D', KeyEvent.KEYCODE_D), charKey('f', 'F', KeyEvent.KEYCODE_F),
|
|
charKey('g', 'G', KeyEvent.KEYCODE_G), charKey('h', 'H', KeyEvent.KEYCODE_H),
|
|
charKey('j', 'J', KeyEvent.KEYCODE_J), charKey('k', 'K', KeyEvent.KEYCODE_K),
|
|
charKey('l', 'L', KeyEvent.KEYCODE_L),
|
|
charKey(';', ':', KeyEvent.KEYCODE_SEMICOLON),
|
|
charKey('\'', '"', KeyEvent.KEYCODE_APOSTROPHE),
|
|
"Enter");
|
|
addRow(panel, "Shift",
|
|
charKey('z', 'Z', KeyEvent.KEYCODE_Z), charKey('x', 'X', KeyEvent.KEYCODE_X),
|
|
charKey('c', 'C', KeyEvent.KEYCODE_C), charKey('v', 'V', KeyEvent.KEYCODE_V),
|
|
charKey('b', 'B', KeyEvent.KEYCODE_B), charKey('n', 'N', KeyEvent.KEYCODE_N),
|
|
charKey('m', 'M', KeyEvent.KEYCODE_M),
|
|
charKey(',', '<', KeyEvent.KEYCODE_COMMA),
|
|
charKey('.', '>', KeyEvent.KEYCODE_PERIOD),
|
|
charKey('/', '?', KeyEvent.KEYCODE_SLASH),
|
|
"Up");
|
|
addRow(panel, "Alt", charKey(' ', ' ', KeyEvent.KEYCODE_SPACE), "Left", "Down", "Right");
|
|
return panel;
|
|
}
|
|
|
|
// The title bar: a decorative "drag here" label (not itself
|
|
// interactive - see the class doc, dragging is handled entirely by
|
|
// xr_input.c before a hit ever reaches here) plus a real Close button,
|
|
// which is dispatched as an ordinary click like any other key. Its
|
|
// height and the Close button's width must stay in sync with
|
|
// xr_input.c's TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION.
|
|
private LinearLayout buildTitleBar() {
|
|
LinearLayout bar = new LinearLayout(activity);
|
|
bar.setOrientation(LinearLayout.HORIZONTAL);
|
|
bar.setBackgroundColor(0xFF3A3A3A);
|
|
|
|
TextView label = new TextView(activity);
|
|
label.setText("Keyboard - drag here to move");
|
|
label.setTextColor(0xFFFFFFFF);
|
|
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22);
|
|
label.setGravity(Gravity.CENTER_VERTICAL);
|
|
label.setPadding(24, 0, 0, 0);
|
|
bar.addView(label, new LinearLayout.LayoutParams(
|
|
0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
|
|
|
Button close = new Button(activity);
|
|
close.setText("Close");
|
|
close.setOnClickListener(v -> nativeRequestClose());
|
|
close.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
|
bar.addView(close,
|
|
new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
|
|
return bar;
|
|
}
|
|
|
|
// Each row fills the panel's remaining height evenly (weight 1 on the
|
|
// row itself). Accepts a mix of String (a fixed-action control key,
|
|
// handled by handleKeyDown()/handleKeyUp()) and CharKey (a
|
|
// Shift-sensitive character key, handled by handleCharKeyDown()/
|
|
// handleCharKeyUp()) per item, so a row can match real keyboard
|
|
// geometry (e.g. "Tab" followed by a run of CharKeys).
|
|
// Within a row, CharKeys and most control keys share width equally
|
|
// except Space (wider, like a real keyboard) and Esc/Back/Enter/Tab/
|
|
// Shift/Ctrl/Alt (narrower, see keyWeight()).
|
|
private void addRow(LinearLayout panel, Object... items) {
|
|
LinearLayout row = new LinearLayout(activity);
|
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
|
for (Object item : items) {
|
|
Button key = new Button(activity);
|
|
float weight;
|
|
if (item instanceof CharKey) {
|
|
CharKey charKey = (CharKey) item;
|
|
charKey.button = key;
|
|
key.setText(charKeyLabel(charKey, shiftActive));
|
|
key.setOnTouchListener(charKeyTouchListener(charKey));
|
|
charKeys.add(charKey);
|
|
// Space is a CharKey too (base == shifted == ' '), but keeps
|
|
// its traditional wide key like the other control keys do.
|
|
weight = (charKey.base == ' ') ? 4f : 1f;
|
|
} else {
|
|
String label = (String) item;
|
|
key.setText(label);
|
|
weight = keyWeight(label);
|
|
// Shift/Ctrl/Alt are local UI toggles with their own
|
|
// instantaneous press/release semantics (see
|
|
// toggleModifier()), not a value forwarded to the game for
|
|
// as long as the key is held - a plain click is enough.
|
|
// Every other control key reaches the game and needs real
|
|
// hold-duration semantics (see controlKeyTouchListener()).
|
|
if (label.equals("Shift") || label.equals("Ctrl") || label.equals("Alt")) {
|
|
key.setOnClickListener(v -> handleToggleKey(label));
|
|
key.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
|
} else {
|
|
key.setOnTouchListener(controlKeyTouchListener(label));
|
|
}
|
|
if (label.equals("Shift")) {
|
|
shiftButton = key;
|
|
} else if (label.equals("Ctrl")) {
|
|
ctrlButton = key;
|
|
} else if (label.equals("Alt")) {
|
|
altButton = key;
|
|
}
|
|
}
|
|
row.addView(key, new LinearLayout.LayoutParams(
|
|
0, ViewGroup.LayoutParams.MATCH_PARENT, weight));
|
|
}
|
|
panel.addView(row, new LinearLayout.LayoutParams(
|
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
|
}
|
|
|
|
// Drives a control key (everything controlKeyCode() maps, i.e. every
|
|
// key in addRow() except CharKeys and the Shift/Ctrl/Alt toggles) from
|
|
// its button's own touch lifecycle, so it reaches the game for exactly
|
|
// as long as the controller's trigger is held on it (see the class
|
|
// doc). ACTION_CANCEL (the ray straying off the button, or another
|
|
// overlay claiming it - see xr_input.c) releases the key the same as
|
|
// ACTION_UP, so a key can never be left stuck down.
|
|
private View.OnTouchListener controlKeyTouchListener(String label) {
|
|
return (v, event) -> {
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
v.setPressed(true);
|
|
handleKeyDown(label);
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
v.setPressed(false);
|
|
handleKeyUp(label);
|
|
break;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
v.setPressed(false);
|
|
handleKeyUp(label);
|
|
break;
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
// Same as controlKeyTouchListener() above, for a CharKey.
|
|
private View.OnTouchListener charKeyTouchListener(CharKey key) {
|
|
return (v, event) -> {
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
v.setPressed(true);
|
|
handleCharKeyDown(key);
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
v.setPressed(false);
|
|
handleCharKeyUp(key);
|
|
break;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
v.setPressed(false);
|
|
handleCharKeyUp(key);
|
|
break;
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
private static float keyWeight(String label) {
|
|
if (label.equals("Back") || label.equals("Esc") || label.equals("Enter")
|
|
|| label.equals("Tab") || label.equals("Shift") || label.equals("Ctrl")
|
|
|| label.equals("Alt")) {
|
|
return 1.5f;
|
|
}
|
|
return 1f;
|
|
}
|
|
|
|
// Maps a control key's label to the Android keycode forwarded via
|
|
// SDLActivity.onNativeKeyDown()/onNativeKeyUp() - the same pair a
|
|
// physical Bluetooth keyboard's key events already drive (see
|
|
// SDLActivity.handleKeyEvent()). F-keys are contiguous in
|
|
// android.view.KeyEvent (KEYCODE_F1..KEYCODE_F12), so "F1".."F12" are
|
|
// computed rather than listed individually.
|
|
private static int controlKeyCode(String label) {
|
|
switch (label) {
|
|
case "Back": return KeyEvent.KEYCODE_DEL;
|
|
case "Esc": return KeyEvent.KEYCODE_ESCAPE;
|
|
case "Enter": return KeyEvent.KEYCODE_ENTER;
|
|
case "Tab": return KeyEvent.KEYCODE_TAB;
|
|
case "Up": return KeyEvent.KEYCODE_DPAD_UP;
|
|
case "Down": return KeyEvent.KEYCODE_DPAD_DOWN;
|
|
case "Left": return KeyEvent.KEYCODE_DPAD_LEFT;
|
|
case "Right": return KeyEvent.KEYCODE_DPAD_RIGHT;
|
|
default:
|
|
if (label.charAt(0) == 'F') {
|
|
int n = Integer.parseInt(label.substring(1));
|
|
return KeyEvent.KEYCODE_F1 + (n - 1);
|
|
}
|
|
throw new IllegalArgumentException("no keycode for " + label);
|
|
}
|
|
}
|
|
|
|
// Shift is a local layer toggle; Ctrl/Alt are one-shot armed modifiers
|
|
// (see toggleModifier()) - neither is forwarded to the game as a key of
|
|
// its own, so a plain click (rather than the hold-duration dispatch
|
|
// every other key below uses) is enough.
|
|
private void handleToggleKey(String label) {
|
|
if (label.equals("Shift")) {
|
|
shiftActive = !shiftActive;
|
|
updateShiftVisual();
|
|
return;
|
|
}
|
|
toggleModifier(label);
|
|
}
|
|
|
|
// Everything controlKeyCode() maps (Back/Esc/Enter/Tab/arrows/F1-F12)
|
|
// is non-printable, so it's forwarded as a real Android key event via
|
|
// that code - down on this key's own ACTION_DOWN, up on ACTION_UP/
|
|
// CANCEL (see controlKeyTouchListener()), the same down/up split a
|
|
// physical Bluetooth keyboard's own key events already carry. This all
|
|
// matters beyond just the visible typedTextView below: the engine's
|
|
// cutscene skip handler (cutsloop.c's cutscene_key_handler()) only
|
|
// reacts to Esc/Enter/Space, so those need to reach the game as actual
|
|
// game input, not just be echoed locally.
|
|
private void handleKeyDown(String label) {
|
|
int keyCode = controlKeyCode(label);
|
|
SDLActivity.onNativeKeyDown(keyCode);
|
|
|
|
if (label.equals("Back")) {
|
|
if (typedText.length() > 0) {
|
|
typedText.setLength(typedText.length() - 1);
|
|
}
|
|
} else {
|
|
typedText.append('[').append(label).append(']');
|
|
}
|
|
typedTextView.setText(typedText.toString());
|
|
}
|
|
|
|
private void handleKeyUp(String label) {
|
|
SDLActivity.onNativeKeyUp(controlKeyCode(label));
|
|
releaseArmedModifiers();
|
|
}
|
|
|
|
// A CharKey's printed character depends only on the Shift layer
|
|
// (updateShiftVisual() keeps its button's label in sync). Which game
|
|
// input it produces depends on whether a modifier is armed: unmodified,
|
|
// it goes through nativeSendPrintableCharDown()/Up() below like a
|
|
// normal typed character; with Ctrl/Alt armed, it instead goes through
|
|
// the same onNativeKeyDown()/onNativeKeyUp() path handleKeyDown()/
|
|
// handleKeyUp() use for control keys, since that's the only path that
|
|
// carries ev.key.keysym.mod through to the engine (sdl_events.c's
|
|
// pump_events() reads Ctrl/Alt only off that path, never off
|
|
// SDL_TEXTINPUT).
|
|
private void handleCharKeyDown(CharKey key) {
|
|
char c = shiftActive ? key.shifted : key.base;
|
|
if (ctrlArmed || altArmed) {
|
|
SDLActivity.onNativeKeyDown(key.androidKeyCode);
|
|
} else {
|
|
nativeSendPrintableCharDown(c);
|
|
}
|
|
typedText.append(c);
|
|
typedTextView.setText(typedText.toString());
|
|
}
|
|
|
|
private void handleCharKeyUp(CharKey key) {
|
|
char c = shiftActive ? key.shifted : key.base;
|
|
if (ctrlArmed || altArmed) {
|
|
SDLActivity.onNativeKeyUp(key.androidKeyCode);
|
|
releaseArmedModifiers();
|
|
} else {
|
|
nativeSendPrintableCharUp(c);
|
|
}
|
|
}
|
|
|
|
// Ctrl/Alt are "armed" rather than held: pressing one immediately sends
|
|
// its keydown (so SDL's own modifier tracking picks it up for whatever
|
|
// key comes next - see handleCharKeyDown()/handleKeyDown()) and
|
|
// brackets its label for feedback; pressing the same key again before
|
|
// it's been used cancels it (sends the matching keyup, un-brackets).
|
|
// The normal case - actually being consumed by the next key press - is
|
|
// handled by releaseArmedModifiers() below, not here.
|
|
private void toggleModifier(String label) {
|
|
boolean ctrl = label.equals("Ctrl");
|
|
Button button = ctrl ? ctrlButton : altButton;
|
|
int keyCode = ctrl ? KeyEvent.KEYCODE_CTRL_LEFT : KeyEvent.KEYCODE_ALT_LEFT;
|
|
boolean nowArmed = ctrl ? !ctrlArmed : !altArmed;
|
|
if (ctrl) {
|
|
ctrlArmed = nowArmed;
|
|
} else {
|
|
altArmed = nowArmed;
|
|
}
|
|
if (nowArmed) {
|
|
SDLActivity.onNativeKeyDown(keyCode);
|
|
} else {
|
|
SDLActivity.onNativeKeyUp(keyCode);
|
|
}
|
|
button.setText(nowArmed ? "[" + label + "]" : label);
|
|
}
|
|
|
|
// Called after any key actually reaches the game (a control key's
|
|
// handleKeyUp(), or a CharKey's handleCharKeyUp()) so an armed Ctrl/Alt
|
|
// only ever applies to the very next key, then releases - matching a
|
|
// real Ctrl/Alt+key combo's keyup once the combo is done.
|
|
private void releaseArmedModifiers() {
|
|
if (ctrlArmed) {
|
|
SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_CTRL_LEFT);
|
|
ctrlArmed = false;
|
|
ctrlButton.setText("Ctrl");
|
|
}
|
|
if (altArmed) {
|
|
SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_ALT_LEFT);
|
|
altArmed = false;
|
|
altButton.setText("Alt");
|
|
}
|
|
}
|
|
|
|
// Shift never itself reaches the game (see class doc) - it only flips
|
|
// which character each CharKey shows/sends, including its own label.
|
|
// Note this means a Ctrl+Shift+key chord won't carry Shift to the
|
|
// engine (Ctrl/Alt are real forwarded modifier keys, Shift here isn't) -
|
|
// not worth solving unless it actually comes up.
|
|
private void updateShiftVisual() {
|
|
shiftButton.setText(shiftActive ? "[Shift]" : "Shift");
|
|
for (CharKey key : charKeys) {
|
|
key.button.setText(charKeyLabel(key, shiftActive));
|
|
}
|
|
}
|
|
|
|
// Synthesizes the SDL_TEXTINPUT/SDL_KEYUP pair pump_events()
|
|
// (sdl_events.c) requires for printable characters (see
|
|
// handleCharKeyDown()/handleCharKeyUp() above), by building and
|
|
// pushing them directly in native code rather than through Android's
|
|
// IME (there's no real IME session behind this off-screen,
|
|
// never-attached grid for that plumbing to hook into).
|
|
private static native void nativeSendPrintableCharDown(char c);
|
|
|
|
private static native void nativeSendPrintableCharUp(char c);
|
|
|
|
// Hides this overlay's quad (xr_overlay_set_visible(..., false)) - the
|
|
// only way to do so, since the controller's menu button doesn't affect
|
|
// the keyboard (see this class's doc comment and xr_input.c).
|
|
private static native void nativeRequestClose();
|
|
|
|
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
|
|
// thread, right after it resolves this class via the activity's own
|
|
// ClassLoader (plain FindClass() can't see app classes from a thread
|
|
// that was attached to the JVM rather than spawned from Java).
|
|
public static void nativeInit(Activity activity) {
|
|
if (instance == null) {
|
|
instance = new KeyboardOverlay(activity);
|
|
}
|
|
}
|
|
|
|
public static byte[] nativeTakePixelsIfDirty() {
|
|
return instance == null ? null : instance.takePixelsIfDirty();
|
|
}
|
|
|
|
public static void nativeDispatchTouch(final float u, final float v, final boolean down) {
|
|
if (instance != null) {
|
|
instance.dispatchTouch(u, v, down);
|
|
}
|
|
}
|
|
|
|
public static void nativeUpdateCursor(final float u, final float v, final boolean visible) {
|
|
if (instance != null) {
|
|
instance.dispatchUpdateCursor(u, v, visible);
|
|
}
|
|
}
|
|
}
|