first hangul ime

This commit is contained in:
minjaesong
2021-10-22 15:40:41 +09:00
parent 026ea1d9f3
commit 1c370ac610
5 changed files with 560 additions and 27 deletions

View File

@@ -100,7 +100,8 @@ object DefaultConfig {
"fx_differential" to false,
//"fx_3dlut" to false,
"basekeyboardlayout" to "us_qwerty"
"basekeyboardlayout" to "us_qwerty",
"inputmethod" to "none"
// settings regarding debugger

View File

@@ -5,8 +5,16 @@ import java.io.File
data class TerrarumKeyLayout(
val name: String,
val symbols: Array<Array<String?>>?,
val acceptChar: ((Int) -> String?)? = null
val symbols: Array<Array<String?>>?
)
data class TerrarumInputMethod(
val name: String,
// (keycodes, shiftin, altgrin)
val acceptChar: (IntArray, Boolean, Boolean) -> Pair<String, String>, // Pair<Display Char, Output Char if any>
val endCompose: () -> String,
val reset: () -> Unit,
val composing: () -> Boolean
)
/**
@@ -26,8 +34,10 @@ object IME {
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, TerrarumInputMethod>()
private val context = org.graalvm.polyglot.Context.newBuilder("js")
.allowHostAccess(org.graalvm.polyglot.HostAccess.NONE)
@@ -40,6 +50,11 @@ object IME {
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 {
printdbg(this, "Registering High layer ${it.nameWithoutExtension.lowercase()}")
highLayers[it.nameWithoutExtension.lowercase()] = parseImeFile(it)
}
}
fun invoke() {}
@@ -48,15 +63,22 @@ object IME {
return lowLayers[name.lowercase()]!!
}
fun getHighLayerByName(name: String): TerrarumInputMethod {
return highLayers[name.lowercase()]!!
}
fun getAllLowLayers(): List<String> {
return lowLayers.keys.toList()
}
fun getAllHighLayers(): List<String> {
return highLayers.keys.toList()
}
private fun parseKeylayoutFile(file: File): TerrarumKeyLayout {
val src = file.readText(Charsets.UTF_8)
val jsval = context.eval("js", "Object.freeze($src)")
val jsval = context.eval("js", "'use strict';Object.freeze($src)")
val name = jsval.getMember("n").asString()
val out = Array(256) { Array<String?>(4) { null } }
for (keycode in 0L until 256L) {
@@ -78,4 +100,23 @@ object IME {
return TerrarumKeyLayout(name, out)
}
private fun parseImeFile(file: File): TerrarumInputMethod {
val src = file.readText(Charsets.UTF_8)
val jsval = context.eval("js", "'use strict';$src")
val name = jsval.getMember("n").asString()
return TerrarumInputMethod(name, { it, shifted, alted ->
val a = jsval.invokeMember("accept", context.eval("js", "'${it.joinToString(",")}'.split(',')"), shifted, alted)
a.getArrayElement(0).asString() to a.getArrayElement(1).asString()
}, {
jsval.invokeMember("end").asString()
}, {
jsval.invokeMember("reset")
}, {
jsval.invokeMember("composing").asBoolean()
}
)
}
}

View File

@@ -138,19 +138,29 @@ class UIKeyboardControlPanel(remoCon: UIRemoCon?) : UICanvas() {
private val lowLayerNames = lowLayerCodes.map { { IME.getLowLayerByName(it).name } }
private val keyboardLayoutSelection = UIItemTextSelector(this, drawX + width - textSelWidth - 3, 400, lowLayerNames, lowLayerCodes.linearSearch { it == App.getConfigString("basekeyboardlayout") }!!, textSelWidth)
private val imeCodes = listOf("null", "ko_kr_2set_standard", "ko_kr_3set_390")
private val imeNames = listOf({ "$EMDASH" },{ "표준 두벌식" },{ "세벌식 3-90" })
private val imeSelection = UIItemTextSelector(this, drawX + width - textSelWidth - 3, 440, imeNames, 0, textSelWidth)
private val imeCodes0 = IME.getAllHighLayers()
private val imeCodes = listOf("none") + IME.getAllHighLayers()
private val imeNames = listOf({"$EMDASH"}) + imeCodes0.map { { IME.getHighLayerByName(it).name } }
private val imeSelection = UIItemTextSelector(this, drawX + width - textSelWidth - 3, 440, imeNames, imeCodes.linearSearch { it == App.getConfigString("inputmethod") }!!, textSelWidth)
private val keyboardTestPanel = UIItemTextLineInput(this, drawX + (width - 480) / 2 + 3, 480, 474, enableIMEButton = true, enablePasteButton = true)
private val keyboardConfigItems = listOf(
keyboardLayoutSelection,
imeSelection,
keyboardTestPanel
)
init {
keyboardLayoutSelection.selectionChangeListener = {
App.setConfig("basekeyboardlayout", lowLayerCodes[it])
}
imeSelection.selectionChangeListener = {
App.setConfig("inputmethod", imeCodes[it])
}
keycaps.values.forEach { addUIitem(it) }
updateKeycaps()
@@ -161,9 +171,9 @@ class UIKeyboardControlPanel(remoCon: UIRemoCon?) : UICanvas() {
updateKeycaps()
}
addUIitem(keyboardLayoutSelection)
addUIitem(imeSelection)
addUIitem(keyboardTestPanel)
// addUIitem(keyboardLayoutSelection)
// addUIitem(imeSelection)
// addUIitem(keyboardTestPanel)
}
private fun resetKeyConfig() {
@@ -219,6 +229,9 @@ class UIKeyboardControlPanel(remoCon: UIRemoCon?) : UICanvas() {
if (keycapClicked >= 0 && controlSelected < 0) {
controlPalette.update(delta)
}
else {
keyboardConfigItems.forEach { it.update(delta) }
}
}
override fun renderUI(batch: SpriteBatch, camera: Camera) {
@@ -231,14 +244,19 @@ class UIKeyboardControlPanel(remoCon: UIRemoCon?) : UICanvas() {
batch.color = Color.WHITE
if (keycapClicked >= 0 && controlSelected < 0) {
controlPalette.render(batch, camera)
}
App.fontGame.draw(batch, Lang["MENU_LABEL_KEYBOARD_LAYOUT"], kbx + 1, keyboardLayoutSelection.initialY)
App.fontGame.draw(batch, Lang["MENU_LABEL_IME"], kbx + 1, imeSelection.initialY)
if (keycapClicked >= 0 && controlSelected < 0) {
controlPalette.render(batch, camera)
}
else {
keyboardConfigItems.forEach { it.render(batch, camera) }
}
val title = Lang["MENU_CONTROLS_KEYBOARD"]
batch.color = Color.WHITE
App.fontGame.draw(batch, title, drawX.toFloat() + (width - App.fontGame.getWidth(title)) / 2, drawY.toFloat())
}
@@ -252,11 +270,19 @@ class UIKeyboardControlPanel(remoCon: UIRemoCon?) : UICanvas() {
override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
buttonReset.touchDown(screenX, screenY, pointer, button)
keyboardConfigItems.forEach { it.touchDown(screenX, screenY, pointer, button) }
return true
}
override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean {
buttonReset.touchUp(screenX, screenY, pointer, button)
keyboardConfigItems.forEach { it.touchUp(screenX, screenY, pointer, button) }
return true
}
override fun scrolled(amountX: Float, amountY: Float): Boolean {
super.scrolled(amountX, amountY)
keyboardConfigItems.forEach { it.scrolled(amountX, amountY) }
return true
}

View File

@@ -8,10 +8,13 @@ import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.glutils.FrameBuffer
import net.torvald.terrarum.*
import net.torvald.terrarum.gamecontroller.IME
import net.torvald.terrarum.gamecontroller.IngameController
import net.torvald.terrarum.gamecontroller.TerrarumInputMethod
import net.torvald.terrarum.utils.Clipboard
import net.torvald.terrarumsansbitmap.gdx.CodepointSequence
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
import java.util.ArrayList
import kotlin.streams.toList
data class InputLenCap(val count: Int, val unit: CharLenUnit) {
@@ -87,7 +90,11 @@ class UIItemTextLineInput(
true
)
var isActive = true
var isActive: Boolean = true
set(value) {
resetIME()
field = value
}
var cursorX = 0
var cursorDrawScroll = 0
@@ -113,6 +120,21 @@ class UIItemTextLineInput(
get() = buttonsShown > 0 && relativeMouseX in btn2PosX - posX until btn2PosX - posX + WIDTH_ONEBUTTON && relativeMouseY in 0 until height
private var imeOn = false
private var composingView = CodepointSequence()
private fun getIME(): TerrarumInputMethod? {
if (!imeOn) return null
val selectedIME = App.getConfigString("inputmethod")
if (selectedIME == "none") return null
try {
return IME.getHighLayerByName(selectedIME)
}
catch (e: NullPointerException) {
return null
}
}
private fun forceLitCursor() {
cursorBlinkCounter = 0f
@@ -139,13 +161,14 @@ class UIItemTextLineInput(
isActive = mouseUp
}
// TODO cursorDrawX kerning-aware
if (App.getConfigString("inputmethod") == "none") imeOn = false
// process keypresses
if (isActive) {
IngameController.withKeyboardEvent { (_, char, _, keycodes) ->
fboUpdateLatch = true
forceLitCursor()
val ime = getIME()
if (keycodes.contains(Input.Keys.V) && (keycodes.contains(Input.Keys.CONTROL_LEFT) || keycodes.contains(Input.Keys.CONTROL_RIGHT))) {
paste()
@@ -154,18 +177,37 @@ class UIItemTextLineInput(
else if (keycodes.contains(Input.Keys.C) && (keycodes.contains(Input.Keys.CONTROL_LEFT) || keycodes.contains(Input.Keys.CONTROL_RIGHT))) {
copyToClipboard()
}
else if (cursorX > 0 && keycodes.contains(Input.Keys.BACKSPACE)) {
cursorX -= 1
textbuf.removeAt(cursorX)
cursorDrawX = App.fontGame.getWidth(textbuf.subList(0, cursorX))
tryCursorForward()
else if (keycodes.contains(Input.Keys.BACKSPACE)) {
if (ime != null && composingView.size > 0) {
resetIME()
}
else if (cursorX <= 0) {
cursorX = 0
cursorDrawX = 0
cursorDrawScroll = 0
}
else {
if (cursorX > 0) {
while (true) {
cursorX -= 1
val oldCode = textbuf.removeAt(cursorX)
// continue deleting hangul pieces because of the font...
if (cursorX == 0 || (oldCode !in 0x115F..0x11FF && oldCode !in 0xD7B0..0xD7FF)) break
}
cursorDrawX = App.fontGame.getWidth(textbuf.subList(0, cursorX))
tryCursorForward()
}
}
}
else if (cursorX > 0 && keycodes.contains(Input.Keys.LEFT)) {
// TODO IME endComposing()
cursorX -= 1
cursorDrawX = App.fontGame.getWidth(textbuf.subList(0, cursorX))
tryCursorForward()
}
else if (cursorX < textbuf.size && keycodes.contains(Input.Keys.RIGHT)) {
// TODO IME endComposing()
cursorX += 1
cursorDrawX = App.fontGame.getWidth(textbuf.subList(0, cursorX))
tryCursorBack()
@@ -174,7 +216,18 @@ class UIItemTextLineInput(
// - 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("<"))) {
val codepoints = char.toCodePoints()
val shiftin = keycodes.contains(Input.Keys.SHIFT_LEFT) || keycodes.contains(Input.Keys.SHIFT_RIGHT)
val altgrin = keycodes.contains(Input.Keys.ALT_RIGHT)
val codepoints = if (ime != null) {
val newStatus = ime.acceptChar(keycodes, shiftin, altgrin)
composingView = CodepointSequence(newStatus.first.toCodePoints())
newStatus.second.toCodePoints()
}
else char.toCodePoints()
println("textinput codepoints: ${codepoints.map { it.toString(16) }.joinToString()}")
if (!maxLen.exceeds(textbuf, codepoints)) {
textbuf.addAll(cursorX, codepoints)
@@ -191,7 +244,7 @@ class UIItemTextLineInput(
}
if (textbuf.size == 0) {
currentPlaceholderText = ArrayList(placeholder().toCodePoints())
currentPlaceholderText = CodepointSequence(placeholder().toCodePoints())
}
cursorBlinkCounter += delta
@@ -214,13 +267,27 @@ class UIItemTextLineInput(
if (!mouseDown) mouseLatched = false
}
private fun String.toCodePoints() = this.codePoints().toList()
private fun String.toCodePoints() = this.codePoints().toList().filter { it > 0 }
private fun toggleIME() {
if (App.getConfigString("inputmethod") == "none") {
imeOn = false
return
}
imeOn = !imeOn
resetIME()
}
private fun resetIME() {
getIME()?.reset?.invoke()
composingView = CodepointSequence()
}
private fun paste() {
resetIME()
val codepoints = Clipboard.fetch().substringBefore('\n').substringBefore('\t').toCodePoints()
val actuallyInserted = arrayListOf(0)
@@ -306,20 +373,20 @@ class UIItemTextLineInput(
}
// draw text
batch.color = if (textbuf.isEmpty()) TEXTINPUT_COL_TEXT_DISABLED else TEXTINPUT_COL_TEXT
batch.draw(fbo.colorBufferTexture, posX + 2f, posY + 2f, fbo.width.toFloat(), fbo.height.toFloat())
// draw text cursor
val cursorXOnScreen = posX - cursorDrawScroll + cursorDrawX + 3
if (isActive && cursorOn) {
val baseCol = if (maxLen.exceeds(textbuf, listOf(32))) TEXTINPUT_COL_TEXT_NOMORE else TEXTINPUT_COL_TEXT
batch.color = baseCol.cpy().mul(0.5f,0.5f,0.5f,1f)
Toolkit.fillArea(batch, posX - cursorDrawScroll + cursorDrawX + 3, posY, 2, 24)
Toolkit.fillArea(batch, cursorXOnScreen, posY, 2, 24)
batch.color = baseCol
Toolkit.fillArea(batch, posX - cursorDrawScroll + cursorDrawX + 3, posY, 1, 23)
Toolkit.fillArea(batch, cursorXOnScreen, posY, 1, 23)
}
// draw icon
@@ -343,6 +410,19 @@ class UIItemTextLineInput(
}
// compose view background
if (composingView.size > 0) {
val previewTextWidth = App.fontGame.getWidth(composingView)
val previewWindowWidth = previewTextWidth.coerceAtLeast(20)
batch.color = TEXTINPUT_COL_BACKGROUND
Toolkit.fillArea(batch, cursorXOnScreen + 2, posY + 27, previewWindowWidth, 20)
// compose view border
batch.color = Toolkit.Theme.COL_ACTIVE
Toolkit.drawBoxBorder(batch, cursorXOnScreen + 1, posY + 26, previewWindowWidth + 2, 22)
// compose view text
App.fontGame.draw(batch, composingView, cursorXOnScreen + 2 + (previewWindowWidth - previewTextWidth) / 2, posY + 27)
}
super.render(batch, camera)
}
@@ -353,5 +433,13 @@ class UIItemTextLineInput(
fbo.dispose()
}
/*private fun CodepointSequence.toJavaString(): String {
val sb = StringBuilder()
this.forEach {
sb.append(Character.toChars(it))
}
return sb.toString()
}*/
}
}