Files
Terrarum/src/net/torvald/terrarum/gamecontroller/IME.kt
2023-06-06 18:16:34 +09:00

258 lines
9.4 KiB
Kotlin

package net.torvald.terrarum.gamecontroller
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.g2d.TextureRegion
import net.torvald.terrarum.App
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
import java.io.File
typealias IMECandidates = List<String>
typealias IMEOutput = String
typealias Keysyms = Array<Array<String?>>
typealias Keysymfun = Map<String, String>
data class TerrarumKeyLayout(
val name: String,
val capsMode: TerrarumKeyCapsMode,
val symbols: Keysyms
)
enum class TerrarumKeyCapsMode {
CAPS, SHIFT, BACK
}
data class TerrarumIME(
val name: String,
val config: TerrarumIMEConf,
// (headkey, shiftin, altgrin, lowLayerKeysym)
val acceptChar: (Int, Boolean, Boolean, String) -> Pair<IMECandidates, IMEOutput>,
val backspace: () -> IMECandidates,
val endCompose: () -> IMEOutput,
val reset: () -> Unit,
val composing: () -> Boolean
)
data class TerrarumIMEConf(
val name: String,
val copying: String,
val lang: String,
val candidates: TerrarumIMEViewCount,
val symbols: Keysyms?,
val symbolsfun: Keysymfun?,
val mode: TerrarumIMEMode
)
enum class TerrarumIMEViewCount {
NONE, ONE, MANY;
fun toInt() = when (this) {
NONE -> 0
ONE -> 1
MANY -> 10 // an hard-coded config
}
}
enum class TerrarumIMEMode {
CANDIDATES, REWRITE
}
/**
* Key Layout File Structure for Low Layer:
* - n: Displayed name of the keyboard layout
* - t: Key symbols in 256R4C string array (C1: unshifted, C2: Shift, C3: AltGr, C4: Shift-AltGr)
*
* IME consists of two keyboard layers:
* - Low layer: "english" keyboard (qwerty, colemak, etc), stateless
* - High layer: chinese/japanese/korean/etc. keyboard, stateful
*
* Input to the IME Keyboard layout is always GDX/LWJGL3 keycode (only LWJGL3 offers OS-Keylayout-independent keycodes)
*
* Created by minjaesong on 2021-10-20.
*/
object IME {
class LayoutNotFound(id: String): NullPointerException("Keyboard layout not found: $id")
const val KEYLAYOUT_DIR = "assets/keylayout/"
const val KEYLAYOUT_EXTENSION = "key"
const val IME_EXTENSION = "ime"
private val lowLayers = HashMap<String, TerrarumKeyLayout>()
private val highLayers = HashMap<String, TerrarumIME>()
private val context = org.graalvm.polyglot.Context.newBuilder("js")
.allowHostAccess(org.graalvm.polyglot.HostAccess.EXPLICIT)
// .allowHostClassLookup { it.equals("net.torvald.terrarum.gamecontroller.IMEProviderDelegate") }
.allowHostClassLookup { false }
.allowIO(false)
.build()
val icons = HashMap<String, TextureRegion>()
init {
context.getBindings("js").putMember("IMEProvider", IMEProviderDelegate(this))
File(KEYLAYOUT_DIR).listFiles { file, s -> s.endsWith(".$KEYLAYOUT_EXTENSION") }.sortedBy { it.name }.forEach {
printdbg(this, "Registering Low layer ${it.nameWithoutExtension.lowercase()}")
lowLayers[it.nameWithoutExtension.lowercase()] = parseKeylayoutFile(it)
}
File(KEYLAYOUT_DIR).listFiles { file, s -> s.endsWith(".$IME_EXTENSION") }.sortedBy { it.name }.forEach {
printdbg(this, "Registering High layer ${it.nameWithoutExtension.lowercase()}")
highLayers[it.nameWithoutExtension.lowercase()] = parseImeFile(it)
}
val iconSheet = TextureRegionPack("assets/graphics/gui/ime_icons_by_language.tga", 20, 20)
val iconPixmap = Pixmap(Gdx.files.internal("assets/graphics/gui/ime_icons_by_language.tga"))
for (k in 0 until iconPixmap.height step 20) {
val langCode = StringBuilder()
for (c in 0 until 20) {
val x = c
var charnum = 0
for (b in 0 until 7) {
val y = k + b
if (iconPixmap.getPixel(x, y) and 255 != 0) {
charnum = charnum or (1 shl b)
}
}
if (charnum != 0) langCode.append(charnum.toChar())
}
if (langCode.isNotEmpty()) {
printdbg(this, "Icon order #${(k+1) / 20} - icons[\"$langCode\"] = iconSheet.get(1, ${k/20})")
icons["$langCode"] = iconSheet.get(1, k / 20).also { it.flip(false, false) }
}
}
App.disposables.add(iconSheet)
iconPixmap.dispose()
}
@JvmStatic fun invoke() {}
fun getLowLayerByName(name: String): TerrarumKeyLayout {
return lowLayers[name.lowercase()]!!
}
fun getHighLayerByName(name: String): TerrarumIME {
return highLayers[name.lowercase()]!!
}
fun getAllLowLayers(): List<String> {
return lowLayers.keys.toList()
}
fun getAllHighLayers(): List<String> {
return highLayers.keys.toList()
}
private fun String.toCapsMode() = when (this.lowercase()) {
"caps" -> TerrarumKeyCapsMode.CAPS
"shift" -> TerrarumKeyCapsMode.SHIFT
"back" -> TerrarumKeyCapsMode.BACK
else -> throw IllegalArgumentException("Unknown capslock mode: $this")
}
private fun String.toViewCount() = when (this.lowercase()) {
"none" -> TerrarumIMEViewCount.NONE
"one" -> TerrarumIMEViewCount.ONE
"many" -> TerrarumIMEViewCount.MANY
else -> throw IllegalArgumentException("Unknown candidates mode: $this")
}
private fun parseKeylayoutFile(file: File): TerrarumKeyLayout {
val src = file.readText(Charsets.UTF_8)
val jsval = context.eval("js", "'use strict';Object.freeze($src)")
val name = jsval.getMember("n").asString()
val capsmode = jsval.getMember("capslock").asString().toCapsMode()
val out = Array(256) { Array<String?>(4) { null } }
for (keycode in 0L until 256L) {
val a = jsval.getMember("t").getArrayElement(keycode)
if (!a.isNull) {
for (layer in 0L until 4L) {
if (a.arraySize > layer) {
val b = a.getArrayElement(layer)
if (!b.isNull) {
out[keycode.toInt()][layer.toInt()] = b.asString()
}
}
}
}
}
// println("[IME] Test Keymap print for $name:"); for (keycode in 0 until 256) { print("$keycode:\t"); println(out[keycode].joinToString("\t")) }
return TerrarumKeyLayout(name, capsmode, out)
}
private fun String.toCanditates(): List<String> =
this.split(IMEDictionary.CAND_DELIM).mapNotNull { it.ifBlank { null } }
private fun String.toIMEMode(): TerrarumIMEMode =
when (this.lowercase()) {
"rewrite" -> TerrarumIMEMode.REWRITE
"candidates" -> TerrarumIMEMode.CANDIDATES
else -> throw IllegalArgumentException("Unknown operation mode: $this")
}
private fun parseImeFile(file: File): TerrarumIME {
val code = file.readText(Charsets.UTF_8)
val jsval = context.eval("js", "\"use strict\";(function(){$code})()")
val name = jsval.getMember("n").asString()
val candidatesCount = jsval.getMember("v").asString().toViewCount()
val copying = jsval.getMember("c").asString()
val keysyms = Array(256) { Array<String?>(4) { null } }
var keysymtable: Keysymfun? = null
val mode = jsval.getMember("m").asString().toIMEMode()
val lang = jsval.getMember("l").asString()
// keylayout is static (not affected by the Low Layout)
if (jsval.hasMember("t")) {
for (keycode in 0L until 256L) {
val a = jsval.getMember("t").getArrayElement(keycode)
if (!a.isNull) {
for (layer in 0L until 4L) {
if (a.arraySize > layer) {
val b = a.getArrayElement(layer)
if (!b.isNull) {
keysyms[keycode.toInt()][layer.toInt()] = b.asString()
}
}
}
}
}
}
// keylayout is dynamic (does affected by the Low Layout)
else if (jsval.hasMember("tf")) {
keysymtable = HashMap()
val tf = jsval.getMember("tf")
tf.memberKeys.forEach {
keysymtable[it] = tf.getMember(it).asString()
}
}
return TerrarumIME(
name,
TerrarumIMEConf(name, copying, lang, candidatesCount, if (keysymtable == null) keysyms else null, if (keysymtable == null) null else keysymtable, mode),
{ headkey, shifted, alted, lowLayerKeysym ->
val a = jsval.invokeMember("accept", headkey, shifted, alted, lowLayerKeysym)
// println(a.getArrayElement(0).asString().map { it.code.toString(16) })
a.getArrayElement(0).asString().toCanditates() to a.getArrayElement(1).asString()
}, {
jsval.invokeMember("backspace").asString().toCanditates()
}, {
jsval.invokeMember("end").asString()
}, {
jsval.invokeMember("reset")
}, {
jsval.invokeMember("composing").asBoolean()
}
)
}
}