mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-08-24 22:31:30 -04:00
Fix arrow keys on safari + release keys and mouse on blur
This commit is contained in:
@@ -25,6 +25,11 @@ namespace gdjs {
|
||||
* if location is not specified.
|
||||
*/
|
||||
private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];
|
||||
|
||||
/**
|
||||
* The `KeyboardEvent.location` of keys on the numeric keypad.
|
||||
*/
|
||||
private static _NUMPAD_LOCATION: integer = 3;
|
||||
private _pressedKeys: Hashtable<boolean>;
|
||||
private _justPressedKeys: Hashtable<boolean>;
|
||||
private _releasedKeys: Hashtable<boolean>;
|
||||
@@ -111,11 +116,24 @@ namespace gdjs {
|
||||
*
|
||||
* @param keyCode The raw key code
|
||||
* @param location The location
|
||||
* @param code The KeyboardEvent.code, used to know if the key really is on
|
||||
* the numpad when a numpad location is reported.
|
||||
*/
|
||||
static getLocationAwareKeyCode(
|
||||
keyCode: number,
|
||||
location: number | null | undefined
|
||||
location: number | null | undefined,
|
||||
code?: string | null
|
||||
): integer {
|
||||
if (
|
||||
location === InputManager._NUMPAD_LOCATION &&
|
||||
code &&
|
||||
!code.startsWith('Numpad')
|
||||
) {
|
||||
// macOS sets the "numeric pad" modifier flag for the arrow keys, which makes
|
||||
// WebKit (Safari) report them with a numpad location. Only `code` tells the
|
||||
// numpad keys apart from the arrow keys, so trust it over the location.
|
||||
location = 0;
|
||||
}
|
||||
if (location) {
|
||||
// If it is a numpad number, do not modify it.
|
||||
if (96 <= keyCode && keyCode <= 105) {
|
||||
@@ -135,11 +153,13 @@ namespace gdjs {
|
||||
* 2 for right keys, and 3 for numpad keys.
|
||||
* @param keyCode The raw key code associated to the key press.
|
||||
* @param location The location of the event.
|
||||
* @param code The KeyboardEvent.code of the event.
|
||||
*/
|
||||
onKeyPressed(keyCode: number, location?: number): void {
|
||||
onKeyPressed(keyCode: number, location?: number, code?: string): void {
|
||||
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
|
||||
keyCode,
|
||||
location
|
||||
location,
|
||||
code
|
||||
);
|
||||
this._pressedKeys.put(locationAwareKeyCode, true);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, true);
|
||||
@@ -152,11 +172,13 @@ namespace gdjs {
|
||||
* 2 for right keys, and 3 for numpad keys.
|
||||
* @param keyCode The raw key code associated to the key release.
|
||||
* @param location The location of the event.
|
||||
* @param code The KeyboardEvent.code of the event.
|
||||
*/
|
||||
onKeyReleased(keyCode: number, location?: number): void {
|
||||
onKeyReleased(keyCode: number, location?: number, code?: string): void {
|
||||
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
|
||||
keyCode,
|
||||
location
|
||||
location,
|
||||
code
|
||||
);
|
||||
this._pressedKeys.put(locationAwareKeyCode, false);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, false);
|
||||
@@ -170,6 +192,11 @@ namespace gdjs {
|
||||
*/
|
||||
releaseAllPressedKeys(): void {
|
||||
for (const locationAwareKeyCode in this._pressedKeys.items) {
|
||||
// Keys keep an entry once released, so only the keys actually held down
|
||||
// must be made to go through the release state.
|
||||
if (!this._pressedKeys.items[locationAwareKeyCode]) {
|
||||
continue;
|
||||
}
|
||||
this._pressedKeys.put(locationAwareKeyCode, false);
|
||||
this._justPressedKeys.put(locationAwareKeyCode, false);
|
||||
this._releasedKeys.put(locationAwareKeyCode, true);
|
||||
@@ -427,6 +454,23 @@ namespace gdjs {
|
||||
this._releasedMouseButtons[buttonCode] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release all the mouse buttons that are currently pressed.
|
||||
*/
|
||||
releaseAllPressedMouseButtons(): void {
|
||||
for (
|
||||
let buttonCode = 0;
|
||||
buttonCode < this._pressedMouseButtons.length;
|
||||
buttonCode++
|
||||
) {
|
||||
if (this._pressedMouseButtons[buttonCode]) {
|
||||
// Go through `onMouseButtonReleased` so that the touch simulated by
|
||||
// the left button is ended too.
|
||||
this.onMouseButtonReleased(buttonCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the mouse button corresponding to buttonCode is pressed.
|
||||
* @param buttonCode The mouse button code (0: Left button, 1: Right button).
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace gdjs {
|
||||
|
||||
_wasDisposed: boolean = false;
|
||||
|
||||
_unregisterFocusListeners: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* @param game The game that is being rendered
|
||||
* @param forceFullscreen If fullscreen should be always activated
|
||||
@@ -753,7 +755,7 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.onKeyPressed(e.keyCode, e.location);
|
||||
manager.onKeyPressed(e.keyCode, e.location, e.code);
|
||||
};
|
||||
document.onkeyup = (e) => {
|
||||
if (isFocusingDomElement()) {
|
||||
@@ -770,11 +772,11 @@ namespace gdjs {
|
||||
// This means the key would be considered as "stuck" from the game's perspective
|
||||
// it would never be released unless it's pressed and released again (without meta).
|
||||
// Out of caution, we simulate a release of the key that were pressed with meta key.
|
||||
for (const {
|
||||
location,
|
||||
keyCode,
|
||||
} of keysPressedWithMetaPressedByCode.values()) {
|
||||
manager.onKeyReleased(keyCode, location);
|
||||
for (const [
|
||||
code,
|
||||
{ location, keyCode },
|
||||
] of keysPressedWithMetaPressedByCode) {
|
||||
manager.onKeyReleased(keyCode, location, code);
|
||||
}
|
||||
keysPressedWithMetaPressedByCode.clear();
|
||||
}
|
||||
@@ -792,7 +794,32 @@ namespace gdjs {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
manager.onKeyReleased(e.keyCode, e.location);
|
||||
manager.onKeyReleased(e.keyCode, e.location, e.code);
|
||||
};
|
||||
|
||||
// No "keyup" or "mouseup" is received for the keys and mouse buttons that are
|
||||
// still held down when the game loses the focus (when switching to another
|
||||
// window or tab), which would leave them stuck in a pressed state.
|
||||
// Release them all instead.
|
||||
const releaseAllPressedInputs = () => {
|
||||
keysPressedWithMetaPressedByCode.clear();
|
||||
manager.releaseAllPressedKeys();
|
||||
manager.releaseAllPressedMouseButtons();
|
||||
};
|
||||
const onBlur = () => {
|
||||
releaseAllPressedInputs();
|
||||
};
|
||||
const onVisibilityChange = () => {
|
||||
// On mobile, switching to another app can hide the game without blurring it.
|
||||
if (document.visibilityState === 'hidden') {
|
||||
releaseAllPressedInputs();
|
||||
}
|
||||
};
|
||||
window.addEventListener('blur', onBlur);
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
this._unregisterFocusListeners = () => {
|
||||
window.removeEventListener('blur', onBlur);
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
|
||||
// Mouse:
|
||||
@@ -1099,11 +1126,16 @@ namespace gdjs {
|
||||
/**
|
||||
* Dispose the renderers (PixiJS and/or Three.js) as well as DOM elements
|
||||
* used for the game (the canvas, if specified, and the additional DOM container
|
||||
* created on top of it to allow display HTML elements, for example for text inputs).
|
||||
* created on top of it to allow display HTML elements, for example for text inputs),
|
||||
* and the events listeners registered on the window and document.
|
||||
*
|
||||
* @param removeCanvas If true, the canvas will be removed from the DOM.
|
||||
*/
|
||||
dispose(removeCanvas?: boolean) {
|
||||
if (this._unregisterFocusListeners) {
|
||||
this._unregisterFocusListeners();
|
||||
this._unregisterFocusListeners = null;
|
||||
}
|
||||
this._pixiRenderer?.destroy();
|
||||
this._threeRenderer?.dispose();
|
||||
this._pixiRenderer = null;
|
||||
|
||||
@@ -91,6 +91,74 @@ describe('gdjs.InputManager', () => {
|
||||
inputManager.onKeyReleased(17);
|
||||
});
|
||||
|
||||
it('should ignore a numpad location that is contradicted by the code', () => {
|
||||
// Safari on macOS reports the arrow keys with a numpad location, because macOS
|
||||
// sets the "numeric pad" modifier flag for them.
|
||||
inputManager.onKeyPressed(38, 3, 'ArrowUp');
|
||||
expect(inputManager.getLastPressedKey()).to.be(38);
|
||||
expect(inputManager.isKeyPressed(38)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(3038)).to.be(false);
|
||||
inputManager.onKeyReleased(38, 3, 'ArrowUp');
|
||||
expect(inputManager.wasKeyReleased(38)).to.be(true);
|
||||
expect(inputManager.wasKeyReleased(3038)).to.be(false);
|
||||
inputManager.onFrameEnded();
|
||||
|
||||
// A numpad key with NumLock off keeps its numpad location.
|
||||
inputManager.onKeyPressed(38, 3, 'Numpad8');
|
||||
expect(inputManager.getLastPressedKey()).to.be(3038);
|
||||
expect(inputManager.isKeyPressed(3038)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(38)).to.be(false);
|
||||
inputManager.onKeyReleased(38, 3, 'Numpad8');
|
||||
expect(inputManager.wasKeyReleased(3038)).to.be(true);
|
||||
inputManager.onFrameEnded();
|
||||
|
||||
// Without a code, the location is trusted as before.
|
||||
inputManager.onKeyPressed(38, 3);
|
||||
expect(inputManager.getLastPressedKey()).to.be(3038);
|
||||
inputManager.onKeyReleased(38, 3);
|
||||
});
|
||||
|
||||
it('should release the held keys and mouse buttons when the game loses the focus', () => {
|
||||
// A key that was already pressed and released before losing the focus.
|
||||
inputManager.onKeyPressed(65, 0, 'KeyA');
|
||||
inputManager.onKeyReleased(65, 0, 'KeyA');
|
||||
inputManager.onFrameEnded();
|
||||
|
||||
// Keys and mouse buttons still held down when the focus is lost.
|
||||
inputManager.onKeyPressed(87, 0, 'KeyW');
|
||||
inputManager.onKeyPressed(16, 1, 'ShiftLeft');
|
||||
inputManager.onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON);
|
||||
expect(inputManager.isKeyPressed(87)).to.be(true);
|
||||
expect(inputManager.isKeyPressed(1016)).to.be(true);
|
||||
expect(
|
||||
inputManager.isMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON)
|
||||
).to.be(true);
|
||||
inputManager.onFrameEnded();
|
||||
|
||||
inputManager.releaseAllPressedKeys();
|
||||
inputManager.releaseAllPressedMouseButtons();
|
||||
expect(inputManager.isKeyPressed(87)).to.be(false);
|
||||
expect(inputManager.isKeyPressed(1016)).to.be(false);
|
||||
expect(inputManager.wasKeyReleased(87)).to.be(true);
|
||||
expect(inputManager.wasKeyReleased(1016)).to.be(true);
|
||||
expect(inputManager.anyKeyPressed()).to.be(false);
|
||||
expect(
|
||||
inputManager.isMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON)
|
||||
).to.be(false);
|
||||
expect(
|
||||
inputManager.isMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON)
|
||||
).to.be(true);
|
||||
expect(inputManager.anyMouseButtonPressed()).to.be(false);
|
||||
// The touch simulated by the left button must have ended too.
|
||||
expect(
|
||||
inputManager.hasTouchEnded(gdjs.InputManager.MOUSE_TOUCH_ID)
|
||||
).to.be(true);
|
||||
|
||||
// The key released before the focus loss must not be released a second time.
|
||||
expect(inputManager.wasKeyReleased(65)).to.be(false);
|
||||
inputManager.onFrameEnded();
|
||||
});
|
||||
|
||||
it('should handle mouse events', () => {
|
||||
inputManager.onMouseMove(500, 600);
|
||||
expect(inputManager.getCursorX()).to.be(500);
|
||||
|
||||
Reference in New Issue
Block a user