Keyboard mapping by keyCode (#4542)

* Keyboard mapping by keyCode

* Keyboard mapping by keyCode - v2
This commit is contained in:
SomeTroglodyte
2021-07-17 21:26:20 +02:00
committed by GitHub
parent 70f34f2f85
commit 3a7da738c4
3 changed files with 61 additions and 30 deletions

View File

@ -95,7 +95,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
Gdx.app.clipboard.contents = base64Gzip Gdx.app.clipboard.contents = base64Gzip
} }
copyMapAsTextButton.onClick (copyMapAsTextAction) copyMapAsTextButton.onClick (copyMapAsTextAction)
keyPressDispatcher['\u0003'] = copyMapAsTextAction // Ctrl-C keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction
rightSideTable.add(copyMapAsTextButton).row() rightSideTable.add(copyMapAsTextButton).row()
} else { } else {
val loadFromClipboardButton = "Load copied data".toTextButton() val loadFromClipboardButton = "Load copied data".toTextButton()
@ -111,7 +111,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
} }
} }
loadFromClipboardButton.onClick(loadFromClipboardAction) loadFromClipboardButton.onClick(loadFromClipboardAction)
keyPressDispatcher['\u0016'] = loadFromClipboardAction // Ctrl-V keyPressDispatcher[KeyCharAndCode.ctrl('V')] = loadFromClipboardAction
rightSideTable.add(loadFromClipboardButton).row() rightSideTable.add(loadFromClipboardButton).row()
rightSideTable.add(couldNotLoadMapLabel).row() rightSideTable.add(couldNotLoadMapLabel).row()
} }
@ -123,7 +123,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
}, this).open() }, this).open()
} }
deleteButton.onClick(deleteAction) deleteButton.onClick(deleteAction)
keyPressDispatcher['\u007f'] = deleteAction // Input.Keys.DEL but ascii has precedence keyPressDispatcher[KeyCharAndCode.DEL] = deleteAction
rightSideTable.add(deleteButton).row() rightSideTable.add(deleteButton).row()
topTable.add(rightSideTable) topTable.add(rightSideTable)

View File

