Compare commits

...

8 Commits

22 changed files with 456 additions and 248 deletions

Binary file not shown.

View File

@@ -15,7 +15,7 @@ interface IME {
current: Int? = 0
)
fun onWords(words: List<Word>)
fun onWords(words: List<Word>, capMode: Int?)
fun onNextWord()
@@ -23,15 +23,21 @@ interface IME {
fun onWordSelected(word: Word)
fun onDeleteWord(word: Word)
fun onCommit(text: CharSequence = "", beforeCursor: Int = 0, afterCursor: Int = 0)
fun onCompose(text: CharSequence)
fun onDeleteText(beforeCursor: Int = 0, afterCursor: Int = 0, finishComposing: Boolean = false)
fun onGetTextBeforeCursor(n: Int): CharSequence?
fun defaultView()
fun onSwitchInputHandler(inputMode: InputMode)
fun onUpdateStatusIcon(icon: Int?)
fun record()
fun transcribe()
}

View File

@@ -2,8 +2,10 @@ package net.mezimmah.wkt9
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.inputmethodservice.InputMethodService
import android.provider.Settings
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import android.view.View
@@ -12,19 +14,23 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.view.inputmethod.InputMethodSubtype
import androidx.preference.PreferenceManager
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputhandler.IdleInputHandler
import net.mezimmah.wkt9.inputhandler.InputHandler
import net.mezimmah.wkt9.inputhandler.LetterInputHandler
import net.mezimmah.wkt9.inputhandler.NumberInputHandler
import net.mezimmah.wkt9.inputhandler.WordInputHandler
import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.inputmode.InputModeManager
import net.mezimmah.wkt9.keypad.Event
import net.mezimmah.wkt9.keypad.Key
import net.mezimmah.wkt9.keypad.KeyEventStat
import net.mezimmah.wkt9.layout.Words
import net.mezimmah.wkt9.layout.LoadingLayout
import net.mezimmah.wkt9.layout.MessageLayout
import net.mezimmah.wkt9.layout.WordsLayout
import net.mezimmah.wkt9.t9.T9
import net.mezimmah.wkt9.voice.Whisper
import java.util.Locale
@@ -32,11 +38,14 @@ class WKT9IME: IME, InputMethodService() {
private val tag = "WKT9"
private val inputModeManager = InputModeManager(this)
private val whisper: Whisper = Whisper(this)
private lateinit var locale: Locale
private var inputHandler: InputHandler? = null
private var wordsView: Words? = null
private var wordsLayoutView: WordsLayout? = null
private var loadingLayoutView: LoadingLayout? = null
private var messageLayoutView: MessageLayout? = null
private val keyDownStats = KeyEventStat(0, 0)
private val keyUpStats = KeyEventStat(0, 0)
@@ -45,6 +54,8 @@ class WKT9IME: IME, InputMethodService() {
private var selectionStart: Int = 0
private var selectionEnd: Int = 0
private var capMode: Int? = null
override fun onCandidates(candidates: ArrayList<CharSequence>, current: Int?) {
// this.candidates?.load(candidates, current)
}
@@ -83,9 +94,11 @@ class WKT9IME: IME, InputMethodService() {
@SuppressLint("InflateParams")
override fun onCreateInputView(): View? {
wordsView = layoutInflater.inflate(R.layout.words, null) as Words
wordsLayoutView = layoutInflater.inflate(R.layout.words, null) as WordsLayout
loadingLayoutView = layoutInflater.inflate(R.layout.loading, null) as LoadingLayout
messageLayoutView = layoutInflater.inflate(R.layout.message, null) as MessageLayout
return wordsView
return wordsLayoutView
}
override fun onCurrentInputMethodSubtypeChanged(newSubtype: InputMethodSubtype?) {
@@ -103,8 +116,8 @@ class WKT9IME: IME, InputMethodService() {
deleteText(beforeCursor, afterCursor)
}
override fun onGetTextBeforeCursor(n: Int): CharSequence? {
return this.currentInputConnection?.getTextBeforeCursor(n, 0)
override fun onDeleteWord(word: Word) {
inputHandler?.onDeleteWord(word)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@@ -264,21 +277,30 @@ class WKT9IME: IME, InputMethodService() {
else showStatusIcon(icon)
}
override fun onWords(words: List<Word>) {
wordsView?.words = words
override fun onWords(words: List<Word>, capMode: Int?) {
this.capMode = capMode
wordsLayoutView?.words = words
}
override fun onWordSelected(word: Word) {
this.onCompose(word.word)
val compose = when (capMode) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> word.word.uppercase()
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> word.word.replaceFirstChar { it.uppercase() }
else -> word.word
}
this.onCompose(compose)
this.inputHandler?.onWordSelected(word)
}
override fun onNextWord() {
wordsView?.next()
wordsLayoutView?.next()
}
override fun onPreviousWord() {
wordsView?.previous()
wordsLayoutView?.previous()
}
private fun deleteText(beforeCursor: Int, afterCursor: Int) {
@@ -287,8 +309,22 @@ class WKT9IME: IME, InputMethodService() {
}
}
override fun record() {
setInputView(messageLayoutView)
whisper.record()
}
override fun transcribe() {
setInputView(loadingLayoutView)
whisper.transcribe()
}
override fun defaultView() {
setInputView(wordsLayoutView)
}
private fun finishComposing() {
wordsView?.clear()
wordsLayoutView?.clear()
inputHandler?.onFinishComposing()
}
@@ -301,7 +337,15 @@ class WKT9IME: IME, InputMethodService() {
private fun switchInputMode(mode: InputMode) {
inputHandler = when(mode) {
InputMode.Word -> WordInputHandler(this, this, locale)
InputMode.Letter -> LetterInputHandler(this, this, locale)
InputMode.Letter -> {
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val timeout = prefs.getString(getString(R.string.compose_timeout), "700")
val composeTimeout = timeout?.toLong() ?: 700L
timeout?.let { LetterInputHandler(this, this, locale, composeTimeout) }
}
InputMode.Number -> NumberInputHandler(this, this)
else -> IdleInputHandler(this, this)
}

View File

@@ -11,8 +11,8 @@ interface WordDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(vararg words: Word)
@Query("DELETE FROM word WHERE word = :word AND locale = :locale")
fun delete(word: String, locale: String)
@Query("DELETE FROM word WHERE id = :id")
fun delete(id: Int)
@Query("SELECT * FROM word WHERE code LIKE :code || '%' AND locale = :locale " +
"ORDER BY length, weight DESC LIMIT :limit")
@@ -20,4 +20,7 @@ interface WordDao {
@Query("UPDATE word SET weight = weight + 1 WHERE id=:id")
suspend fun increaseWeight(id: Int)
@Query("SELECT * FROM word WHERE word = :word AND locale = :locale COLLATE NOCASE")
suspend fun selectWord(word: String, locale: String): Word?
}

View File

@@ -3,79 +3,77 @@ package net.mezimmah.wkt9.inputhandler
import android.text.InputType
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.inputmode.TextPositionInfo
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 net.mezimmah.wkt9.keypad.Keypad
import java.util.Locale
open class DefaultInputHandler(
private val wkt9: IME,
private val context: WKT9IME
private val wkt9: WKT9IME
) {
protected val tag = "WKT9"
protected var keypad: Keypad = Keypad()
protected var capMode: Int = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
protected open fun capMode(key: Key): Int? {
val modes = listOf(
private val capModes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
null
)
var index = modes.indexOf(capMode)
private var currentCapMode: Int? = null
protected val punctuationMarks = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
protected val sentenceDelimiters = listOf('.', '?', '!')
protected val wordDelimiters = listOf('\t', '\n', ' ', ',', ':', ';')
protected val tag = "WKT9"
protected val keypad: Keypad = Keypad()
protected var wordStart: Boolean = false
protected var sentenceStart: Boolean = false
init {
setCursorPositionStatus()
wkt9.currentInputEditorInfo?.let {
val inputType = it.inputType
val typeFlags = inputType.and(InputType.TYPE_MASK_FLAGS)
if (typeFlags.and(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) == InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) {
currentCapMode = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
} else if (typeFlags.and(InputType.TYPE_TEXT_FLAG_CAP_WORDS) == InputType.TYPE_TEXT_FLAG_CAP_WORDS) {
currentCapMode = InputType.TYPE_TEXT_FLAG_CAP_WORDS
} else if (typeFlags.and(InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) == InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) {
currentCapMode = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
}
}
}
protected fun effectiveCapMode(): Int? {
return when (currentCapMode) {
InputType.TYPE_TEXT_FLAG_CAP_WORDS -> {
if (wordStart) InputType.TYPE_TEXT_FLAG_CAP_WORDS
else null
}
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> {
if (sentenceStart) InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
else null
}
else -> currentCapMode
}
}
protected open fun toggleCapMode(key: Key) {
var index = capModes.indexOf(currentCapMode)
when (key) {
Key.B2 -> {
if (index == 0) index = modes.count()
if (index == 0) index = capModes.count()
index--
}
else -> index++
}
return modes[index % modes.count()]
}
protected open fun finalizeWordOrSentence(stats: KeyEventStat) {
val candidates = ArrayList<CharSequence>()
wkt9.onCandidates(
candidates = candidates,
current = stats.repeats % candidates.count()
)
}
protected open fun getTextPositionInfo(text: CharSequence): TextPositionInfo {
val trimmed = text.trimEnd()
val regex = "[.!?]$".toRegex()
val startSentence = text.isEmpty() || regex.containsMatchIn(trimmed)
val startWord = text.isEmpty() || (startSentence || trimmed.length < text.length)
return TextPositionInfo(
startSentence = startSentence,
startWord = startWord
)
}
protected fun getDefaultCapMode(typeFlags: Int): Int? {
val modes = listOf(
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
)
modes.forEach {
if (typeFlags.and(it) == it) return it
}
return null
currentCapMode = capModes[index % capModes.count()]
}
protected fun triggerKeyEvent(keyCode: Int) {
@@ -89,4 +87,27 @@ open class DefaultInputHandler(
protected fun triggerOriginalKeyEvent(key: Key) {
triggerKeyEvent(key.keyCode)
}
protected fun setCursorPositionStatus() {
val textBeforeCursor = getTextBeforeCursor()
if (
textBeforeCursor.isNullOrEmpty() ||
textBeforeCursor.trim().isEmpty() ||
sentenceDelimiters.contains(textBeforeCursor.trim().last())
) {
sentenceStart = true
wordStart = true
} else if (wordDelimiters.contains(textBeforeCursor.last())) {
sentenceStart = false
wordStart = true
} else {
sentenceStart = false
wordStart = false
}
}
private fun getTextBeforeCursor(): CharSequence? {
return wkt9.currentInputConnection?.getTextBeforeCursor(15, 0)
}
}

View File

@@ -6,6 +6,7 @@ import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.keypad.Command
import net.mezimmah.wkt9.keypad.Key
@@ -15,12 +16,14 @@ import java.util.Locale
class IdleInputHandler(
val ime: IME,
val wkt9: WKT9IME,
) : DefaultInputHandler(ime, wkt9), InputHandler {
) : DefaultInputHandler(wkt9), InputHandler {
init {
wkt9.showStatusIcon(R.drawable.idle_input)
}
override fun onFinishComposing() {}
override fun onLongClickCandidate(text: String) {
TODO("Not yet implemented")
}
override fun onDeleteWord(word: Word) {}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {

View File

@@ -8,9 +8,9 @@ import net.mezimmah.wkt9.keypad.KeyEventStat
import java.util.Locale
interface InputHandler {
fun onFinishComposing()
fun onDeleteWord(word: Word)
fun onLongClickCandidate(text: String)
fun onFinishComposing()
fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat)

View File

@@ -30,23 +30,22 @@ class LetterInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale,
): SpellCheckerSession.SpellCheckerSessionListener, DefaultInputHandler(ime, wkt9), InputHandler {
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 sentenceStart: Boolean = false
private var wordStart: Boolean = false
private val timeoutScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var timeoutJob: Job? = null
private val content = StringBuilder()
init {
val textServiceManager = wkt9.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) as TextServicesManager
@@ -56,85 +55,157 @@ class LetterInputHandler(
this,
false
)
updateIcon()
}
override fun finalizeWordOrSentence(stats: KeyEventStat) {
timeoutJob?.cancel()
override fun onDeleteWord(word: Word) {}
val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
val index = stats.repeats % ends.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % ends.count() else null
var beforeCursor = 0
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {}
if (lastIndex != null) beforeCursor += ends[lastIndex].length
wkt9.onCommit(ends[index], beforeCursor)
}
override fun onFinishComposing() {}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {}
override fun onSwitchLocale(locale: Locale) {
this.locale = locale
}
override fun onGetSuggestions(results: Array<out SuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onGetSentenceSuggestions(results: Array<out SentenceSuggestionsInfo>?) {
TODO("Not yet implemented")
}
override fun onFinishComposing() {
}
override fun onLongClickCandidate(text: String) {
TODO("Not yet implemented")
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
Command.CAP_MODE -> capMode(key)
Command.CHARACTER -> composeCharacter(key, stats)
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> composeCharacter(key)
Command.DELETE -> delete()
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
// Command.RECORD -> wkt9.onRecord(true)
Command.RECORD -> wkt9.record()
Command.SPACE -> finalizeWordOrSentence(stats)
// Command.TRANSCRIBE -> wkt9.onTranscribe()
Command.TRANSCRIBE -> wkt9.transcribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
override fun onWordSelected(word: Word) {}
private fun composeCharacter(key: Key, stats: KeyEventStat) {
if (lastKey == key) repeats++
else {
wkt9.onCommit()
override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
lastKey = key
repeats = 0
updateIcon()
}
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()
}
wkt9.onCompose(layout[index].toString())
lastComposeChar = layout[index]
word.append(char)
wkt9.onCompose(char)
setComposeTimeout()
}
private fun setComposeTimeout() {
private fun finalizeWordOrSentence(stats: KeyEventStat) {
if (word.isNotEmpty()) storeWord()
timeoutJob?.cancel()
timeoutJob = timeoutScope.launch {
delay(400L)
wkt9.onCommit("")
val index = stats.repeats % punctuationMarks.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % punctuationMarks.count() else null
var beforeCursor = 0
lastKey = null
if (lastIndex != null) beforeCursor += punctuationMarks[lastIndex].length
wordStart = true
sentenceStart = index in 1..3
wkt9.onCommit(punctuationMarks[index], beforeCursor)
updateIcon()
}
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun resetKey(key: Key? = null) {
lastKey = key
repeats = 0
}
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)
}
}
private fun finishComposingChar() {
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("")
@@ -149,14 +220,18 @@ class LetterInputHandler(
}
private fun updateIcon() {
val icon = when (capMode) {
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 -> if (wordStart) R.drawable.letter_cap else R.drawable.letter_lower
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> R.drawable.letter_cap
else -> R.drawable.letter_lower
}
// wkt9.onUpdateStatusIcon(icon)
if (icon == lastIcon) return
wkt9.onUpdateStatusIcon(icon)
lastIcon = icon
}
}
//

View File

@@ -4,6 +4,7 @@ import android.util.Log
import android.view.KeyEvent
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.IME
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.entity.Word
import net.mezimmah.wkt9.inputmode.InputMode
import net.mezimmah.wkt9.keypad.Command
@@ -17,9 +18,13 @@ class NumberInputHandler(
) : InputHandler {
private val tag = "WKT9"
init {
wkt9.showStatusIcon(R.drawable.number_input)
}
// We don't need to implement methods below
override fun onFinishComposing() {}
override fun onLongClickCandidate(text: String) {}
override fun onDeleteWord(word: Word) {}
override fun onSwitchLocale(locale: Locale) {}
override fun onWordSelected(word: Word) {}

View File

@@ -1,6 +1,7 @@
package net.mezimmah.wkt9.inputhandler
import android.annotation.SuppressLint
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
import kotlinx.coroutines.CoroutineScope
@@ -22,8 +23,8 @@ import java.util.Locale
class WordInputHandler(
val ime: IME,
private var wkt9: WKT9IME,
private var locale: Locale,
) : DefaultInputHandler(ime, wkt9), InputHandler {
private var locale: Locale
) : DefaultInputHandler(wkt9), InputHandler {
private val codeword = StringBuilder()
private var staleCodeword = false
private var lastSelectedWord: Word? = null
@@ -35,11 +36,17 @@ class WordInputHandler(
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var queryJob: Job? = null
override fun capMode(key: Key): Int? {
return super.capMode(key)
init {
updateIcon()
}
override fun finalizeWordOrSentence(stats: KeyEventStat) {
override fun toggleCapMode(key: Key) {
super.toggleCapMode(key)
updateIcon()
}
private fun finalizeWordOrSentence(stats: KeyEventStat) {
val ends = listOf(" ", ". ", "? ", "! ", ", ", ": ", "; ")
val index = stats.repeats % ends.count()
val lastIndex = if (stats.repeats > 0) (stats.repeats - 1) % ends.count() else null
@@ -48,10 +55,24 @@ class WordInputHandler(
if (lastIndex != null) beforeCursor += ends[lastIndex].length
wkt9.onCommit(ends[index], beforeCursor)
sentenceStart = index in 1..3
updateIcon()
}
override fun onDeleteWord(word: Word) {
ioScope.launch {
wordDao.delete(word.id)
handleCodewordChange(codeword)
}
}
override fun onSwitchLocale(locale: Locale) {
this.locale = locale
updateIcon()
}
override fun onFinishComposing() {
@@ -63,28 +84,36 @@ class WordInputHandler(
lastSelectedWord = null
}
override fun onLongClickCandidate(text: String) {
ioScope.launch {
wordDao.delete(text, locale.language)
handleCodewordChange(codeword)
}
}
override fun onRunCommand(command: Command, key: Key, event: KeyEvent, stats: KeyEventStat) {
when (command) {
// Command.CAP_MODE -> capMode(key)
Command.CAP_MODE -> toggleCapMode(key)
Command.CHARACTER -> buildCodeword(key)
Command.DELETE -> delete()
Command.DELETE -> delete(event.repeatCount)
Command.ENTER -> enter(key)
Command.FINISH_DELETE -> finishDelete()
Command.INPUT_MODE -> inputMode(key)
Command.MOVE_CURSOR -> moveCursor(key)
Command.NUMBER -> triggerOriginalKeyEvent(key)
Command.RECORD -> record()
Command.SPACE -> finalizeWordOrSentence(stats)
Command.TRANSCRIBE -> transcribe()
else -> Log.d(tag, "Command not implemented: $command")
}
}
private fun record() {
if (codeword.isNotEmpty()) {
wkt9.onCommit()
codeword.clear()
}
wkt9.record()
}
private fun transcribe() {
wkt9.transcribe()
}
override fun onWordSelected(word: Word) {
lastSelectedWord = word
}
@@ -101,12 +130,15 @@ class WordInputHandler(
handleCodewordChange(codeword)
}
private fun delete() {
private fun delete(repeatCount: Int) {
lastSelectedWord = null
if (codeword.length > 1) {
codeword.deleteAt(codeword.length - 1)
handleCodewordChange(codeword)
} else if (codeword.isNotEmpty() && repeatCount > 1) {
codeword.clear()
wkt9.onCompose("")
} else if (codeword.isNotEmpty()) {
codeword.clear()
wkt9.onCompose("")
@@ -115,6 +147,11 @@ class WordInputHandler(
}
}
private fun finishDelete() {
setCursorPositionStatus()
updateIcon()
}
private fun enter(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit("")
else triggerOriginalKeyEvent(key)
@@ -129,7 +166,7 @@ class WordInputHandler(
// The codeword is stale when it does not yield any candidates in the DB
staleCodeword = words.isEmpty()
if (words.isNotEmpty()) wkt9.onWords(words)
if (words.isNotEmpty()) wkt9.onWords(words, effectiveCapMode())
}
}
@@ -142,6 +179,8 @@ class WordInputHandler(
}
private fun inputMode(key: Key) {
if (codeword.isNotEmpty()) wkt9.onCommit()
if (key == Key.B4) wkt9.onSwitchInputHandler(InputMode.Number)
else wkt9.onSwitchInputHandler(InputMode.Letter)
}
@@ -154,15 +193,16 @@ class WordInputHandler(
@SuppressLint("DiscouragedApi")
private fun updateIcon() {
// val mode = when (capMode) {
// InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> "upper"
// InputType.TYPE_TEXT_FLAG_CAP_WORDS,
// InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> if (wordStart) "cap" else "lower"
// else -> "lower"
// }
// val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
// val icon = context.resources.getIdentifier(name, "drawable", context.packageName)
val mode = when (effectiveCapMode()) {
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS -> "upper"
InputType.TYPE_TEXT_FLAG_CAP_WORDS,
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES -> "cap"
else -> "lower"
}
// wkt9.onUpdateStatusIcon(icon)
val name = "word_${locale}_${mode}".replace('-', '_').lowercase()
val icon = wkt9.resources.getIdentifier(name, "drawable", wkt9.packageName)
wkt9.onUpdateStatusIcon(icon)
}
}

View File

@@ -7,6 +7,7 @@ enum class Command {
DELETE,
DIAL,
ENTER,
FINISH_DELETE,
INPUT_MODE,
MOVE_CURSOR,
NUMBER,

View File

@@ -133,8 +133,7 @@ enum class Key(
),
CommandMapping(
events = listOf(Event.keyDown),
inputModes = listOf(InputMode.Number),
inputModes = listOf(InputMode.Number, InputMode.Idle),
overrideConsume = true,
consume = null
)
@@ -389,6 +388,12 @@ enum class Key(
command = Command.DELETE
),
CommandMapping(
events = listOf(Event.afterShortDown, Event.afterLongDown),
inputModes = listOf(InputMode.Word, InputMode.Letter),
command = Command.FINISH_DELETE
),
CommandMapping(
inputModes = listOf(InputMode.Number),
overrideConsume = true,

View File

@@ -0,0 +1,9 @@
package net.mezimmah.wkt9.layout
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
class LoadingLayout(context: Context, attributeSet: AttributeSet): LinearLayout(context, attributeSet) {
}

View File

@@ -0,0 +1,9 @@
package net.mezimmah.wkt9.layout
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
class MessageLayout(context: Context, attributeSet: AttributeSet): LinearLayout(context, attributeSet) {
}

View File

@@ -1,10 +1,8 @@
package net.mezimmah.wkt9.layout
import android.annotation.SuppressLint
import android.app.PendingIntent.getActivity
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.ContextThemeWrapper
import android.view.View
import android.widget.HorizontalScrollView
@@ -15,8 +13,7 @@ import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.entity.Word
class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
private val tag = "WKT9"
class WordsLayout(context: Context, attributeSet: AttributeSet): HorizontalScrollView(context, attributeSet), View.OnClickListener, View.OnLongClickListener {
private var wkt9: WKT9IME
private var wordCount: Int = 0
private var current: Int = 0
@@ -94,7 +91,19 @@ class Words(context: Context, attributeSet: AttributeSet): HorizontalScrollView(
}
override fun onLongClick(v: View?): Boolean {
Log.d(tag, "We need to delete this word from the db")
val words = this.words ?: return true
val wordContainer = findViewById<LinearLayout>(R.id.words)
for (i in 0 until wordCount) {
val child: View = wordContainer.getChildAt(i)
if (v != child) continue
wordContainer.removeView(child)
wkt9.onDeleteWord(words[i])
break
}
return true
}

View File

@@ -2,17 +2,12 @@ package net.mezimmah.wkt9.voice
import android.media.MediaRecorder
import android.util.Log
import android.view.View
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.mezimmah.wkt9.R
import net.mezimmah.wkt9.WKT9IME
import net.mezimmah.wkt9.inputhandler.InputHandler
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
@@ -23,17 +18,11 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
class Whisper(
private val context: WKT9IME,
private val inputHandler: InputHandler?,
private val ui: View
private val wkt9: WKT9IME,
) {
private val tag = "WKT9"
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var ioJob: Job? = null
private var recorder: MediaRecorder? = null
private var recording: File? = null
@@ -47,19 +36,18 @@ class Whisper(
stopRecording()
val recording = this.recording ?: return
showTranscribing()
val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
ioJob?.cancel()
ioJob = ioScope.launch {
try {
val transcription = run(recording)
val mainScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
mainScope.launch {
showCandidates()
wkt9.onCommit(transcription)
wkt9.defaultView()
}
// inputHandler?.onInsertText(transcription.plus(" "))
} catch (e: IOException) {
Log.d(tag, "A failure occurred in the communication with the speech-to-text server", e)
}
@@ -70,9 +58,7 @@ class Whisper(
fun record() {
if (recorder != null) stopRecording()
showMessage()
recording = File.createTempFile("recording.3gp", null, context.cacheDir)
recording = File.createTempFile("recording.3gp", null, wkt9.cacheDir)
recorder = MediaRecorder().also {
it.setAudioSource(MediaRecorder.AudioSource.MIC)
it.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
@@ -88,36 +74,6 @@ class Whisper(
}
}
private fun showCandidates() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.VISIBLE
// loadingView.visibility = View.GONE
// messageView.visibility = View.GONE
}
private fun showMessage() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.GONE
// loadingView.visibility = View.GONE
// messageView.visibility = View.VISIBLE
}
private fun showTranscribing() {
// val candidatesView = ui.findViewById<HorizontalScrollView>(R.id.suggestion_container)
// val loadingView = ui.findViewById<LinearLayout>(R.id.loading_container)
// val messageView = ui.findViewById<LinearLayout>(R.id.message_container)
//
// candidatesView.visibility = View.GONE
// loadingView.visibility = View.VISIBLE
// messageView.visibility = View.GONE
}
private fun stopRecording() {
recorder?.run {
stop()

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.LoadingLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:theme="@style/Theme.AppCompat.DayNight"
android:gravity="bottom"
android:background="@color/black"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/suggestions"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="40dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing..." />
</net.mezimmah.wkt9.layout.LoadingLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<net.mezimmah.wkt9.layout.MessageLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
@@ -10,11 +10,11 @@
android:background="@color/black"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/suggestions"
style="?android:attr/progressBarStyleLarge"
<ImageView
android:layout_height="40dp"
android:layout_width="wrap_content"
android:layout_height="40dp" />
android:src="@drawable/mic"/>
<TextView
android:layout_width="wrap_content"
@@ -24,6 +24,6 @@
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400"
android:text="Transcribing, please wait..." />
android:text="Recording..." />
</LinearLayout>
</net.mezimmah.wkt9.layout.MessageLayout>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp">
<TextView
android:id="@+id/suggestion_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/suggestion_text"
android:minWidth="40dp"
android:paddingVertical="5dp"
android:paddingHorizontal="8dp"
android:textSize="20sp"
android:textFontWeight="400" />
</LinearLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<net.mezimmah.wkt9.layout.Words xmlns:android="http://schemas.android.com/apk/res/android"
<net.mezimmah.wkt9.layout.WordsLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
@@ -14,4 +14,4 @@
android:layout_height="44dp"
android:orientation="horizontal" />
</net.mezimmah.wkt9.layout.Words>
</net.mezimmah.wkt9.layout.WordsLayout>

View File

@@ -29,6 +29,6 @@
<item>400</item>
<item>600</item>
<item>800</item>
<item>900</item>
<item>1000</item>
</string-array>
</resources>

View File

@@ -20,6 +20,12 @@
android:languageTag="es-ES"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="Spanish AR"
android:imeSubtypeLocale="es_AR"
android:languageTag="es-AR"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="German DE"
android:imeSubtypeLocale="de_DE"
@@ -32,4 +38,10 @@
android:languageTag="pt-PT"
android:imeSubtypeMode="keyboard" />
<subtype
android:label="Portuguese BR"
android:imeSubtypeLocale="pt_BR"
android:languageTag="pt-BR"
android:imeSubtypeMode="keyboard" />
</input-method>