chinese IME almost done

This commit is contained in:
minjaesong
2021-10-27 11:45:56 +09:00
parent b0b1d185ad
commit 0dbcd0711b
9 changed files with 23831 additions and 22 deletions

9070
assets/keylayout/cj5-sc.han Normal file

File diff suppressed because it is too large Load Diff

14362
assets/keylayout/cj5-tc.han Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -453,5 +453,6 @@ return Object.freeze({"n":"두벌식 표준","states":states,"c":"CuriousTo\uA75
return ret return ret
}, },
"reset":()=>{ reset() }, "reset":()=>{ reset() },
"composing":()=>(states.code!=0) "composing":()=>(states.code!=0),
"maxCandidates":()=>1
}) })

View File

@@ -474,5 +474,6 @@ return Object.freeze({"n":"세벌식 3-90","states":states,"c":"CuriousTo\uA75Bv
return ret return ret
}, },
"reset":()=>{ reset() }, "reset":()=>{ reset() },
"composing":()=>(states.code!=0) "composing":()=>(states.code!=0),
"maxCandidates":()=>1
}) })

View File

@@ -498,5 +498,6 @@ return Object.freeze({"n":"신세벌식 P2","states":states,"c":"CuriousTo\uA75B
return ret return ret
}, },
"reset":()=>{ reset() }, "reset":()=>{ reset() },
"composing":()=>(states.code!=0) "composing":()=>(states.code!=0),
"maxCandidates":()=>1
}) })

View File

@@ -0,0 +1,313 @@
let states = {"keylayouts":[[""],[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
["0",")"],
["1","!"],
["2","@"],
["3","#"],
["4","$"],
["5","%"],
["6","^"],
["7","&"],
["8","*"],
["9","("],
["*"],
["#"],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
["a","A"],
["b","B"],
["c","C"],
["d","D"],
["e","E"],
["f","F"],
["g","G"],
["h","H"],
["i","I"],
["j","J"],
["k","K"],
["l","L"],
["m","M"],
["n","N"],
["o","O"],
["p","P"],
["q","Q"],
["r","R"],
["s","S"],
["t","T"],
["u","U"],
["v","V"],
["w","W"],
["x","X"],
["y","Y"],
["z","Z"],
[",","<"],
[".",">"],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[" "],
[undefined],
[undefined],
[undefined],
["\n"],
["\x08"],
["`","~"],
["-","_"],
["=","+"],
["[","{"],
["]","}"],
["\\","|"],
[";",":"],
["'",'"'],
["/","?"],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
["0"],
["1"],
["2"],
["3"],
["4"],
["5"],
["6"],
["7"],
["8"],
["9"],
["/"],
["*"],
["-"],
["+"],
["."],
["."],
["\n"],
["="],
["("],
[")"],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined],
[undefined]
],
"dict":IMEProvider.requestDictionary("cj5-tc.han"),
"code":0, //0: not composing, 1: composing (has candidates), 2: composing (no candidates)
"buf":"",
"candidates":""/*comma-separated values*/}
let reset = () => {
states.code = 0
states.buf = ""
states.candidates = ""
}
//let bufDebugStringify = (buf) => [0,1,2].map(i => (buf[i] == undefined) ? "·" : `\\u${buf[i].codePointAt(0).toString(16).toUpperCase()}`).join(' ')
let bufDebugStringify = (buf) => [0,1,2].map(i => (buf[i] == undefined) ? "·" : `${buf[i]}`).join(' ')
return Object.freeze({"n":"五倉正體 Qwerty","states":states,"c":"CuriousTo\uA75Bvald, 倉頡之友 。馬來西亞 http://www.chinesecj.com",
// return: [displayed output, composed output]
"accept":(headkey,shiftin,altgrin)=>{
let layer = 1*shiftin// + 2*altgrin
let cjkey = states.keylayouts[headkey][layer]
let cjkeyAsc = cjkey.codePointAt(0)
if (states.code == 1 && 48 <= cjkeyAsc && cjkeyAsc <= 57) {
let raw = ''+states.buf
let selection = states.candidates.split(',')[cjkeyAsc - 49]
reset()
return ['', selection || raw]
}
else if (97 <= cjkeyAsc && cjkeyAsc <= 122 || cjkeyAsc == 42) {
states.buf += cjkey
states.candidates = states.dict.get(states.buf) // comma-separated values
states.code = 1 + (states.candidates.length == 0)
console.log(`cangjie in, buf: ${states.buf}, candidates: ${states.candidates}`)
return [`${states.buf},${states.candidates}`, '']
}
else {
states.code = 0
return ['', ''+states.buf+cjkey]
}
return ['', cjkey]
},
"backspace":()=>{
return ''
},
"end":()=>{
// console.log(`end composing`)
let ret = ''+states.buf
reset()
return ret
},
"reset":()=>{ reset() },
"composing":()=>(states.code!=0),
"maxCandidates":()=>10
})

