From 3ca96bce7fcf0d72859d89b442d2c87600131b06 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Wed, 27 Oct 2021 15:22:43 +0900 Subject: [PATCH] working cangjie IME implementation --- assets/keylayout/zh_cn_cangjie5.ime | 318 ++++++++++++++++++ assets/keylayout/zh_tw_cangjie5.ime | 71 ++-- .../torvald/terrarum/gamecontroller/IME.kt | 4 +- .../gamecontroller/IMEProviderDelegate.kt | 41 ++- .../terrarum/ui/UIItemTextLineInput.kt | 53 ++- src/net/torvald/util/SortedArrayList.kt | 27 ++ 6 files changed, 458 insertions(+), 56 deletions(-) create mode 100644 assets/keylayout/zh_cn_cangjie5.ime diff --git a/assets/keylayout/zh_cn_cangjie5.ime b/assets/keylayout/zh_cn_cangjie5.ime new file mode 100644 index 000000000..9e61faed0 --- /dev/null +++ b/assets/keylayout/zh_cn_cangjie5.ime @@ -0,0 +1,318 @@ +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-sc.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 getCandidatesUsingBuf = () => { + 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}` +} +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 (1 == states.code && " " == cjkey) { + let ret = (1 == states.code) ? states.candidates[0] : (''+states.buf) + reset() + return ['', ret] + } + else if (states.code < 2 && states.buf.length < 5 && 97 <= cjkeyAsc && cjkeyAsc <= 122 || cjkeyAsc == 42) { + states.buf += cjkey + return [getCandidatesUsingBuf(), ''] + } + else { + let ret = ''+states.buf+cjkey + reset() + return ['', ret] + } +}, +"backspace":()=>{ + if (states.buf.length <= 1) { + reset() + return '' + } + + states.buf = states.buf.substring(0, states.buf.length - 1) + return getCandidatesUsingBuf() +}, +"end":()=>{ + let ret = (1 == states.code) ? states.candidates[0] : (''+states.buf) + reset() + return ret +}, +"reset":()=>{ reset() }, +"composing":()=>(states.code!=0), +"maxCandidates":()=>10 +}) \ No newline at end of file diff --git a/assets/keylayout/zh_tw_cangjie5.ime b/assets/keylayout/zh_tw_cangjie5.ime index e13128943..1be48e3b6 100644 --- a/assets/keylayout/zh_tw_cangjie5.ime +++ b/assets/keylayout/zh_tw_cangjie5.ime @@ -4,16 +4,16 @@ let states = {"keylayouts":[[""],[undefined], [undefined], [undefined], [undefined], -["0",")"], -["1","!"], +["0",")"], +["1","!"], ["2","@"], ["3","#"], -["4","$"], +["4","¥"], ["5","%"], -["6","^"], +["6","…"], ["7","&"], -["8","*"], -["9","("], +["8","×"], +["9","("], ["*"], ["#"], [undefined], @@ -52,26 +52,26 @@ let states = {"keylayouts":[[""],[undefined], ["x","X"], ["y","Y"], ["z","Z"], -[",","<"], -[".",">"], +[",","《"], +["。","》"], [undefined], [undefined], [undefined], [undefined], [undefined], -[" "], +[" ", " "], [undefined], [undefined], [undefined], ["\n"], ["\x08"], -["`","~"], -["-","_"], +["·","~"], +["-","—"], ["=","+"], -["[","{"], -["]","}"], -["\\","|"], -[";",":"], +["「","{"], +["」","}"], +["、","|"], +[";",":"], ["'",'"'], ["/","?"], [undefined], @@ -263,8 +263,12 @@ let reset = () => { 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(' ') +let getCandidatesUsingBuf = () => { + 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}` +} return Object.freeze({"n":"五倉正體 Qwerty","states":states,"c":"CuriousTo\uA75Bvald, 倉頡之友 。馬來西亞 http://www.chinesecj.com", // return: [displayed output, composed output] "accept":(headkey,shiftin,altgrin)=>{ @@ -279,31 +283,32 @@ return Object.freeze({"n":"五倉正體 Qwerty","states":states,"c":"CuriousTo\u reset() return ['', selection || raw] } - else if (97 <= cjkeyAsc && cjkeyAsc <= 122 || cjkeyAsc == 42) { + else if (1 == states.code && " " == cjkey) { + let ret = (1 == states.code) ? states.candidates[0] : (''+states.buf) + reset() + return ['', ret] + } + else if (states.code < 2 && states.buf.length < 5 && 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}`, ''] + return [getCandidatesUsingBuf(), ''] } else { - states.code = 0 - return ['', ''+states.buf+cjkey] + let ret = ''+states.buf+cjkey + reset() + return ['', ret] } - - return ['', cjkey] }, "backspace":()=>{ + if (states.buf.length <= 1) { + reset() + return '' + } - - return '' + states.buf = states.buf.substring(0, states.buf.length - 1) + return getCandidatesUsingBuf() }, "end":()=>{ -// console.log(`end composing`) - let ret = ''+states.buf + let ret = (1 == states.code) ? states.candidates[0] : (''+states.buf) reset() return ret }, diff --git a/src/net/torvald/terrarum/gamecontroller/IME.kt b/src/net/torvald/terrarum/gamecontroller/IME.kt index ae82d827b..bcc89c8be 100644 --- a/src/net/torvald/terrarum/gamecontroller/IME.kt +++ b/src/net/torvald/terrarum/gamecontroller/IME.kt @@ -56,12 +56,12 @@ object IME { 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") }.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") }.forEach { + 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) } diff --git a/src/net/torvald/terrarum/gamecontroller/IMEProviderDelegate.kt b/src/net/torvald/terrarum/gamecontroller/IMEProviderDelegate.kt index 54701b1c6..8199bf9b2 100644 --- a/src/net/torvald/terrarum/gamecontroller/IMEProviderDelegate.kt +++ b/src/net/torvald/terrarum/gamecontroller/IMEProviderDelegate.kt @@ -1,5 +1,7 @@ package net.torvald.terrarum.gamecontroller +import net.torvald.terrarum.App.printdbg +import net.torvald.util.SortedArrayList import java.io.File import java.io.FileReader @@ -13,11 +15,14 @@ class IMEProviderDelegate(val ime: IME) { } -class IMEDictionary(filename: String) { +class IMEDictionary(private val filename: String) { - private val candidates = HashMap() + private val candidates = HashMap(16384) + private val keys = SortedArrayList(16384) - init { + private var dictLoaded = false + + private fun loadDict() { val reader = FileReader(File("assets/keylayout/", filename)) reader.forEachLine { val (key, value) = it.split(',') @@ -26,10 +31,38 @@ class IMEDictionary(filename: String) { } else { candidates[key] = value + keys.add(key) } } + + printdbg(this, "Dictionary loaded: $filename") + + dictLoaded = true } - operator fun get(key: String): String = candidates[key] ?: "" + init { + loadDict() // loading the dict doesn't take too long so no need to do it lazily + } + + operator fun get(key: String): String { + //if (!dictLoaded) loadDict() + + val out = StringBuilder() + var outsize = 0 + var index = keys.searchForInterval(key) { it }.second + + while (outsize < 10) { + val keysym = keys[index] + if (!keysym.startsWith(key)) break + + val outstr = ",${candidates[keysym]}" + outsize += outstr.count { it == ',' } + out.append(outstr) + + index += 1 + } + + return if (out.isNotEmpty()) out.substring(1) else "" + } } \ No newline at end of file diff --git a/src/net/torvald/terrarum/ui/UIItemTextLineInput.kt b/src/net/torvald/terrarum/ui/UIItemTextLineInput.kt index 7c3fd96e5..481e515ef 100644 --- a/src/net/torvald/terrarum/ui/UIItemTextLineInput.kt +++ b/src/net/torvald/terrarum/ui/UIItemTextLineInput.kt @@ -7,6 +7,7 @@ import com.badlogic.gdx.graphics.OrthographicCamera import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.glutils.FrameBuffer +import com.jme3.math.FastMath import net.torvald.terrarum.* import net.torvald.terrarum.gamecontroller.IME import net.torvald.terrarum.gamecontroller.IngameController @@ -121,6 +122,9 @@ class UIItemTextLineInput( private var imeOn = false private var candidates: List = listOf() + private val candidatesBackCol = TEXTINPUT_COL_BACKGROUND.cpy().mul(1f,1f,1f,1.5f) + private val candidateNumberStrWidth = App.fontGame.getWidth("8. ") + private fun getIME(): TerrarumInputMethod? { if (!imeOn) return null @@ -228,7 +232,7 @@ class UIItemTextLineInput( // accept: // - literal "<" // - keysymbol that does not start with "<" (not always has length of 1 because UTF-16) - else if (char != null && char[0].code >= 32 && (char == "<" || !char.startsWith("<"))) { + else if (char != null && char.length > 0 && char[0].code >= 32 && (char == "<" || !char.startsWith("<"))) { val shiftin = keycodes.contains(Input.Keys.SHIFT_LEFT) || keycodes.contains(Input.Keys.SHIFT_RIGHT) val altgrin = keycodes.contains(Input.Keys.ALT_RIGHT) @@ -343,8 +347,6 @@ class UIItemTextLineInput( return textbuf.toJavaString() } - private val candidateNumberStrWidth = App.fontGame.getWidth("8. ") - override fun render(batch: SpriteBatch, camera: Camera) { batch.end() @@ -406,7 +408,7 @@ class UIItemTextLineInput( batch.draw(fbo.colorBufferTexture, posX + 2f, posY + 2f, fbo.width.toFloat(), fbo.height.toFloat()) // draw text cursor - val cursorXOnScreen = posX - cursorDrawScroll + cursorDrawX + 3 + val cursorXOnScreen = posX - cursorDrawScroll + cursorDrawX + 2 if (isActive && cursorOn) { val baseCol = if (maxLen.exceeds(textbuf, listOf(32))) TEXTINPUT_COL_TEXT_NOMORE else TEXTINPUT_COL_TEXT @@ -441,39 +443,56 @@ class UIItemTextLineInput( // draw candidates view if (candidates.isNotEmpty()) { val textWidths = candidates.map { App.fontGame.getWidth(CodepointSequence(it)) } - val candidateWinH = App.fontGame.lineHeight.toInt() * candidates.size + val candidatesMax = getIME()!!.maxCandidates() + val candidatesCount = minOf(candidatesMax, candidates.size) + val halfcount = FastMath.ceil(candidatesCount / 2f) + val candidateWinH = App.fontGame.lineHeight.toInt() * halfcount + val candidatePosX = cursorXOnScreen + 4 + val candidatePosY = posY + 2 // candidate view text - if (getIME()!!.maxCandidates() > 1) { - val candidateWinW = textWidths.maxOrNull()!!.coerceAtLeast(20) + candidateNumberStrWidth + if (candidatesMax > 1) { + val longestCandidateW = textWidths.maxOrNull()!! + candidateNumberStrWidth + val candidateWinW = if (candidatesCount == 1) longestCandidateW else 2*longestCandidateW + 3 // candidate view background - batch.color = TEXTINPUT_COL_BACKGROUND - Toolkit.fillArea(batch, cursorXOnScreen + 2, posY + 27, candidateWinW + 4, candidateWinH) + batch.color = candidatesBackCol + Toolkit.fillArea(batch, candidatePosX, candidatePosY, candidateWinW + 4, candidateWinH) // candidate view border batch.color = Toolkit.Theme.COL_ACTIVE - Toolkit.drawBoxBorder(batch, cursorXOnScreen + 1, posY + 26, candidateWinW + 6, candidateWinH + 2) + Toolkit.drawBoxBorder(batch, candidatePosX - 1, candidatePosY - 1, candidateWinW + 6, candidateWinH + 2) - for (i in 0..minOf(9, candidates.lastIndex)) { + // candidate texts + for (i in 0 until candidatesCount) { val candidateNum = listOf(i+48,46,32) - App.fontGame.draw(batch, CodepointSequence(candidateNum + candidates[i]), cursorXOnScreen + 4, posY + 27 + i * 20) + App.fontGame.draw(batch, CodepointSequence(candidateNum + candidates[i]), + candidatePosX + (i / halfcount) * (longestCandidateW + 3) + 2, + candidatePosY + (i % halfcount) * 20 + ) + } + + // candidate view splitter + if (candidatesCount > 1) { + batch.color = batch.color.cpy().mul(0.65f,0.65f,0.65f,1f) + Toolkit.fillArea(batch, candidatePosX + longestCandidateW + 2, candidatePosY, 1, candidateWinH) } } else { - val candidateWinW = textWidths.maxOrNull()!!.coerceAtLeast(20) + val candidateWinW = textWidths.maxOrNull()!!.coerceAtLeast(6) // candidate view background - batch.color = TEXTINPUT_COL_BACKGROUND - Toolkit.fillArea(batch, cursorXOnScreen + 2, posY + 27, candidateWinW, candidateWinH) + batch.color = candidatesBackCol + Toolkit.fillArea(batch, candidatePosX, candidatePosY, candidateWinW, candidateWinH) // candidate view border batch.color = Toolkit.Theme.COL_ACTIVE - Toolkit.drawBoxBorder(batch, cursorXOnScreen + 1, posY + 26, candidateWinW + 2, candidateWinH + 2) + Toolkit.drawBoxBorder(batch, candidatePosX - 1, candidatePosY - 1, candidateWinW + 2, candidateWinH + 2) val previewTextWidth = textWidths[0] - App.fontGame.draw(batch, candidates[0], cursorXOnScreen + 2 + (candidateWinW - previewTextWidth) / 2, posY + 27) + App.fontGame.draw(batch, candidates[0], candidatePosX + (candidateWinW - previewTextWidth) / 2, candidatePosY) } } + batch.color = Color.WHITE super.render(batch, camera) } diff --git a/src/net/torvald/util/SortedArrayList.kt b/src/net/torvald/util/SortedArrayList.kt index dddfd067b..daa6704fa 100644 --- a/src/net/torvald/util/SortedArrayList.kt +++ b/src/net/torvald/util/SortedArrayList.kt @@ -115,6 +115,33 @@ class SortedArrayList>(initialSize: Int = 10) : MutableCollecti return null // key not found } + /** + * e.g. + * + * 0 2 4 5 7 , find 3 + * + * will return (1, 2), which corresponds value (2, 4) of which input value 3 is in between. + */ + fun > searchForInterval(searchQuery: R, searchHow: (T) -> R): Pair { + var low: Int = 0 + var high: Int = this.size - 1 + + while (low <= high) { + val mid = (low + high).ushr(1) + val midVal = searchHow(get(mid)) + + if (searchQuery < midVal) + high = mid - 1 + else if (searchQuery > midVal) + low = mid + 1 + else + return Pair(mid, mid) + } + + val first = Math.max(high, 0) + val second = Math.min(low, this.size - 1) + return Pair(first, second) + } /** Searches the element using given predicate instead of the element itself. Returns the element desired, null when there is no such element. * (e.g. search the Actor by its ID rather than the actor instance) *