@ -8,13 +8,15 @@ import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.Stage 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 * 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 * 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) * (Exception: international keyboard AltGr-combos)
* An update supporting easy declarations for any modifier combos would need to use Gdx.input.isKeyPressed() * 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 * 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) * Example: KeyCharAndCode('R'), KeyCharAndCode(Input.Keys.F1)
*/ */
data class KeyCharAndCode(val char: Char, val code: Int) { data class KeyCharAndCode(val char: Char, val code: Int) {
// express keys with a Char value /** helper 'cloning constructor' to allow feeding both fields from a factory function */
constructor(char: Char): this(char.toLowerCase(), 0) private constructor(from: KeyCharAndCode): this(from.char, from.code)
// express keys that only have a keyCode like F1 /** 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) 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 // see https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/char-int-conversions.md
override fun toString(): String { override fun toString(): String {
// debug helper // debug helper
@ -46,13 +44,29 @@ data class KeyCharAndCode(val char: Char, val code: Int) {
} }
} }
// Convenience shortcuts for frequently used constants
companion object { companion object {
// Convenience shortcuts for frequently used constants
val BACK = KeyCharAndCode(Input.Keys.BACK) val BACK = KeyCharAndCode(Input.Keys.BACK)
val ESC = KeyCharAndCode('\u001B') val ESC = KeyCharAndCode(Input.Keys.ESCAPE)
val RETURN = KeyCharAndCode('\r') val RETURN = KeyCharAndCode(Input.Keys.ENTER)
val NEWLINE = KeyCharAndCode('\n') val NUMPAD_ENTER = KeyCharAndCode(Input.Keys.NUMPAD_ENTER)
val SPACE = KeyCharAndCode(' ') 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<KeyCharAndCode, (()
// access by KeyCharAndCode // access by KeyCharAndCode
operator fun set(key: KeyCharAndCode, action: () -> Unit) { operator fun set(key: KeyCharAndCode, action: () -> Unit) {
if (key == KeyCharAndCode.UNKNOWN) return
super.put(key, action) 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) if (key == KeyCharAndCode.RETURN)
super.put(KeyCharAndCode.NEWLINE, action) super.put(KeyCharAndCode.NUMPAD_ENTER, action)
// Likewise always match Back to ESC // Likewise always match Back to ESC
if (key == KeyCharAndCode.BACK) if (key == KeyCharAndCode.BACK)
super.put(KeyCharAndCode.ESC, action) super.put(KeyCharAndCode.ESC, action)
// And make two codes for DEL equivalent
if (key == KeyCharAndCode.DEL)
super.put(KeyCharAndCode.FORWARD_DEL, action)
checkInstall() checkInstall()
} }
override fun remove(key: KeyCharAndCode): (() -> Unit)? { override fun remove(key: KeyCharAndCode): (() -> Unit)? {
if (key == KeyCharAndCode.UNKNOWN) return null
val result = super.remove(key) val result = super.remove(key)
if (key == KeyCharAndCode.RETURN) if (key == KeyCharAndCode.RETURN)
super.remove(KeyCharAndCode.NEWLINE) super.remove(KeyCharAndCode.NUMPAD_ENTER)
if (key == KeyCharAndCode.BACK) if (key == KeyCharAndCode.BACK)
super.remove(KeyCharAndCode.ESC) super.remove(KeyCharAndCode.ESC)
if (key == KeyCharAndCode.DEL)
super.remove(KeyCharAndCode.FORWARD_DEL)
checkInstall() checkInstall()
return result return result
} }
@ -145,13 +166,23 @@ class KeyPressDispatcher(val name: String? = null) : HashMap<KeyCharAndCode, (()
listener = listener =
object : InputListener() { object : InputListener() {
override fun keyTyped(event: InputEvent?, character: Char): Boolean { override fun keyTyped(event: InputEvent?, character: Char): Boolean {
val key = KeyCharAndCode(event, character) // look for both key code and ascii entries - ascii first as the
// Char constructor of KeyCharAndCode generates keyCode based instances
// preferentially but we would miss Ctrl- combos otherwise
val key = when {
contains(KeyCharAndCode.ascii(character)) ->
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 // see if we want to handle this key, and if not, let it propagate
if (!contains(key) || (checkIgnoreKeys?.invoke() == true)) if (!contains(key) || (checkIgnoreKeys?.invoke() == true))
return super.keyTyped(event, character) 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 { try {
this@KeyPressDispatcher[key]?.invoke() this@KeyPressDispatcher[key]?.invoke()
} catch (ex: Exception) {} } catch (ex: Exception) {}

View File

@ -251,11 +251,11 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
if (!mapHolder.setCenterPosition(capital.location)) if (!mapHolder.setCenterPosition(capital.location))
game.setScreen(CityScreen(capital)) game.setScreen(CityScreen(capital))
} }
keyPressDispatcher['\u000F'] = { this.openOptionsPopup() } // Ctrl-O: Game Options keyPressDispatcher[KeyCharAndCode.ctrl('O')] = { this.openOptionsPopup() } // Game Options
keyPressDispatcher['\u0013'] = { game.setScreen(SaveGameScreen(gameInfo)) } // Ctrl-S: Save keyPressDispatcher[KeyCharAndCode.ctrl('S')] = { game.setScreen(SaveGameScreen(gameInfo)) } // Save
keyPressDispatcher['\u000C'] = { game.setScreen(LoadGameScreen(this)) } // Ctrl-L: Load keyPressDispatcher[KeyCharAndCode.ctrl('L')] = { game.setScreen(LoadGameScreen(this)) } // Load
keyPressDispatcher['+'] = { this.mapHolder.zoomIn() } // '+' Zoom - Input.Keys.NUMPAD_ADD would need dispatcher patch keyPressDispatcher[Input.Keys.NUMPAD_ADD] = { this.mapHolder.zoomIn() } // '+' Zoom
keyPressDispatcher['-'] = { this.mapHolder.zoomOut() } // '-' Zoom keyPressDispatcher[Input.Keys.NUMPAD_SUBTRACT] = { this.mapHolder.zoomOut() } // '-' Zoom
keyPressDispatcher.setCheckpoint() keyPressDispatcher.setCheckpoint()
} }