272 lines
7.7 KiB
Kotlin
272 lines
7.7 KiB
Kotlin
package net.mezimmah.wkt9.inputhandler
|
|
|
|
import android.content.Context
|
|
import android.text.InputType
|
|
import android.util.Log
|
|
import android.view.KeyEvent
|
|
import android.view.textservice.SentenceSuggestionsInfo
|
|
import android.view.textservice.SpellCheckerSession
|
|
import android.view.textservice.SuggestionsInfo
|
|
import android.view.textservice.TextServicesManager
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.SupervisorJob
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import net.mezimmah.wkt9.R
|
|
import net.mezimmah.wkt9.WKT9IME
|
|
import net.mezimmah.wkt9.IME
|
|
import net.mezimmah.wkt9.db.AppDatabase
|
|
import net.mezimmah.wkt9.entity.Word
|
|
import net.mezimmah.wkt9.inputmode.InputMode
|
|
import net.mezimmah.wkt9.keypad.Command
|
|
import net.mezimmah.wkt9.keypad.Key
|
|
import net.mezimmah.wkt9.keypad.KeyEventStat
|
|
import net.mezimmah.wkt9.keypad.KeyLayout
|
|
import java.util.Locale
|
|
|
|
class LetterInputHandler(
|
|
val ime: IME,
|
|
private var wkt9: WKT9IME,
|
|
private var locale: Locale,
|
|
private var composeTimeout: Long
|
|
): SpellCheckerSession.SpellCheckerSessionListener, DefaultInputHandler(wkt9), InputHandler {
|
|
private val db = AppDatabase.getInstance(wkt9)
|
|
private val wordDao = db.getWordDao()
|
|
|
|
private val word = StringBuilder()
|
|
|
|
private var lastKey: Key? = null
|
|
private var repeats: Int = 0
|
|
private var lastComposeChar: Char? = null
|
|
private var lastIcon: Int? = null
|
|
|
|
private var spellCheckerSession: SpellCheckerSession? = null
|
|
|
|
private var timeoutJob: Job? = null
|
|
|
|
init {
|
|
val textServiceManager = wkt9.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
|
|
|
|
spellCheckerSession = textServiceManager.newSpellCheckerSession(
|
|
null,
|
|
locale,
|
|
this,
|
|
false
|
|
)
|
|
|
|
updateIcon()
|
|
}
|
|
|
|
private fun finalizeWordOrSentence(stats: KeyEventStat) {
|
|
if (word.isNotEmpty()) storeWord()
|
|
|
|
timeoutJob?.cancel()
|
|
|
|
val index = stats.repeats % punctuationMarks.count()
|
|
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % punctuationMarks.count() else null
|
|
var beforeCursor = 0
|
|
|
|
if (lastIndex != null) beforeCursor += punctuationMarks[lastIndex].length
|
|
|
|
wordStart = true
|
|
sentenceStart = index in 1..3
|
|
|
|
wkt9.onCommit(punctuationMarks[index], beforeCursor)
|
|
updateIcon()
|
|
}
|
|
|
|
private fun storeWord() {
|
|
val str = word.toString()
|
|
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
|
|
word.clear()
|
|
|
|
scope.launch {
|
|
val word = Word(
|
|
word = str,
|
|
code = keypad.getCodeForWord(str),
|
|
weight = 0,
|
|
length = str.length,
|
|
locale = locale.language
|
|
)
|
|
val result = wordDao.selectWord(str, locale.language)
|
|
|
|
if (result == null) wordDao.insert(word)
|
|
}
|
|
}
|
|
|
|
override fun onSwitchLocale(locale: Locale) {
|
|
this.locale = locale
|
|
}
|
|
|
|
override fun onDeleteWord(word: Word) {}
|
|
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
|
|
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
|
|
override fun onFinishComposing() {}
|
|
|
|
override fun toggleCapMode(key: Key) {
|
|
super.toggleCapMode(key)
|
|
|
|
updateIcon()
|
|
}
|
|
|
|
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
|
|
when (command) {
|
|
Command.CAP_MODE -> toggleCapMode(key)
|
|
Command.CHARACTER -> composeCharacter(key)
|
|
Command.DELETE -> delete()
|
|
Command.INPUT_MODE -> inputMode(key)
|
|
Command.NUMBER -> triggerOriginalKeyEvent(key)
|
|
// Command.RECORD -> wkt9.onRecord(true)
|
|
Command.SPACE -> finalizeWordOrSentence(stats)
|
|
// Command.TRANSCRIBE -> wkt9.onTranscribe()
|
|
else -> Log.d(tag, "Command not implemented: $command")
|
|
}
|
|
}
|
|
|
|
override fun onWordSelected(word: Word) {}
|
|
|
|
private fun composeCharacter(key: Key) {
|
|
val composing = timeoutJob?.isActive ?: false
|
|
|
|
timeoutJob?.cancel()
|
|
|
|
if (lastKey == key) {
|
|
repeats++
|
|
|
|
word.deleteAt(word.length - 1)
|
|
} else {
|
|
resetKey(key)
|
|
|
|
if (composing) finishComposingChar()
|
|
}
|
|
|
|
val layout = KeyLayout.chars[key] ?: return
|
|
val index = repeats % layout.count()
|
|
val char = when(effectiveCapMode()) {
|
|
null -> layout[index].toString()
|
|
else -> layout[index].uppercase()
|
|
}
|
|
|
|
lastComposeChar = layout[index]
|
|
|
|
word.append(char)
|
|
wkt9.onCompose(char)
|
|
setComposeTimeout()
|
|
}
|
|
|
|
private fun resetKey(key: Key? = null) {
|
|
lastKey = key
|
|
repeats = 0
|
|
}
|
|
|
|
private fun finishComposingChar() {
|
|
val wordDelimiters = listOf(' ', ',', ':', ';')
|
|
val sentenceDelimiters = listOf('.', '?', '!')
|
|
|
|
wkt9.onCommit()
|
|
|
|
if (sentenceDelimiters.contains(lastComposeChar)) {
|
|
sentenceStart = true
|
|
wordStart = true
|
|
} else if (wordDelimiters.contains(lastComposeChar)) {
|
|
sentenceStart = false
|
|
wordStart = true
|
|
} else {
|
|
wordStart = false
|
|
sentenceStart = false
|
|
}
|
|
|
|
if (wordStart) {
|
|
word.deleteAt(word.length - 1)
|
|
|
|
if (word.isNotEmpty()) storeWord()
|
|
}
|
|
|
|
updateIcon()
|
|
}
|
|
|
|
private fun setComposeTimeout() {
|
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
|
|
timeoutJob = scope.launch {
|
|
delay(composeTimeout)
|
|
resetKey()
|
|
finishComposingChar()
|
|
}
|
|
}
|
|
|
|
private fun delete() {
|
|
if (word.isNotEmpty()) word.deleteAt(word.length - 1)
|
|
|
|
resetKey()
|
|
|
|
if (timeoutJob?.isActive == true) {
|
|
timeoutJob?.cancel()
|
|
wkt9.onCompose("")
|
|
} else {
|
|
wkt9.onDeleteText(1)
|
|
}
|
|
}
|
|
|
|
private fun inputMode(key: Key) {
|
|
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Word)
|
|
else wkt9.onSwitchInputHandler(InputMode.Number)
|
|
}
|
|
|
|
private fun updateIcon() {
|
|
val icon = when (effectiveCapMode()) {
|
|
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> R.drawable.letter_upper
|
|
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
|
|
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> R.drawable.letter_cap
|
|
else -> R.drawable.letter_lower
|
|
}
|
|
|
|
if (icon == lastIcon) return
|
|
|
|
wkt9.onUpdateStatusIcon(icon)
|
|
|
|
lastIcon = icon
|
|
}
|
|
}
|
|
//
|
|
// override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
|
|
// results?.map {
|
|
// val suggestions = it.getSuggestionsInfoAt(0)
|
|
//
|
|
// for (index in 0 until suggestions.suggestionsCount) {
|
|
// val suggestion = suggestions.getSuggestionAt(index)
|
|
//
|
|
// candidates.add(suggestion)
|
|
// }
|
|
// }
|
|
//
|
|
// if (candidates.isEmpty()) return
|
|
//
|
|
// candidateSource = CandidateSource.Dictionary
|
|
//
|
|
// wkt9.onCandidates(
|
|
// candidates = candidates,
|
|
// current = null
|
|
// )
|
|
// }
|
|
|
|
// private fun getSuggestions() {
|
|
// val lastWord = getLastWord()
|
|
//
|
|
// if (lastWord.length < 3) return
|
|
//
|
|
// val words = arrayOf(
|
|
// TextInfo(
|
|
// lastWord.plus("#"), // Add hash to string to get magic performance
|
|
// 0,
|
|
// lastWord.length + 1, // We added the hash, remember
|
|
// 0,
|
|
// 0
|
|
// )
|
|
// )
|
|
//
|
|
// spellCheckerSession?.getSentenceSuggestions(words, 15)
|
|
// } |