Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions core/src/main/java/emu/joric/KeyboardMatrix.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,38 @@ public abstract class KeyboardMatrix extends InputAdapter {
private TreeMap<Long, Integer> delayedReleaseKeys = new TreeMap<Long, Integer>();

private int lastKeyDownKeycode;


/**
* Sym/Meta-key (libGDX Keys.SYM) state. "Meta" is the DOM / cross-platform
* name for the Command key on Mac, the Windows key on Windows, and the
* Super key on Linux. libGDX's lwjgl3 backend maps all three to
* Keys.SYM. The Oric had no equivalent key, and Sym/Meta+key combinations
* are conventionally OS / application shortcuts on every modern
* platform, so we deliberately drop them at the matrix level: while
* a Sym/Meta key is held, non-Sym/non-Meta keyDowns are suppressed (so
* they never set a matrix bit).
*
* State is updated by:
* - lwjgl3 desktop: keyDown(Keys.SYM) / keyUp(Keys.SYM). The desktop
* backend correctly translates GLFW_KEY_LEFT/RIGHT_SUPER into
* Keys.SYM, so these paths fire naturally.
* - GWT/web: a DOM-level listener installed by GwtKeyboardMatrix
* that reads event.metaKey directly and funnels through the same
* setSymPressed entry point. Necessary because libGDX's GWT
* backend currently maps the Meta key to Keys.UNKNOWN (see
* DefaultGwtInput.java's `// FIXME` on KEY_LEFT_WINDOW_KEY), so
* the keyDown(Keys.SYM) path never fires in the browser.
*
* Note: on Android, libGDX's Keys.SYM actually corresponds to the
* historical Symbol key (a Function-style modifier on early hardware
* Android keyboards such as the HTC G1 and Motorola Droid), now very
* rare (essentially extinct?) hardware. Treating it in the same way as
* a Meta key seems logical: pressing Sym+A on such hardware would otherwise
* produce a stray A on the Oric (just like CMD+A on Mac would if we did
* not have this code to suppress it).
*/
private boolean symPressed;

/**
* Constructor for UserInput.
*/
Expand All @@ -168,8 +199,24 @@ public KeyboardMatrix() {
}

public boolean keyDown(int keycode) {
// Track Sym/Meta-key press. See the symPressed field comment for why.
// Keys.SYM is not in keyConvHashMap (the Oric has no Sym/Meta keys),
// so without this special-casing the early return below would skip
// the tracking. This path fires on the lwjgl3 desktop backend; the
// GWT backend delivers Keys.UNKNOWN instead, so for GWT we use
// a JSNI listener in GwtKeyboardMatrix to set this state.
if (keycode == Keys.SYM) {
setSymPressed(true);
return false;
}
// While the Sym/Meta key is held, suppress the keyDown so the matrix
// bit is never set in the first place.
if (symPressed) {
return false;
}

if (!keyConvHashMap.containsKey(keycode)) return false;

if (keycode == 0) {
// The framework wasn't able to identify the key, so we'll have to
// deduce it from the key typed character.
Expand All @@ -192,8 +239,15 @@ public boolean keyDown(int keycode) {
}

public boolean keyUp(int keycode) {
// Track Sym/Meta-key release. See keyDown's comment on why this
// special-case sits above the early return.
if (keycode == Keys.SYM) {
setSymPressed(false);
return false;
}

if (!keyConvHashMap.containsKey(keycode)) return false;

if (keycode != 0) {
long currentTime = TimeUtils.nanoTime();
long minKeyReleaseTime = minKeyReleaseTimes[keycode];
Expand Down Expand Up @@ -269,7 +323,20 @@ public void checkDelayedReleaseKeys() {
}
}

/**
* Sets the Sym/Meta-key pressed state. Funnel point for the two code
* paths that detect Sym/Meta changes: lwjgl3 desktop's keyDown/keyUp for
* Keys.SYM, and the GWT JSNI DOM listener reading event.metaKey.
*
* Visibility is protected so the GWT subclass's JSNI block can
* invoke it directly (JSNI ignores Java visibility but protected
* documents the intent).
*/
protected void setSymPressed(boolean pressed) {
symPressed = pressed;
}

public abstract int getKeyMatrixRow(int row);

public abstract void setKeyMatrixRow(int row, int value);
}
98 changes: 97 additions & 1 deletion html/src/main/java/emu/joric/gwt/GwtKeyboardMatrix.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public class GwtKeyboardMatrix extends KeyboardMatrix {
*/
public GwtKeyboardMatrix() {
keyMatrix = createKeyMatrixArray();
// Install a DOM-level Meta-key watcher. See
// registerMetaKeyWatcher's Javadoc for the libGDX GWT backend
// gaps it works around (Meta-key tracking and, on macOS,
// recovery from swallowed keyups). Only the UI-thread
// constructor calls this; the web-worker constructor below
// has no DOM to listen to.
registerMetaKeyWatcher();
}

/**
Expand All @@ -44,7 +51,96 @@ public native JavaScriptObject getSharedArrayBuffer()/*-{
var keyMatrix = this.@emu.joric.gwt.GwtKeyboardMatrix::keyMatrix;
return keyMatrix.buffer;
}-*/;


/**
* Installs DOM-level keydown/keyup listeners that work around two
* Meta-key issues in libGDX's GWT backend. The first applies on
* every OS; the second is macOS-specific:
*
* 1. The GWT backend maps the Meta key to Keys.UNKNOWN instead of
* Keys.SYM like the lwjgl3 desktop does (see the long-
* standing // FIXME in DefaultGwtInput.java). As a result
* KeyboardMatrix's keyUp/keyDown Keys.SYM-driven Meta tracking
* never fires on web. So here we read event.metaKey directly on
* every DOM-level keydown/keyup and funnel it to the base class
* via setSymPressed(boolean), driving the "suppress non-Meta
* keyDowns while the Meta key is held" logic that lives there.
*
* 2. macOS suppresses keyUp events for non-modifier keys whose
* keyUp would occur while a Meta (CMD) key is held. This is
* an OS-level behaviour inherited by Chrome, Safari and Firefox
* (open browser bugs dating back to 2011, still unresolved as of
* writing). The consequence in libGDX's GWT backend
* (DefaultGwtInput.java) is that its internal pressedKeys[]
* array gets stuck `true` for the un-released key. Typical
* symptom for Oric input behaviour: after CMD+X, X stays
* pressed, generating auto-repeat X inputs until the next plain
* X press.
*
* Listen with useCapture=true so this handler runs before
* libGDX's own document-level listener (which uses
* useCapture=false per DefaultGwtInput.hookEvents), ensuring the
* base class's symPressed flag is up to date by the time the
* libGDX-translated keyDown reaches the base class's suppress
* check.
*
* (Note that in the case where a non-Meta key is still held down
* when Meta is released, macOS appears to generate suitable key
* events so they effectively appear as new key presses without
* the Meta modifier. So we don't need any special code to handle
* that case.)
*/
private native void registerMetaKeyWatcher()/*-{
var self = this;
// DOM keyCodes of non-Meta keys we've seen keydown but not
// (yet) keyup for. Excludes the Meta key DOM codes (91, 92,
// 93 - values vary slightly across browsers and key
// locations - plus 224 for some Firefox variants).
var trackedKeys = {};

var update = function(e) {
var wasMetaPressed = self.@emu.joric.KeyboardMatrix::symPressed;
var isMetaPressed = !!e.metaKey;
self.@emu.joric.KeyboardMatrix::setSymPressed(Z)(isMetaPressed);
var code = e.keyCode;

// Track DOM press/release for non-Meta keys.
if (code !== 91 && code !== 92 && code !== 93 && code !== 224) {
if (e.type === 'keydown') {
trackedKeys[code] = true;
} else if (e.type === 'keyup') {
delete trackedKeys[code];
}
}

// Meta key just released. Synthesise keyup events for any
// still-tracked keys so libGDX's internal pressedKeys[]
// gate is cleared. Snapshot before iterating because the
// synthetic dispatch re-enters this handler (the keyup
// branch above) and would otherwise mutate trackedKeys
// mid-iteration.
if (wasMetaPressed && !isMetaPressed) {
var snapshot = [];
for (var k in trackedKeys) snapshot.push(k);
trackedKeys = {};
for (var i = 0; i < snapshot.length; i++) {
var kc = +snapshot[i];
// keyCode and which aren't part of KeyboardEvent's init dict,
// so passing them to the constructor has no effect. Override
// via Object.defineProperty after construction. libGDX's
// keyForCode reads keyCode via NativeEvent.getKeyCode().
var ke = new KeyboardEvent('keyup', { bubbles: true, cancelable: true });
Object.defineProperty(ke, 'keyCode', { value: kc });
Object.defineProperty(ke, 'which', { value: kc });
$doc.dispatchEvent(ke);
}
}
};

$doc.addEventListener('keydown', update, true);
$doc.addEventListener('keyup', update, true);
}-*/;

@Override
public int getKeyMatrixRow(int row) {
return keyMatrix.get(row);
Expand Down
Loading