diff --git a/app/build.gradle b/app/build.gradle index 34416de5..7c72366c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "com.kazumaproject.markdownhelperkeyboard" minSdk 24 targetSdk 36 - versionCode 798 - versionName "1.7.105" + versionCode 799 + versionName "1.7.106" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/assets/emoji/token_emoji.dat b/app/src/main/assets/emoji/token_emoji.dat index 93d79505..97777b3b 100644 Binary files a/app/src/main/assets/emoji/token_emoji.dat and b/app/src/main/assets/emoji/token_emoji.dat differ diff --git a/app/src/main/assets/emoticon/token_emoticon.dat b/app/src/main/assets/emoticon/token_emoticon.dat index 2337abc2..059c162f 100644 Binary files a/app/src/main/assets/emoticon/token_emoticon.dat and b/app/src/main/assets/emoticon/token_emoticon.dat differ diff --git a/app/src/main/assets/kotowaza/token_kotowaza.dat b/app/src/main/assets/kotowaza/token_kotowaza.dat index 51b1574e..00a86ee6 100644 Binary files a/app/src/main/assets/kotowaza/token_kotowaza.dat and b/app/src/main/assets/kotowaza/token_kotowaza.dat differ diff --git a/app/src/main/assets/neologd/token_neologd.dat.zip b/app/src/main/assets/neologd/token_neologd.dat.zip index 68e803cd..7fb15b8b 100644 Binary files a/app/src/main/assets/neologd/token_neologd.dat.zip and b/app/src/main/assets/neologd/token_neologd.dat.zip differ diff --git a/app/src/main/assets/ngram/system_ngram.dat b/app/src/main/assets/ngram/system_ngram.dat index a145dc2b..832c9bdc 100644 Binary files a/app/src/main/assets/ngram/system_ngram.dat and b/app/src/main/assets/ngram/system_ngram.dat differ diff --git a/app/src/main/assets/person_name/token_person_names.dat b/app/src/main/assets/person_name/token_person_names.dat index 17a5c1bf..2f28b0c0 100644 Binary files a/app/src/main/assets/person_name/token_person_names.dat and b/app/src/main/assets/person_name/token_person_names.dat differ diff --git a/app/src/main/assets/places/token_places.dat.zip b/app/src/main/assets/places/token_places.dat.zip index 02035885..f1cd285b 100644 Binary files a/app/src/main/assets/places/token_places.dat.zip and b/app/src/main/assets/places/token_places.dat.zip differ diff --git a/app/src/main/assets/pos_table.dat b/app/src/main/assets/pos_table.dat index 088cdf73..df3a6852 100644 Binary files a/app/src/main/assets/pos_table.dat and b/app/src/main/assets/pos_table.dat differ diff --git a/app/src/main/assets/reading_correction/token_reading_correction.dat b/app/src/main/assets/reading_correction/token_reading_correction.dat index c1515e35..a17f213b 100644 Binary files a/app/src/main/assets/reading_correction/token_reading_correction.dat and b/app/src/main/assets/reading_correction/token_reading_correction.dat differ diff --git a/app/src/main/assets/single_kanji/token_singleKanji.dat b/app/src/main/assets/single_kanji/token_singleKanji.dat index 250d6e39..3e43e23b 100644 Binary files a/app/src/main/assets/single_kanji/token_singleKanji.dat and b/app/src/main/assets/single_kanji/token_singleKanji.dat differ diff --git a/app/src/main/assets/symbol/token_symbol.dat b/app/src/main/assets/symbol/token_symbol.dat index e2901e7a..d6c7cd78 100644 Binary files a/app/src/main/assets/symbol/token_symbol.dat and b/app/src/main/assets/symbol/token_symbol.dat differ diff --git a/app/src/main/assets/system/tango.dat.zip b/app/src/main/assets/system/tango.dat.zip index fe1fbec1..f8c9e144 100644 Binary files a/app/src/main/assets/system/tango.dat.zip and b/app/src/main/assets/system/tango.dat.zip differ diff --git a/app/src/main/assets/system/token.dat.zip b/app/src/main/assets/system/token.dat.zip index 554d01bb..6a9c6fb5 100644 Binary files a/app/src/main/assets/system/token.dat.zip and b/app/src/main/assets/system/token.dat.zip differ diff --git a/app/src/main/assets/system/yomi.dat.zip b/app/src/main/assets/system/yomi.dat.zip index 3138e535..c491c387 100644 Binary files a/app/src/main/assets/system/yomi.dat.zip and b/app/src/main/assets/system/yomi.dat.zip differ diff --git a/app/src/main/assets/web/token_web.dat.zip b/app/src/main/assets/web/token_web.dat.zip index 9ae9fa57..e4d34b16 100644 Binary files a/app/src/main/assets/web/token_web.dat.zip and b/app/src/main/assets/web/token_web.dat.zip differ diff --git a/app/src/main/assets/wiki/token_wiki.dat.zip b/app/src/main/assets/wiki/token_wiki.dat.zip index eeabda90..6152ac31 100644 Binary files a/app/src/main/assets/wiki/token_wiki.dat.zip and b/app/src/main/assets/wiki/token_wiki.dat.zip differ diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/KanaKanjiEngine.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/KanaKanjiEngine.kt index 9af6c650..3231b31b 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/KanaKanjiEngine.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/KanaKanjiEngine.kt @@ -84,6 +84,25 @@ private const val POS_ID_NUMBER_ARABIC: Short = 2044 private const val POS_ID_NUMBER_SEPARATED: Short = 2045 private const val POS_ID_NUMBER_KANJI: Short = 2046 +internal fun shouldIncludeSymbolEmojiReading( + inputLength: Int, + readingLength: Int, + predictionConfig: PredictionConfig, +): Boolean = readingLength == inputLength || ( + predictionConfig.japanesePredictionEnabled && + predictionConfig.symbolEmojiEnabled && + predictionConfig.acceptsJapaneseReading(inputLength, readingLength) + ) + +internal fun createJapaneseNumberValueBasedCandidates( + input: String, + showSymbolCandidates: Boolean = true, +): List { + if (!showSymbolCandidates) return emptyList() + val numberValue = input.toNumber()?.second?.toLongOrNull() ?: return emptyList() + return createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) +} + class KanaKanjiEngine { data class IncrementalPerformanceSnapshot( @@ -1253,7 +1272,7 @@ class KanaKanjiEngine { rightId = 2040 ) - val valueBasedCandidates = if (numberValue != null) { + val valueBasedCandidates = if (predictionConfig.showSymbolCandidates && numberValue != null) { createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) } else { emptyList() @@ -1281,6 +1300,7 @@ class KanaKanjiEngine { yomiTrie = emojiYomiTrie, succinctBitVector = emojiSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmojiCandidates, ) val emoticonCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -1288,6 +1308,7 @@ class KanaKanjiEngine { yomiTrie = emoticonYomiTrie, succinctBitVector = emoticonSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmoticonCandidates, ) val symbolCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -1295,6 +1316,7 @@ class KanaKanjiEngine { yomiTrie = symbolYomiTrie, succinctBitVector = symbolSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showSymbolCandidates, ) val emojiListDeferred = deferredFromDictionarySymbols( @@ -1354,7 +1376,7 @@ class KanaKanjiEngine { ) val symbolCommonPrefixDeferredHalfWidth = - if (input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) + if (predictionConfig.showSymbolCandidates && input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) else emptyList() val symbolHalfWidthListDeferred = @@ -1549,7 +1571,10 @@ class KanaKanjiEngine { else -> emptyList() } - val numbersDeferred = generateNumberCandidates(input) + val numbersDeferred = generateNumberCandidates( + input = input, + showSymbolCandidates = predictionConfig.showSymbolCandidates, + ) val mozcUTPersonNames = if (mozcUtPersonName == true) getMozcUTPersonNames(input, predictionConfig) else emptyList() @@ -1761,7 +1786,7 @@ class KanaKanjiEngine { rightId = 2040 ) - val valueBasedCandidates = if (numberValue != null) { + val valueBasedCandidates = if (predictionConfig.showSymbolCandidates && numberValue != null) { createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) } else { emptyList() @@ -1800,6 +1825,7 @@ class KanaKanjiEngine { yomiTrie = emojiYomiTrie, succinctBitVector = emojiSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmojiCandidates, ) val emoticonCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -1807,6 +1833,7 @@ class KanaKanjiEngine { yomiTrie = emoticonYomiTrie, succinctBitVector = emoticonSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmoticonCandidates, ) val symbolCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -1814,6 +1841,7 @@ class KanaKanjiEngine { yomiTrie = symbolYomiTrie, succinctBitVector = symbolSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showSymbolCandidates, ) val emojiListDeferred = deferredFromDictionarySymbols( @@ -1873,7 +1901,7 @@ class KanaKanjiEngine { ) val symbolCommonPrefixDeferredHalfWidth = - if (input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) + if (predictionConfig.showSymbolCandidates && input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) else emptyList() val symbolHalfWidthListDeferred = @@ -2076,7 +2104,10 @@ class KanaKanjiEngine { else -> emptyList() } - val numbersDeferred = generateNumberCandidates(input) + val numbersDeferred = generateNumberCandidates( + input = input, + showSymbolCandidates = predictionConfig.showSymbolCandidates, + ) val mozcUTPersonNames = if (mozcUtPersonName == true) getMozcUTPersonNames(input, predictionConfig) else emptyList() @@ -2298,7 +2329,7 @@ class KanaKanjiEngine { rightId = 2040 ) - val valueBasedCandidates = if (numberValue != null) { + val valueBasedCandidates = if (predictionConfig.showSymbolCandidates && numberValue != null) { createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) } else { emptyList() @@ -2379,11 +2410,56 @@ class KanaKanjiEngine { emptyList() } + val emojiCommonPrefixDeferred = deferredPredictionEmojiSymbols( + input = input, + yomiTrie = emojiYomiTrie, + succinctBitVector = emojiSuccinctBitVectorLBSYomi, + predictionConfig = predictionConfig, + enabled = predictionConfig.showEmojiCandidates, + ) + + val emoticonCommonPrefixDeferred = deferredPredictionEmojiSymbols( + input = input, + yomiTrie = emoticonYomiTrie, + succinctBitVector = emoticonSuccinctBitVectorLBSYomi, + predictionConfig = predictionConfig, + enabled = predictionConfig.showEmoticonCandidates, + ) + val symbolCommonPrefixDeferred = deferredPredictionEmojiSymbols( input = input, yomiTrie = symbolYomiTrie, succinctBitVector = symbolSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showSymbolCandidates, + ) + + val emojiListDeferred = deferredFromDictionarySymbols( + input = input, + commonPrefixListString = emojiCommonPrefixDeferred, + yomiTrie = emojiYomiTrie, + tokenArray = emojiTokenArray, + tangoTrie = emojiTangoTrie, + succinctBitVectorLBSYomi = emojiSuccinctBitVectorLBSYomi, + succinctBitVectorIsLeafYomi = emojiSuccinctBitVectorIsLeafYomi, + succinctBitVectorTokenArray = emojiSuccinctBitVectorTokenArray, + succinctBitVectorTangoLBS = emojiSuccinctBitVectorTangoLBS, + type = 11, + predictionConfig = predictionConfig, + ) + + val emoticonListDeferred = deferredFromDictionarySymbols( + input = input, + commonPrefixListString = emoticonCommonPrefixDeferred, + yomiTrie = emoticonYomiTrie, + tokenArray = emoticonTokenArray, + tangoTrie = emoticonTangoTrie, + succinctBitVectorLBSYomi = emoticonSuccinctBitVectorLBSYomi, + succinctBitVectorIsLeafYomi = emoticonSuccinctBitVectorIsLeafYomi, + succinctBitVectorTokenArray = emoticonSuccinctBitVectorTokenArray, + succinctBitVectorTangoLBS = emoticonSuccinctBitVectorTangoLBS, + type = 12, + predictionConfig = predictionConfig, ) val symbolListDeferred = deferredFromDictionarySymbols( @@ -2402,7 +2478,7 @@ class KanaKanjiEngine { if (input.length == 1) { val finalListOneLetter = - resultNBestFinalDeferred.candidates.sortedBy { it.score } + (englishDeferred + englishZenkaku).sortedBy { it.score } + hirakanaAndKana + symbolListDeferred + singleKanjiListDeferred + resultNBestFinalDeferred.candidates.sortedBy { it.score } + (englishDeferred + englishZenkaku).sortedBy { it.score } + hirakanaAndKana + (emojiListDeferred + emoticonListDeferred).sortedBy { it.score } + symbolListDeferred + singleKanjiListDeferred return BunsetsuCandidateResult( candidates = finalListOneLetter, splitPatterns = resultNBestFinalDeferred.splitPatterns, @@ -2552,7 +2628,10 @@ class KanaKanjiEngine { else -> emptyList() } - val numbersDeferred = generateNumberCandidates(input) + val numbersDeferred = generateNumberCandidates( + input = input, + showSymbolCandidates = predictionConfig.showSymbolCandidates, + ) val mozcUTPersonNames = if (mozcUtPersonName == true) getMozcUTPersonNames(input, predictionConfig) else emptyList() @@ -2576,7 +2655,7 @@ class KanaKanjiEngine { ) val finalList = - resultListFinal + kotowazaListDeferred + (englishDeferred + englishZenkaku).sortedBy { it.score } + hirakanaAndKana + yomiPartListDeferred + symbolListDeferred + singleKanjiListDeferred + resultListFinal + kotowazaListDeferred + (englishDeferred + englishZenkaku).sortedBy { it.score } + (emojiListDeferred + emoticonListDeferred).sortedBy { it.score } + hirakanaAndKana + yomiPartListDeferred + symbolListDeferred + singleKanjiListDeferred return BunsetsuCandidateResult( candidates = finalList, @@ -2774,7 +2853,7 @@ class KanaKanjiEngine { rightId = 2040 ) - val valueBasedCandidates = if (numberValue != null) { + val valueBasedCandidates = if (predictionConfig.showSymbolCandidates && numberValue != null) { createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) } else { emptyList() @@ -2861,11 +2940,56 @@ class KanaKanjiEngine { emptyList() } + val emojiCommonPrefixDeferred = deferredPredictionEmojiSymbols( + input = input, + yomiTrie = emojiYomiTrie, + succinctBitVector = emojiSuccinctBitVectorLBSYomi, + predictionConfig = predictionConfig, + enabled = predictionConfig.showEmojiCandidates, + ) + + val emoticonCommonPrefixDeferred = deferredPredictionEmojiSymbols( + input = input, + yomiTrie = emoticonYomiTrie, + succinctBitVector = emoticonSuccinctBitVectorLBSYomi, + predictionConfig = predictionConfig, + enabled = predictionConfig.showEmoticonCandidates, + ) + val symbolCommonPrefixDeferred = deferredPredictionEmojiSymbols( input = input, yomiTrie = symbolYomiTrie, succinctBitVector = symbolSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showSymbolCandidates, + ) + + val emojiListDeferred = deferredFromDictionarySymbols( + input = input, + commonPrefixListString = emojiCommonPrefixDeferred, + yomiTrie = emojiYomiTrie, + tokenArray = emojiTokenArray, + tangoTrie = emojiTangoTrie, + succinctBitVectorLBSYomi = emojiSuccinctBitVectorLBSYomi, + succinctBitVectorIsLeafYomi = emojiSuccinctBitVectorIsLeafYomi, + succinctBitVectorTokenArray = emojiSuccinctBitVectorTokenArray, + succinctBitVectorTangoLBS = emojiSuccinctBitVectorTangoLBS, + type = 11, + predictionConfig = predictionConfig, + ) + + val emoticonListDeferred = deferredFromDictionarySymbols( + input = input, + commonPrefixListString = emoticonCommonPrefixDeferred, + yomiTrie = emoticonYomiTrie, + tokenArray = emoticonTokenArray, + tangoTrie = emoticonTangoTrie, + succinctBitVectorLBSYomi = emoticonSuccinctBitVectorLBSYomi, + succinctBitVectorIsLeafYomi = emoticonSuccinctBitVectorIsLeafYomi, + succinctBitVectorTokenArray = emoticonSuccinctBitVectorTokenArray, + succinctBitVectorTangoLBS = emoticonSuccinctBitVectorTangoLBS, + type = 12, + predictionConfig = predictionConfig, ) val symbolListDeferred = deferredFromDictionarySymbols( @@ -2882,7 +3006,7 @@ class KanaKanjiEngine { predictionConfig = predictionConfig, ) - if (input.length == 1) return resultNBestFinalDeferred.sortedBy { it.score } + (englishDeferred + englishZenkaku).sortedBy { it.score } + hirakanaAndKana + symbolListDeferred + singleKanjiListDeferred + if (input.length == 1) return resultNBestFinalDeferred.sortedBy { it.score } + (englishDeferred + englishZenkaku).sortedBy { it.score } + hirakanaAndKana + (emojiListDeferred + emoticonListDeferred).sortedBy { it.score } + symbolListDeferred + singleKanjiListDeferred val yomiPartOfDeferred = if (input.length > 16) { emptyList() @@ -3026,7 +3150,10 @@ class KanaKanjiEngine { else -> emptyList() } - val numbersDeferred = generateNumberCandidates(input) + val numbersDeferred = generateNumberCandidates( + input = input, + showSymbolCandidates = predictionConfig.showSymbolCandidates, + ) val mozcUTPersonNames = if (mozcUtPersonName == true) getMozcUTPersonNames(input, predictionConfig) else emptyList() @@ -3045,7 +3172,7 @@ class KanaKanjiEngine { val resultListFinal = resultList.sortedWith(compareBy { it.score }.thenBy { it.string }) - return resultListFinal + kotowazaListDeferred + (englishDeferred + englishZenkaku).sortedBy { it.score } + hirakanaAndKana + yomiPartListDeferred + symbolListDeferred + singleKanjiListDeferred + return resultListFinal + kotowazaListDeferred + (englishDeferred + englishZenkaku).sortedBy { it.score } + (emojiListDeferred + emoticonListDeferred).sortedBy { it.score } + hirakanaAndKana + yomiPartListDeferred + symbolListDeferred + singleKanjiListDeferred } @@ -3232,7 +3359,7 @@ class KanaKanjiEngine { rightId = 2040 ) - val valueBasedCandidates = if (numberValue != null) { + val valueBasedCandidates = if (predictionConfig.showSymbolCandidates && numberValue != null) { createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) } else { emptyList() @@ -3259,6 +3386,7 @@ class KanaKanjiEngine { yomiTrie = emojiYomiTrie, succinctBitVector = emojiSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmojiCandidates, ) val emoticonCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -3266,6 +3394,7 @@ class KanaKanjiEngine { yomiTrie = emoticonYomiTrie, succinctBitVector = emoticonSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmoticonCandidates, ) val symbolCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -3273,6 +3402,7 @@ class KanaKanjiEngine { yomiTrie = symbolYomiTrie, succinctBitVector = symbolSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showSymbolCandidates, ) val emojiListDeferred = deferredFromDictionarySymbols( @@ -3330,7 +3460,7 @@ class KanaKanjiEngine { ) val symbolCommonPrefixDeferredHalfWidth = - if (input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) + if (predictionConfig.showSymbolCandidates && input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) else emptyList() val symbolHalfWidthListDeferred = @@ -3520,7 +3650,10 @@ class KanaKanjiEngine { else -> emptyList() } - val numbersDeferred = generateNumberCandidates(input) + val numbersDeferred = generateNumberCandidates( + input = input, + showSymbolCandidates = predictionConfig.showSymbolCandidates, + ) val mozcUTPersonNames = if (mozcUtPersonName == true) getMozcUTPersonNames(input, predictionConfig) else emptyList() @@ -3728,7 +3861,7 @@ class KanaKanjiEngine { rightId = 2040 ) - val valueBasedCandidates = if (numberValue != null) { + val valueBasedCandidates = if (predictionConfig.showSymbolCandidates && numberValue != null) { createValueBasedSymbolCandidates(numberValue, input.length.toUByte()) } else { emptyList() @@ -3767,6 +3900,7 @@ class KanaKanjiEngine { yomiTrie = emojiYomiTrie, succinctBitVector = emojiSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmojiCandidates, ) val emoticonCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -3774,6 +3908,7 @@ class KanaKanjiEngine { yomiTrie = emoticonYomiTrie, succinctBitVector = emoticonSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showEmoticonCandidates, ) val symbolCommonPrefixDeferred = deferredPredictionEmojiSymbols( @@ -3781,6 +3916,7 @@ class KanaKanjiEngine { yomiTrie = symbolYomiTrie, succinctBitVector = symbolSuccinctBitVectorLBSYomi, predictionConfig = predictionConfig, + enabled = predictionConfig.showSymbolCandidates, ) val emojiListDeferred = deferredFromDictionarySymbols( @@ -3838,7 +3974,7 @@ class KanaKanjiEngine { ) val symbolCommonPrefixDeferredHalfWidth = - if (input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) + if (predictionConfig.showSymbolCandidates && input.all { !it.isLetterOrDigit() && !it.isWhitespace() }) listOf(input.convertFullWidthToHalfWidth()) else emptyList() val symbolHalfWidthListDeferred = @@ -4036,7 +4172,10 @@ class KanaKanjiEngine { else -> emptyList() } - val numbersDeferred = generateNumberCandidates(input) + val numbersDeferred = generateNumberCandidates( + input = input, + showSymbolCandidates = predictionConfig.showSymbolCandidates, + ) val mozcUTPersonNames = if (mozcUtPersonName == true) getMozcUTPersonNames(input, predictionConfig) else emptyList() @@ -5023,16 +5162,18 @@ class KanaKanjiEngine { yomiTrie: LOUDSWithTermId, succinctBitVector: SuccinctBitVector, predictionConfig: PredictionConfig, + enabled: Boolean, ): List { - if (!predictionConfig.japanesePredictionEnabled || !predictionConfig.symbolEmojiEnabled) { - return emptyList() - } + if (!enabled) return emptyList() if (input.length > PredictionConfig.MAX_PREDICTION_INPUT_LENGTH) return emptyList() return yomiTrie.predictiveSearch( prefix = input, succinctBitVector = succinctBitVector ).filter { - it.length == input.length || - predictionConfig.acceptsJapaneseReading(input.length, it.length) + shouldIncludeSymbolEmojiReading( + inputLength = input.length, + readingLength = it.length, + predictionConfig = predictionConfig, + ) } } @@ -5196,7 +5337,10 @@ class KanaKanjiEngine { ) } - private fun generateNumberCandidates(input: String): List { + private fun generateNumberCandidates( + input: String, + showSymbolCandidates: Boolean = true, + ): List { val numPair = input.toNumber() val expoPair = input.toNumberExponent() @@ -5272,6 +5416,11 @@ class KanaKanjiEngine { ) } + candidates += createJapaneseNumberValueBasedCandidates( + input = input, + showSymbolCandidates = showSymbolCandidates, + ) + candidates } else { diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/PredictionConfig.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/PredictionConfig.kt index b0a7c9a2..924870a7 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/PredictionConfig.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/PredictionConfig.kt @@ -25,6 +25,9 @@ data class PredictionConfig( val proverbEnabled: Boolean = true, val externalMozcEnabled: Boolean = true, val symbolEmojiEnabled: Boolean = true, + val showSymbolCandidates: Boolean = true, + val showEmojiCandidates: Boolean = true, + val showEmoticonCandidates: Boolean = true, ) { val normalizedMinimumInputLength: Int get() = minimumInputLength.coerceIn(MIN_INPUT_LENGTH, MAX_INPUT_LENGTH) diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt index 1836e5ba..df899ab0 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/IMEService.kt @@ -222,6 +222,7 @@ import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateS import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateQueryModeResolver import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateRefreshCoordinator import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateRefreshRequest +import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateRefreshTransitionPolicy import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateRequestToken import com.kazumaproject.markdownhelperkeyboard.ime_service.candidate.CandidateRequestTracker import com.kazumaproject.markdownhelperkeyboard.ime_service.clipboard.ClipboardUtil @@ -956,7 +957,13 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } suggestionAdapter?.submitContent(content) - suggestionAdapterFull?.submitContent(fullContent) + // The full candidate view is hidden during normal composing. Submitting to its + // AsyncListDiffer on every keystroke still calculates a complete DiffUtil diff even + // though the user cannot see it. Keep the state current, but submit only when that + // view is actually visible; the visibility transition below refreshes it once. + if (isFullCandidateViewVisible()) { + suggestionAdapterFull?.submitContent(fullContent) + } val presentation = resolveCandidateStripPresentation( candidatesShown = effectiveCandidatesShown, resetCandidateTabSelection = resetCandidateTabSelection, @@ -965,6 +972,14 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, applyCandidateStripPresentation(presentation) } + private fun isFullCandidateViewVisible(): Boolean { + return if (isKeyboardFloatingMode == true) { + floatingKeyboardBinding?.candidatesRowView?.isVisible == true + } else { + mainLayoutBinding?.candidatesRowView?.isVisible == true + } + } + private fun resolveCandidateStripContent( candidates: List, candidatesShown: Boolean, @@ -14645,10 +14660,11 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, prevFlag, currentFlag, ) - if ( - prevFlag == CandidateShowFlag.Idle && - currentFlag == CandidateShowFlag.Updating && - insertString.isNotEmpty() + if (CandidateRefreshTransitionPolicy.shouldEnterActiveCandidatePhase( + previousFlag = prevFlag, + currentFlag = currentFlag, + input = insertString, + ) ) { clearZeroQueryAllState(refresh = false) shortcutToolbarHiddenForCandidates = true @@ -14798,7 +14814,10 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, } } } - prevFlag = currentFlag + prevFlag = CandidateRefreshTransitionPolicy.nextUiPreviousFlag( + currentFlag = currentFlag, + input = insertString, + ) } } @@ -16480,6 +16499,11 @@ class IMEService : InputMethodService(), LifecycleOwner, InputConnection, mainView.suggestionVisibility.apply { this.setImageDrawable(if (isVisible) cachedArrowDropDownDrawable else cachedArrowDropUpDrawable) } + if (!isVisible) { + // The full candidate view was intentionally not diffed while hidden. Submit the + // latest state after it becomes the active surface. + refreshCandidateStripContent(candidatesShown = true) + } } private fun animateViewVisibility( diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt index e4f0dfd0..0749c5e5 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/ImePreferencesSnapshot.kt @@ -352,6 +352,12 @@ data class ImePreferencesSnapshot( appPreference.external_mozc_prediction_enable_preference, symbolEmojiEnabled = appPreference.symbol_emoji_prediction_enable_preference, + showSymbolCandidates = + appPreference.symbol_candidate_enable_preference, + showEmojiCandidates = + appPreference.emoji_candidate_enable_preference, + showEmoticonCandidates = + appPreference.emoticon_candidate_enable_preference, ), flickSensitivityPreferenceValue = appPreference.flick_sensitivity_preference ?: 100, flickThresholdShapePreferenceValue = diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt index dbf0daee..2e890951 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/adapters/SuggestionAdapter.kt @@ -526,24 +526,23 @@ class SuggestionAdapter internal constructor( return when { oldItem is SuggestionDisplayItem.CandidateItem && newItem is SuggestionDisplayItem.CandidateItem -> - oldItem.candidateIndex == newItem.candidateIndex && - oldItem.candidate.string == newItem.candidate.string && - oldItem.candidate.type == newItem.candidate.type + // Candidate rows are positional slots. The RecyclerView has no move + // animation, and onBindViewHolder installs the current candidate click + // listener, so a changed candidate at the same slot is a content change. + // Treating its text/type as identity made every keystroke look like a set + // of removals and insertions to DiffUtil. + oldItem.candidateIndex == newItem.candidateIndex oldItem is SuggestionDisplayItem.GemmaActionItem && newItem is SuggestionDisplayItem.GemmaActionItem -> - oldItem.candidateIndex == newItem.candidateIndex && - oldItem.candidate.string == newItem.candidate.string && - oldItem.candidate.type == newItem.candidate.type + oldItem.candidateIndex == newItem.candidateIndex oldItem is SuggestionDisplayItem.ZeroQueryCloseItem && newItem is SuggestionDisplayItem.ZeroQueryCloseItem -> true oldItem is SuggestionDisplayItem.ZeroQueryCandidateItem && newItem is SuggestionDisplayItem.ZeroQueryCandidateItem -> - oldItem.candidateIndex == newItem.candidateIndex && - oldItem.candidate.string == newItem.candidate.string && - oldItem.candidate.type == newItem.candidate.type + oldItem.candidateIndex == newItem.candidateIndex oldItem is SuggestionDisplayItem.QuickActionsItem && newItem is SuggestionDisplayItem.QuickActionsItem -> true diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshTransitionPolicy.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshTransitionPolicy.kt new file mode 100644 index 00000000..5a691ce5 --- /dev/null +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshTransitionPolicy.kt @@ -0,0 +1,34 @@ +package com.kazumaproject.markdownhelperkeyboard.ime_service.candidate + +import com.kazumaproject.markdownhelperkeyboard.ime_service.models.CandidateShowFlag + +/** + * Keeps non-user candidate refreshes from changing the visible conversion phase. + * + * Dictionary loading can publish an Updating request while the composing input is empty. That + * request refreshes data, but it must not consume the Idle -> Updating transition used by the + * candidate-strip UI to show the full-candidate toggle. + */ +internal object CandidateRefreshTransitionPolicy { + + fun shouldEnterActiveCandidatePhase( + previousFlag: CandidateShowFlag?, + currentFlag: CandidateShowFlag, + input: String, + ): Boolean { + return previousFlag == CandidateShowFlag.Idle && + currentFlag == CandidateShowFlag.Updating && + input.isNotEmpty() + } + + fun nextUiPreviousFlag( + currentFlag: CandidateShowFlag, + input: String, + ): CandidateShowFlag { + return if (currentFlag == CandidateShowFlag.Updating && input.isEmpty()) { + CandidateShowFlag.Idle + } else { + currentFlag + } + } +} diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt index 4bbad17d..6f99fb5f 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt @@ -88,6 +88,8 @@ object AppPreference { private lateinit var preferences: SharedPreferences private val gson = Gson() + private const val LEGACY_SYMBOL_EMOJI_CANDIDATE_ENABLE_KEY = + "symbol_emoji_candidate_enable_preference" private val circularSlotActionEditableSlots = setOf( CircularFlickDirection.SLOT_4, CircularFlickDirection.SLOT_5, @@ -156,6 +158,12 @@ object AppPreference { Pair("external_mozc_prediction_enable_preference", true) private val SYMBOL_EMOJI_PREDICTION_ENABLE_PREFERENCE = Pair("symbol_emoji_prediction_enable_preference", true) + private val SYMBOL_CANDIDATE_ENABLE_PREFERENCE = + Pair("symbol_candidate_enable_preference", true) + private val EMOJI_CANDIDATE_ENABLE_PREFERENCE = + Pair("emoji_candidate_enable_preference", true) + private val EMOTICON_CANDIDATE_ENABLE_PREFERENCE = + Pair("emoticon_candidate_enable_preference", true) private val CANDIDATE_ORDER_OVERRIDE_ENABLE = Pair("candidate_order_override_enable_preference", false) private val MOZCUT_PERSON_NAME = Pair("mozc_ut_person_name_preference", false) @@ -857,9 +865,30 @@ object AppPreference { preferences = PreferenceManager.getDefaultSharedPreferences(context) removeUnsafeLegacyGemmaHandwritingPrompt() migratePredictionLookaheadPreferenceIfNeeded() + migrateSymbolEmojiCandidatePreferenceIfNeeded() migrateSumireKeymapGuideModesIfNeeded() } + fun migrateSymbolEmojiCandidatePreferenceIfNeeded() { + if (!preferences.contains(LEGACY_SYMBOL_EMOJI_CANDIDATE_ENABLE_KEY)) return + val legacyValue = preferences.getBoolean( + LEGACY_SYMBOL_EMOJI_CANDIDATE_ENABLE_KEY, + true, + ) + preferences.edit { + if (!preferences.contains(SYMBOL_CANDIDATE_ENABLE_PREFERENCE.first)) { + it.putBoolean(SYMBOL_CANDIDATE_ENABLE_PREFERENCE.first, legacyValue) + } + if (!preferences.contains(EMOJI_CANDIDATE_ENABLE_PREFERENCE.first)) { + it.putBoolean(EMOJI_CANDIDATE_ENABLE_PREFERENCE.first, legacyValue) + } + if (!preferences.contains(EMOTICON_CANDIDATE_ENABLE_PREFERENCE.first)) { + it.putBoolean(EMOTICON_CANDIDATE_ENABLE_PREFERENCE.first, legacyValue) + } + it.remove(LEGACY_SYMBOL_EMOJI_CANDIDATE_ENABLE_KEY) + } + } + private fun migrateSumireKeymapGuideModesIfNeeded() { if (preferences.getBoolean(SUMIRE_KEYMAP_GUIDE_MIGRATION_KEY, false)) return val legacyGuideEnabled = preferences.getBoolean( @@ -1806,6 +1835,33 @@ object AppPreference { it.putBoolean(SYMBOL_EMOJI_PREDICTION_ENABLE_PREFERENCE.first, value) } + var symbol_candidate_enable_preference: Boolean + get() = preferences.getBoolean( + SYMBOL_CANDIDATE_ENABLE_PREFERENCE.first, + SYMBOL_CANDIDATE_ENABLE_PREFERENCE.second, + ) + set(value) = preferences.edit { + it.putBoolean(SYMBOL_CANDIDATE_ENABLE_PREFERENCE.first, value) + } + + var emoji_candidate_enable_preference: Boolean + get() = preferences.getBoolean( + EMOJI_CANDIDATE_ENABLE_PREFERENCE.first, + EMOJI_CANDIDATE_ENABLE_PREFERENCE.second, + ) + set(value) = preferences.edit { + it.putBoolean(EMOJI_CANDIDATE_ENABLE_PREFERENCE.first, value) + } + + var emoticon_candidate_enable_preference: Boolean + get() = preferences.getBoolean( + EMOTICON_CANDIDATE_ENABLE_PREFERENCE.first, + EMOTICON_CANDIDATE_ENABLE_PREFERENCE.second, + ) + set(value) = preferences.edit { + it.putBoolean(EMOTICON_CANDIDATE_ENABLE_PREFERENCE.first, value) + } + var candidate_order_override_enable_preference: Boolean? get() = preferences.getBoolean( CANDIDATE_ORDER_OVERRIDE_ENABLE.first, diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e5996e29..3580edab 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1546,6 +1546,12 @@ 有効な人名・地名・Wiki・NEologd・Web辞書を前方一致予測に使用します。 記号・絵文字・顔文字 記号、絵文字、顔文字の辞書を前方一致予測に使用します。 + 記号の変換候補 + 記号の変換候補を表示します。 + 絵文字の変換候補 + 絵文字の変換候補を表示します。 + 顔文字の変換候補 + 顔文字の変換候補を表示します。 予測候補数 ユーザー辞書から前方一致で取得する予測候補の最大数です。 学習辞書から前方一致で取得する予測候補の最大数です。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5bfb2e58..97682721 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1586,6 +1586,12 @@ Use enabled person, place, wiki, NEologd, and web dictionaries for completion. Symbols, emoji, and emoticons Use symbol, emoji, and emoticon dictionaries for completion candidates. + Symbol candidates + Show symbol conversion candidates. + Emoji candidates + Show emoji conversion candidates. + Emoticon candidates + Show emoticon conversion candidates. Prediction candidates Maximum number of prefix-match candidates fetched from the user dictionary. Maximum number of prefix-match candidates fetched from the learning dictionary. diff --git a/app/src/main/res/xml/pref_conversion_engine.xml b/app/src/main/res/xml/pref_conversion_engine.xml index 1bb3124c..6fa177d6 100644 --- a/app/src/main/res/xml/pref_conversion_engine.xml +++ b/app/src/main/res/xml/pref_conversion_engine.xml @@ -124,6 +124,27 @@ android:title="@string/symbol_emoji_prediction_enable_title" app:iconSpaceReserved="false" /> + + + + + + diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/SymbolEmojiCandidatePolicyTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/SymbolEmojiCandidatePolicyTest.kt new file mode 100644 index 00000000..b5ec7072 --- /dev/null +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/engine/SymbolEmojiCandidatePolicyTest.kt @@ -0,0 +1,67 @@ +package com.kazumaproject.markdownhelperkeyboard.converter.engine + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class SymbolEmojiCandidatePolicyTest { + + @Test + fun exactSymbolEmojiReadingSurvivesConversionMode() { + val conversionConfig = PredictionConfig( + japanesePredictionEnabled = false, + symbolEmojiEnabled = false, + ) + + assertTrue( + shouldIncludeSymbolEmojiReading( + inputLength = 3, + readingLength = 3, + predictionConfig = conversionConfig, + ) + ) + assertFalse( + shouldIncludeSymbolEmojiReading( + inputLength = 3, + readingLength = 4, + predictionConfig = conversionConfig, + ) + ) + } + + @Test + fun symbolEmojiSettingOnlyControlsCompletionReadings() { + val disabledConfig = PredictionConfig(symbolEmojiEnabled = false) + val enabledConfig = PredictionConfig(symbolEmojiEnabled = true) + + assertTrue( + shouldIncludeSymbolEmojiReading( + inputLength = 3, + readingLength = 3, + predictionConfig = disabledConfig, + ) + ) + assertTrue( + shouldIncludeSymbolEmojiReading( + inputLength = 3, + readingLength = 4, + predictionConfig = enabledConfig, + ) + ) + assertFalse( + shouldIncludeSymbolEmojiReading( + inputLength = 3, + readingLength = 4, + predictionConfig = disabledConfig, + ) + ) + } + + @Test + fun japaneseNumberReadingIncludesValueBasedSymbols() { + assertTrue( + createJapaneseNumberValueBasedCandidates("いち") + .any { it.string == "①" } + ) + } +} diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/session/KanaKanjiConversionSessionParityTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/session/KanaKanjiConversionSessionParityTest.kt index c6f107af..16962f6e 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/session/KanaKanjiConversionSessionParityTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/converter/session/KanaKanjiConversionSessionParityTest.kt @@ -2,10 +2,12 @@ package com.kazumaproject.markdownhelperkeyboard.converter.session import com.kazumaproject.markdownhelperkeyboard.converter.TestEngineFactory import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate +import com.kazumaproject.markdownhelperkeyboard.converter.engine.PredictionConfig import com.kazumaproject.markdownhelperkeyboard.repository.UserDictionaryRepository import com.kazumaproject.markdownhelperkeyboard.user_dictionary.database.UserWord import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.BeforeClass import org.junit.Test @@ -82,6 +84,179 @@ class KanaKanjiConversionSessionParityTest { assertEquals(null, result.bunsetsuResult) } + @Test + fun conversionKeepsExactSymbolEmojiEmoticonAndValueBasedNumberCandidates() = runBlocking { + val session = KanaKanjiConversionSession(engine, ConversionBackend.LEGACY) + + val neko = session.query(request("ねこ", CandidateQueryMode.CONVERSION, bunsetsu = false)) + assertTrue(neko.candidates.map { it.string }.any { it.contains("🐈") }) + + val niko = session.query(request("にこ", CandidateQueryMode.CONVERSION, bunsetsu = false)) + assertTrue(niko.candidates.map { it.string }.contains("(^o^)")) + + val ichi = session.query(request("いち", CandidateQueryMode.CONVERSION, bunsetsu = false)) + assertTrue(ichi.candidates.map { it.string }.contains("①")) + } + + @Test + fun predictionIncludesExactSymbolEmojiEmoticonCandidates() = runBlocking { + val session = KanaKanjiConversionSession(engine, ConversionBackend.LEGACY) + + val exactEmoji = session.query( + request("ねこ", CandidateQueryMode.PREDICTION, bunsetsu = false), + ) + assertTrue(exactEmoji.candidates.map { it.string }.any { it.contains("🐈") }) + + val exactEmojiWithBunsetsu = session.query( + request("ねこ", CandidateQueryMode.PREDICTION, bunsetsu = true), + ) + assertTrue(exactEmojiWithBunsetsu.candidates.map { it.string }.any { it.contains("🐈") }) + + val exactEmoticon = session.query( + request("にこ", CandidateQueryMode.PREDICTION, bunsetsu = false), + ) + assertTrue(exactEmoticon.candidates.map { it.string }.contains("(^o^)")) + + val exactEmoticonWithBunsetsu = session.query( + request("にこ", CandidateQueryMode.PREDICTION, bunsetsu = true), + ) + assertTrue(exactEmoticonWithBunsetsu.candidates.map { it.string }.contains("(^o^)")) + + val exactSymbol = session.query( + request("さんかく", CandidateQueryMode.PREDICTION, bunsetsu = false), + ) + assertTrue(exactSymbol.candidates.any { it.type.toInt() == 13 }) + + val prefixEmoji = session.query( + request("うれし", CandidateQueryMode.PREDICTION, bunsetsu = false), + ) + assertTrue(prefixEmoji.candidates.any { it.type.toInt() == 11 }) + + val prefixEmoticon = session.query( + request("にこに", CandidateQueryMode.PREDICTION, bunsetsu = false), + ) + assertTrue(prefixEmoticon.candidates.any { it.type.toInt() == 12 }) + + val prefixSymbol = session.query( + request("さんか", CandidateQueryMode.PREDICTION, bunsetsu = false), + ) + assertTrue(prefixSymbol.candidates.any { it.type.toInt() == 13 }) + + val disabledConfig = PredictionConfig(symbolEmojiEnabled = false) + val disabledPrefixEmoji = session.query( + request("うれし", CandidateQueryMode.PREDICTION, bunsetsu = false).copy( + predictionConfig = disabledConfig, + ), + ) + assertTrue(disabledPrefixEmoji.candidates.none { it.type.toInt() == 11 }) + + val disabledPrefixEmoticon = session.query( + request("にこに", CandidateQueryMode.PREDICTION, bunsetsu = false).copy( + predictionConfig = disabledConfig, + ), + ) + assertTrue(disabledPrefixEmoticon.candidates.none { it.type.toInt() == 12 }) + + val disabledPrefixSymbol = session.query( + request("さんか", CandidateQueryMode.PREDICTION, bunsetsu = false).copy( + predictionConfig = disabledConfig, + ), + ) + assertTrue(disabledPrefixSymbol.candidates.none { it.type.toInt() == 13 }) + } + + @Test + fun symbolEmojiCandidateDisplaySettingsHideCandidatesInEveryMode() = runBlocking { + val session = KanaKanjiConversionSession(engine, ConversionBackend.LEGACY) + val hiddenConfig = PredictionConfig( + showSymbolCandidates = false, + showEmojiCandidates = false, + showEmoticonCandidates = false, + ) + + for (mode in listOf( + CandidateQueryMode.PREDICTION, + CandidateQueryMode.CONVERSION, + CandidateQueryMode.NO_TAB_DEFAULT, + )) { + for (bunsetsu in listOf(false, true)) { + val neko = session.query( + request("ねこ", mode, bunsetsu).copy(predictionConfig = hiddenConfig), + ) + assertFalse( + "$mode/bunsetsu=$bunsetsu emoji", + neko.candidates.any { it.string.contains("🐈") }, + ) + + val niko = session.query( + request("にこ", mode, bunsetsu).copy(predictionConfig = hiddenConfig), + ) + assertFalse( + "$mode/bunsetsu=$bunsetsu emoticon", + niko.candidates.any { it.string == "(^o^)" }, + ) + + val ichi = session.query( + request("いち", mode, bunsetsu).copy(predictionConfig = hiddenConfig), + ) + assertFalse( + "$mode/bunsetsu=$bunsetsu value-based symbol", + ichi.candidates.any { it.string == "①" }, + ) + } + } + } + + @Test + fun symbolEmojiCandidateDisplaySettingsAreIndependent() = runBlocking { + val session = KanaKanjiConversionSession(engine, ConversionBackend.LEGACY) + + val emojiHidden = session.query( + request("ねこ", CandidateQueryMode.PREDICTION, bunsetsu = false).copy( + predictionConfig = PredictionConfig(showEmojiCandidates = false), + ), + ) + assertFalse(emojiHidden.candidates.any { it.string.contains("🐈") }) + assertTrue( + session.query(request("にこ", CandidateQueryMode.PREDICTION, false)) + .candidates.any { it.string == "(^o^)" }, + ) + assertTrue( + session.query(request("さんかく", CandidateQueryMode.PREDICTION, false)) + .candidates.any { it.type.toInt() == 13 }, + ) + + val emoticonHidden = session.query( + request("にこ", CandidateQueryMode.PREDICTION, bunsetsu = false).copy( + predictionConfig = PredictionConfig(showEmoticonCandidates = false), + ), + ) + assertFalse(emoticonHidden.candidates.any { it.string == "(^o^)" }) + assertTrue( + session.query(request("ねこ", CandidateQueryMode.PREDICTION, false)) + .candidates.any { it.string.contains("🐈") }, + ) + + val symbolHidden = session.query( + request("さんかく", CandidateQueryMode.PREDICTION, bunsetsu = false).copy( + predictionConfig = PredictionConfig(showSymbolCandidates = false), + ), + ) + assertTrue(symbolHidden.candidates.none { it.type.toInt() == 13 }) + assertFalse( + session.query( + request("いち", CandidateQueryMode.CONVERSION, false).copy( + predictionConfig = PredictionConfig(showSymbolCandidates = false), + ), + ) + .candidates.any { it.string == "①" }, + ) + assertTrue( + session.query(request("ねこ", CandidateQueryMode.PREDICTION, false)) + .candidates.any { it.string.contains("🐈") }, + ) + } + @Test fun cancelledInPlaceAppendIsDiscardedBeforeNextRequest() = runBlocking { val localEngine = TestEngineFactory.create() diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshCoordinatorTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshCoordinatorTest.kt index cae735d9..da1589c5 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshCoordinatorTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/ime_service/candidate/CandidateRefreshCoordinatorTest.kt @@ -14,6 +14,34 @@ import org.junit.Test class CandidateRefreshCoordinatorTest { + @Test + fun emptyUpdatingDoesNotConsumeTheNextActiveCandidateTransition() { + val afterDictionaryRefresh = CandidateRefreshTransitionPolicy.nextUiPreviousFlag( + currentFlag = CandidateShowFlag.Updating, + input = "", + ) + + assertEquals(CandidateShowFlag.Idle, afterDictionaryRefresh) + assertTrue( + CandidateRefreshTransitionPolicy.shouldEnterActiveCandidatePhase( + previousFlag = afterDictionaryRefresh, + currentFlag = CandidateShowFlag.Updating, + input = "かな", + ) + ) + } + + @Test + fun nonEmptyUpdatingRemainsTheActiveCandidatePhase() { + assertEquals( + CandidateShowFlag.Updating, + CandidateRefreshTransitionPolicy.nextUiPreviousFlag( + currentFlag = CandidateShowFlag.Updating, + input = "かな", + ) + ) + } + @Test fun repeatedUpdatingRequestsRemainDistinctAndCarryTheirInputSnapshot() { val coordinator = CandidateRefreshCoordinator() diff --git a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/PredictionPreferenceTest.kt b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/PredictionPreferenceTest.kt index 62a156c3..a75de263 100644 --- a/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/PredictionPreferenceTest.kt +++ b/app/src/test/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/PredictionPreferenceTest.kt @@ -55,6 +55,9 @@ class PredictionPreferenceTest { AppPreference.proverb_prediction_enable_preference = false AppPreference.external_mozc_prediction_enable_preference = false AppPreference.symbol_emoji_prediction_enable_preference = false + AppPreference.symbol_candidate_enable_preference = false + AppPreference.emoji_candidate_enable_preference = false + AppPreference.emoticon_candidate_enable_preference = false AppPreference.user_dictionary_prediction_candidate_limit_preference = 2 AppPreference.learn_dictionary_prediction_candidate_limit_preference = 7 @@ -73,6 +76,9 @@ class PredictionPreferenceTest { assertFalse(prediction.proverbEnabled) assertFalse(prediction.externalMozcEnabled) assertFalse(prediction.symbolEmojiEnabled) + assertFalse(prediction.showSymbolCandidates) + assertFalse(prediction.showEmojiCandidates) + assertFalse(prediction.showEmoticonCandidates) assertEquals(2, snapshot.userDictionaryPredictionCandidateLimit) assertEquals(7, snapshot.learnDictionaryPredictionCandidateLimit) } @@ -109,6 +115,21 @@ class PredictionPreferenceTest { } } + @Test + fun legacyCombinedSymbolEmojiCandidatePreferenceIsSplitAndPersisted() { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + preferences.edit() + .putBoolean("symbol_emoji_candidate_enable_preference", false) + .commit() + + AppPreference.migrateSymbolEmojiCandidatePreferenceIfNeeded() + + assertFalse(AppPreference.symbol_candidate_enable_preference) + assertFalse(AppPreference.emoji_candidate_enable_preference) + assertFalse(AppPreference.emoticon_candidate_enable_preference) + assertFalse(preferences.contains("symbol_emoji_candidate_enable_preference")) + } + @Test fun settingsAreIndexedUnderTheirOwningCategories() { val newSettings = SettingSearchIndex.searchable(context, SettingSearchScope.NEW_HOME) @@ -145,6 +166,15 @@ class PredictionPreferenceTest { assertEquals(1, lookahead?.min) assertEquals(6, lookahead?.max) assertTrue(lookahead?.showSeekBarValue == true) + + listOf( + "symbol_candidate_enable_preference", + "emoji_candidate_enable_preference", + "emoticon_candidate_enable_preference", + ).forEach { key -> + val preference = screen.findPreference(key) + assertTrue(preference?.isChecked == true) + } } private companion object { @@ -161,6 +191,9 @@ class PredictionPreferenceTest { "proverb_prediction_enable_preference", "external_mozc_prediction_enable_preference", "symbol_emoji_prediction_enable_preference", + "symbol_candidate_enable_preference", + "emoji_candidate_enable_preference", + "emoticon_candidate_enable_preference", ) val DICTIONARY_KEYS = listOf( "user_dictionary_prediction_candidate_limit_preference",