From 3a7da738c4b7a99fc0f3d985e51607734cab9bd6 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sat, 17 Jul 2021 21:26:20 +0200 Subject: [PATCH] Keyboard mapping by keyCode (#4542) * Keyboard mapping by keyCode * Keyboard mapping by keyCode - v2 --- .../ui/mapeditor/SaveAndLoadMapScreen.kt | 6 +- .../com/unciv/ui/utils/KeyPressDispatcher.kt | 75 +++++++++++++------ .../com/unciv/ui/worldscreen/WorldScreen.kt | 10 +-- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt index d56be75b6b..d67dcb7726 100644 --- a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt @@ -95,7 +95,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc Gdx.app.clipboard.contents = base64Gzip } copyMapAsTextButton.onClick (copyMapAsTextAction) - keyPressDispatcher['\u0003'] = copyMapAsTextAction // Ctrl-C + keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction rightSideTable.add(copyMapAsTextButton).row() } else { val loadFromClipboardButton = "Load copied data".toTextButton() @@ -111,7 +111,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc } } loadFromClipboardButton.onClick(loadFromClipboardAction) - keyPressDispatcher['\u0016'] = loadFromClipboardAction // Ctrl-V + keyPressDispatcher[KeyCharAndCode.ctrl('V')] = loadFromClipboardAction rightSideTable.add(loadFromClipboardButton).row() rightSideTable.add(couldNotLoadMapLabel).row() } @@ -123,7 +123,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc }, this).open() } deleteButton.onClick(deleteAction) - keyPressDispatcher['\u007f'] = deleteAction // Input.Keys.DEL but ascii has precedence + keyPressDispatcher[KeyCharAndCode.DEL] = deleteAction rightSideTable.add(deleteButton).row() topTable.add(rightSideTable) diff --git a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt index 0cfa32d068..70d2e0ccec 100644 --- a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt +++ b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt @@ -8,13 +8,15 @@ import com.badlogic.gdx.scenes.scene2d.InputListener import com.badlogic.gdx.scenes.scene2d.Stage /* - * For now, combination keys cannot easily be expressed. + * For now, many combination keys cannot easily be expressed. * Pressing Ctrl-Letter will arrive one event for Input.Keys.CONTROL_LEFT and one for the ASCII control code point - * so Ctrl-R can be handled using KeyCharAndCode('\u0012') + * so Ctrl-R can be handled using KeyCharAndCode('\u0012') or KeyCharAndCode.ctrl('R') * Pressing Alt-Something likewise will fire once for Alt and once for the unmodified keys with no indication Alt is held * (Exception: international keyboard AltGr-combos) * An update supporting easy declarations for any modifier combos would need to use Gdx.input.isKeyPressed() * Gdx seems to omit support for a modifier mask (e.g. Ctrl-Alt-Shift) so we would need to reinvent this + * + * Note: It is important that KeyCharAndCode is an immutable data class to support usage as HashMap key */ /** @@ -23,18 +25,14 @@ import com.badlogic.gdx.scenes.scene2d.Stage * Example: KeyCharAndCode('R'), KeyCharAndCode(Input.Keys.F1) */ data class KeyCharAndCode(val char: Char, val code: Int) { - // express keys with a Char value - constructor(char: Char): this(char.toLowerCase(), 0) - // express keys that only have a keyCode like F1 + /** helper 'cloning constructor' to allow feeding both fields from a factory function */ + private constructor(from: KeyCharAndCode): this(from.char, from.code) + /** Map keys from a Char - will detect by keycode if one can be mapped, by character otherwise */ + constructor(char: Char): this(mapChar(char)) + /** express keys that only have a keyCode like F1 */ constructor(code: Int): this(Char.MIN_VALUE, code) - // helper for use in InputListener keyTyped() - constructor(event: InputEvent?, character: Char) - : this ( - character.toLowerCase(), - if (character == Char.MIN_VALUE && event!=null) event.keyCode else 0 - ) - // From Kotlin 1.5 on the Ctrl- line will need Char(char.code+64) + // From Kotlin 1.5? on the Ctrl- line will need Char(char.code+64) // see https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/char-int-conversions.md override fun toString(): String { // debug helper @@ -46,13 +44,29 @@ data class KeyCharAndCode(val char: Char, val code: Int) { } } - // Convenience shortcuts for frequently used constants companion object { + // Convenience shortcuts for frequently used constants val BACK = KeyCharAndCode(Input.Keys.BACK) - val ESC = KeyCharAndCode('\u001B') - val RETURN = KeyCharAndCode('\r') - val NEWLINE = KeyCharAndCode('\n') - val SPACE = KeyCharAndCode(' ') + val ESC = KeyCharAndCode(Input.Keys.ESCAPE) + val RETURN = KeyCharAndCode(Input.Keys.ENTER) + val NUMPAD_ENTER = KeyCharAndCode(Input.Keys.NUMPAD_ENTER) + val SPACE = KeyCharAndCode(Input.Keys.SPACE) + val DEL = KeyCharAndCode(Input.Keys.DEL) + val FORWARD_DEL = KeyCharAndCode(Input.Keys.FORWARD_DEL) // this is what I see for both 'Del' keys + /** Guaranteed to be ignored by [KeyPressDispatcher.set] and never to be generated for an actual event, used as fallback to ensure no action is taken */ + val UNKNOWN = KeyCharAndCode(Input.Keys.UNKNOWN) + + /** mini-factory for control codes - case insensitive */ + fun ctrl(letter: Char) = KeyCharAndCode((letter.toInt() and 31).toChar(), 0) + + /** mini-factory for KeyCharAndCode values to be compared by character, not by code */ + fun ascii(char: Char) = KeyCharAndCode(char.toLowerCase(), 0) + + /** factory maps a Char to a keyCode if possible, returns a Char-based instance otherwise */ + fun mapChar(char: Char): KeyCharAndCode { + val code = Input.Keys.valueOf(char.toUpperCase().toString()) + return if (code == -1) KeyCharAndCode(char,0) else KeyCharAndCode(Char.MIN_VALUE, code) + } } } @@ -94,21 +108,28 @@ class KeyPressDispatcher(val name: String? = null) : HashMap Unit) { + if (key == KeyCharAndCode.UNKNOWN) return super.put(key, action) - // On Android the Enter key will fire with Ascii code `Linefeed`, on desktop as `Carriage Return` + // Make both Enter keys equivalent if (key == KeyCharAndCode.RETURN) - super.put(KeyCharAndCode.NEWLINE, action) + super.put(KeyCharAndCode.NUMPAD_ENTER, action) // Likewise always match Back to ESC if (key == KeyCharAndCode.BACK) super.put(KeyCharAndCode.ESC, action) + // And make two codes for DEL equivalent + if (key == KeyCharAndCode.DEL) + super.put(KeyCharAndCode.FORWARD_DEL, action) checkInstall() } override fun remove(key: KeyCharAndCode): (() -> Unit)? { + if (key == KeyCharAndCode.UNKNOWN) return null val result = super.remove(key) if (key == KeyCharAndCode.RETURN) - super.remove(KeyCharAndCode.NEWLINE) + super.remove(KeyCharAndCode.NUMPAD_ENTER) if (key == KeyCharAndCode.BACK) super.remove(KeyCharAndCode.ESC) + if (key == KeyCharAndCode.DEL) + super.remove(KeyCharAndCode.FORWARD_DEL) checkInstall() return result } @@ -145,13 +166,23 @@ class KeyPressDispatcher(val name: String? = null) : HashMap + KeyCharAndCode.ascii(character) + event == null -> + KeyCharAndCode.UNKNOWN + else -> + KeyCharAndCode(event.keyCode) + } // see if we want to handle this key, and if not, let it propagate if (!contains(key) || (checkIgnoreKeys?.invoke() == true)) return super.keyTyped(event, character) - //try-catch mainly for debugging. Breakpoints in the vicinity can make the event fire twice in rapid succession, second time the context can be invalid + // try-catch mainly for debugging. Breakpoints in the vicinity can make the event fire twice in rapid succession, second time the context can be invalid try { this@KeyPressDispatcher[key]?.invoke() } catch (ex: Exception) {} diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 68a7633653..ea5faee51d 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -251,11 +251,11 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam if (!mapHolder.setCenterPosition(capital.location)) game.setScreen(CityScreen(capital)) } - keyPressDispatcher['\u000F'] = { this.openOptionsPopup() } // Ctrl-O: Game Options - keyPressDispatcher['\u0013'] = { game.setScreen(SaveGameScreen(gameInfo)) } // Ctrl-S: Save - keyPressDispatcher['\u000C'] = { game.setScreen(LoadGameScreen(this)) } // Ctrl-L: Load - keyPressDispatcher['+'] = { this.mapHolder.zoomIn() } // '+' Zoom - Input.Keys.NUMPAD_ADD would need dispatcher patch - keyPressDispatcher['-'] = { this.mapHolder.zoomOut() } // '-' Zoom + keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { this.openOptionsPopup() } // Game Options + keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save + keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load + keyPressDispatcher[Input.Keys.NUMPAD_ADD] = { this.mapHolder.zoomIn() } // '+' Zoom + keyPressDispatcher[Input.Keys.NUMPAD_SUBTRACT] = { this.mapHolder.zoomOut() } // '-' Zoom keyPressDispatcher.setCheckpoint() }