View File

@@ -13,12 +13,13 @@ data class TerrarumKeyLayout(
data class TerrarumInputMethod( data class TerrarumInputMethod(
val name: String, val name: String,
// (headkey, shiftin, altgrin) // (headkey, shiftin, altgrin, lowLayerKeysym)
val acceptChar: (Int, Boolean, Boolean) -> Pair<IMECanditates, IMEOutput>, val acceptChar: (Int, Boolean, Boolean, String) -> Pair<IMECanditates, IMEOutput>,
val backspace: () -> IMECanditates, val backspace: () -> IMECanditates,
val endCompose: () -> IMEOutput, val endCompose: () -> IMEOutput,
val reset: () -> Unit, val reset: () -> Unit,
val composing: () -> Boolean val composing: () -> Boolean,
val maxCandidates: () -> Int
) )
/** /**
@@ -46,12 +47,15 @@ object IME {
private val highLayers = HashMap<String, TerrarumInputMethod>() private val highLayers = HashMap<String, TerrarumInputMethod>()
private val context = org.graalvm.polyglot.Context.newBuilder("js") private val context = org.graalvm.polyglot.Context.newBuilder("js")
.allowHostAccess(org.graalvm.polyglot.HostAccess.NONE) .allowHostAccess(org.graalvm.polyglot.HostAccess.ALL)
// .allowHostClassLookup { it.equals("net.torvald.terrarum.gamecontroller.IMEProviderDelegate") }
.allowHostClassLookup { false } .allowHostClassLookup { false }
.allowIO(false) .allowIO(false)
.build() .build()
init { init {
context.getBindings("js").putMember("IMEProvider", IMEProviderDelegate(this))
File(KEYLAYOUT_DIR).listFiles { file, s -> s.endsWith(".$KEYLAYOUT_EXTENSION") }.forEach { File(KEYLAYOUT_DIR).listFiles { file, s -> s.endsWith(".$KEYLAYOUT_EXTENSION") }.forEach {
printdbg(this, "Registering Low layer ${it.nameWithoutExtension.lowercase()}") printdbg(this, "Registering Low layer ${it.nameWithoutExtension.lowercase()}")
lowLayers[it.nameWithoutExtension.lowercase()] = parseKeylayoutFile(it) lowLayers[it.nameWithoutExtension.lowercase()] = parseKeylayoutFile(it)
@@ -115,8 +119,8 @@ object IME {
val name = jsval.getMember("n").asString() val name = jsval.getMember("n").asString()
return TerrarumInputMethod(name, { headkey, shifted, alted -> return TerrarumInputMethod(name, { headkey, shifted, alted, lowLayerKeysym ->
val a = jsval.invokeMember("accept", headkey, shifted, alted) val a = jsval.invokeMember("accept", headkey, shifted, alted, lowLayerKeysym)
a.getArrayElement(0).asString().toCanditates() to a.getArrayElement(1).asString() a.getArrayElement(0).asString().toCanditates() to a.getArrayElement(1).asString()
}, { }, {
jsval.invokeMember("backspace").asString().toCanditates() jsval.invokeMember("backspace").asString().toCanditates()
@@ -126,6 +130,8 @@ object IME {
jsval.invokeMember("reset") jsval.invokeMember("reset")
}, { }, {
jsval.invokeMember("composing").asBoolean() jsval.invokeMember("composing").asBoolean()
}, {
jsval.invokeMember("maxCandidates").asInt()
} }
) )
} }

View File

@@ -0,0 +1,35 @@
package net.torvald.terrarum.gamecontroller
import java.io.File
import java.io.FileReader
class IMEProviderDelegate(val ime: IME) {
private val dictionaries = HashMap<String, IMEDictionary>()
fun requestDictionary(filename: String): IMEDictionary {
return dictionaries.getOrPut(filename) { IMEDictionary(filename) }
}
}
class IMEDictionary(filename: String) {
private val candidates = HashMap<String, String>()
init {
val reader = FileReader(File("assets/keylayout/", filename))
reader.forEachLine {
val (key, value) = it.split(',')
if (candidates.containsKey(key)) {
candidates[key] += ",$value"
}
else {
candidates[key] = value
}
}
}
operator fun get(key: String): String = candidates[key] ?: ""
}

View File

@@ -44,6 +44,9 @@ data class InputLenCap(val count: Int, val unit: CharLenUnit) {
} }
/** /**
* Protip: if there are multiple TextLineInputs on a same UI, draw bottom one first, otherwise the IME's
* candidate window will be hidden by the bottom UIItem if they overlaps.
*
* @param width width of the text input where the text gets drawn, not the entire item * @param width width of the text input where the text gets drawn, not the entire item
* @param height height of the text input where the text gets drawn, not the entire item * @param height height of the text input where the text gets drawn, not the entire item
* *
@@ -230,7 +233,7 @@ class UIItemTextLineInput(
val altgrin = keycodes.contains(Input.Keys.ALT_RIGHT) val altgrin = keycodes.contains(Input.Keys.ALT_RIGHT)
val codepoints = if (ime != null) { val codepoints = if (ime != null) {
val newStatus = ime.acceptChar(headkey, shiftin, altgrin) val newStatus = ime.acceptChar(headkey, shiftin, altgrin, char)
candidates = newStatus.first.map { CodepointSequence(it.toCodePoints()) } candidates = newStatus.first.map { CodepointSequence(it.toCodePoints()) }
newStatus.second.toCodePoints() newStatus.second.toCodePoints()
@@ -340,6 +343,8 @@ class UIItemTextLineInput(
return textbuf.toJavaString() return textbuf.toJavaString()
} }
private val candidateNumberStrWidth = App.fontGame.getWidth("8. ")
override fun render(batch: SpriteBatch, camera: Camera) { override fun render(batch: SpriteBatch, camera: Camera) {
batch.end() batch.end()
@@ -436,21 +441,36 @@ class UIItemTextLineInput(
// draw candidates view // draw candidates view
if (candidates.isNotEmpty()) { if (candidates.isNotEmpty()) {
val textWidths = candidates.map { App.fontGame.getWidth(CodepointSequence(it)) } val textWidths = candidates.map { App.fontGame.getWidth(CodepointSequence(it)) }
val candidateWinW = (textWidths.maxOrNull() ?: 0).coerceAtLeast(20)
val candidateWinH = App.fontGame.lineHeight.toInt() * candidates.size val candidateWinH = App.fontGame.lineHeight.toInt() * candidates.size
// candidate view background
batch.color = TEXTINPUT_COL_BACKGROUND
Toolkit.fillArea(batch, cursorXOnScreen + 2, posY + 27, candidateWinW, candidateWinH)
// candidate view border
batch.color = Toolkit.Theme.COL_ACTIVE
Toolkit.drawBoxBorder(batch, cursorXOnScreen + 1, posY + 26, candidateWinW + 2, candidateWinH + 2)
// candidate view text // candidate view text
for (i in candidates.indices) { if (getIME()!!.maxCandidates() > 1) {
val previewTextWidth = textWidths[i] val candidateWinW = textWidths.maxOrNull()!!.coerceAtLeast(20) + candidateNumberStrWidth
App.fontGame.draw(batch, candidates[i], cursorXOnScreen + 2 + (candidateWinW - previewTextWidth) / 2, posY + 27 + i * 20)
// candidate view background
batch.color = TEXTINPUT_COL_BACKGROUND
Toolkit.fillArea(batch, cursorXOnScreen + 2, posY + 27, candidateWinW + 4, candidateWinH)
// candidate view border
batch.color = Toolkit.Theme.COL_ACTIVE
Toolkit.drawBoxBorder(batch, cursorXOnScreen + 1, posY + 26, candidateWinW + 6, candidateWinH + 2)
for (i in 0..minOf(9, candidates.lastIndex)) {
val candidateNum = listOf(i+48,46,32)
App.fontGame.draw(batch, CodepointSequence(candidateNum + candidates[i]), cursorXOnScreen + 4, posY + 27 + i * 20)
}
}
else {
val candidateWinW = textWidths.maxOrNull()!!.coerceAtLeast(20)
// candidate view background
batch.color = TEXTINPUT_COL_BACKGROUND
Toolkit.fillArea(batch, cursorXOnScreen + 2, posY + 27, candidateWinW, candidateWinH)
// candidate view border
batch.color = Toolkit.Theme.COL_ACTIVE
Toolkit.drawBoxBorder(batch, cursorXOnScreen + 1, posY + 26, candidateWinW + 2, candidateWinH + 2)
val previewTextWidth = textWidths[0]
App.fontGame.draw(batch, candidates[0], cursorXOnScreen + 2 + (candidateWinW - previewTextWidth) / 2, posY + 27)
} }
} }