Skip to content

Commit

Permalink
Implement Emoji Suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
bingzheung committed Sep 4, 2024
1 parent 8bee2f7 commit 11273fc
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ class JyutpingInputMethodService: LifecycleInputMethodService(),
mutableStateOf(isOn)
}
fun updateEmojiSuggestionsState(isOn: Boolean) {
isEmojiSuggestionsOn.value = isOn
val value2save: Int = if (isOn) 1 else 2
val editor = sharedPreferences.edit()
editor.putInt(UserSettingsKey.Emoji, value2save)
Expand Down Expand Up @@ -387,7 +388,9 @@ class JyutpingInputMethodService: LifecycleInputMethodService(),
val processingText: String = value.toneConverted()
val segmentation = Segmentor.segment(processingText, db)
val userLexiconSuggestions: List<Candidate> = if (isInputMemoryOn.value) userDB.suggest(text = processingText, segmentation = segmentation) else emptyList()
val suggestions = Engine.suggest(text = processingText, segmentation = segmentation, db = db)
val needsSymbols: Boolean = isEmojiSuggestionsOn.value && selectedCandidates.isEmpty()
val asap: Boolean = userLexiconSuggestions.isNotEmpty()
val suggestions = Engine.suggest(text = processingText, segmentation = segmentation, db = db, needsSymbols = needsSymbols, asap = asap)
val mark: String = run {
val userLexiconMark = userLexiconSuggestions.firstOrNull()?.mark
if (userLexiconMark != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ fun String.formattedForMark(): String {
val blocks = this.map { if (it.isSeparatorOrTone()) "$it " else it.toString() }
return blocks.joinToString(separator = PresetString.EMPTY).trimEnd()
}

/**
* Create Emoji text from the given code point String.
* @param codepoint Unicode code point. Example: 1F469.200D.1F373
* @return Emoji / Symbol character(s)
*/
fun generateSymbol(codepoint: String) = codepoint
.split(".")
.map { it.toInt(radix = 16) }
.flatMap { Character.toChars(it).toList() }
.joinToString(PresetString.EMPTY)
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import org.jyutping.jyutping.presets.PresetColor

private fun Candidate.width(): Dp = when (this.type) {
CandidateType.Cantonese -> (this.text.length * 20 + 32).dp
else -> if (this.text.length < 2) 52.dp else (this.text.length * 14).dp
else -> if (this.text.length < 2) 60.dp else (this.text.length * 16).dp
}

private class CandidateRow(val identifier: Int, val candidates: List<Candidate>, val width: Dp = candidates.map { it.width() }.fold(0.dp) { acc, w -> acc + w })
Expand Down Expand Up @@ -105,7 +105,7 @@ fun CandidateBoard(height: Dp) {
.defaultMinSize(minHeight = collapseHeight)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(0.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.Bottom
) {
row.candidates.map {
CandidateView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fun CandidateScrollBar() {
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
context.select(it)
}
.padding(horizontal = 6.dp)
.padding(horizontal = if (it.type.isCantonese()) 6.dp else 10.dp)
.padding(bottom = 12.dp)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ fun CandidateView(candidate: Candidate, isDarkMode: Boolean, modifier: Modifier)
verticalArrangement = Arrangement.spacedBy((-2).dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = candidate.romanization,
color = if (isDarkMode) Color.White else Color.Black,
fontSize = 12.sp
)
if (candidate.type.isCantonese()) {
Text(
text = candidate.romanization,
color = if (isDarkMode) Color.White else Color.Black,
fontSize = 12.sp
)
}
Text(
text = candidate.text,
color = if (isDarkMode) Color.White else Color.Black,
Expand Down
60 changes: 48 additions & 12 deletions app/src/main/java/org/jyutping/jyutping/keyboard/Engine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@ import org.jyutping.jyutping.presets.PresetString
import org.jyutping.jyutping.utilities.DatabaseHelper

object Engine {
fun suggest(text: String, segmentation: Segmentation, db: DatabaseHelper): List<Candidate> {
fun suggest(text: String, segmentation: Segmentation, db: DatabaseHelper, needsSymbols: Boolean, asap: Boolean): List<Candidate> {
return when (text.length) {
0 -> emptyList()
1 -> when (text) {
"a" -> db.match(text, input = text) + db.match(text = "aa", input = text) + db.shortcut(text)
"o", "m", "e" -> db.match(text = text, input = text) + db.shortcut(text)
else -> db.shortcut(text)
}
else -> dispatch(text, segmentation, db)
else -> {
if (asap) {
if (segmentation.maxSchemeLength() > 0) {
val candidates = query(text = text, segmentation = segmentation, db = db, needsSymbols = needsSymbols)
if (candidates.isEmpty()) processVerbatim(text = text, db = db) else candidates
} else {
processVerbatim(text = text, db = db)
}
} else {
dispatch(text = text, segmentation = segmentation, db = db, needsSymbols = needsSymbols)
}
}
}
}
private fun dispatch(text: String, segmentation: Segmentation, db: DatabaseHelper): List<Candidate> {
private fun dispatch(text: String, segmentation: Segmentation, db: DatabaseHelper, needsSymbols: Boolean): List<Candidate> {
val hasSeparators: Boolean = text.contains(PresetCharacter.SEPARATOR)
val hasTones: Boolean = text.contains(Regex("[1-6]"))
return when {
Expand All @@ -31,7 +42,7 @@ object Engine {
if (segmentation.maxSchemeLength() < 1) {
processVerbatim(text, db)
} else {
process(text, segmentation, db)
process(text = text, segmentation = segmentation, db = db, needsSymbols = needsSymbols)
}
}
}
Expand All @@ -40,7 +51,7 @@ object Engine {
val textTones = text.filter { it.isDigit() }
val textToneCount = textTones.length
val rawText: String = text.filterNot { it.isDigit() }
val candidates= query(rawText, segmentation, db)
val candidates= query(text = rawText, segmentation = segmentation, db = db, needsSymbols = false)
val qualified: MutableList<Candidate> = mutableListOf()
for (item in candidates) {
val continuous = item.romanization.filterNot { it.isWhitespace() }
Expand Down Expand Up @@ -114,7 +125,7 @@ object Engine {
val isHeadingSeparator: Boolean = text.firstOrNull()?.isSeparatorChar() ?: false
val isTrailingSeparator: Boolean = text.lastOrNull()?.isSeparatorChar() ?: false
val rawText = text.filter { !(it.isSeparatorChar()) }
val candidates = query(rawText, segmentation, db)
val candidates = query(text = rawText, segmentation = segmentation, db = db, needsSymbols = false)
val qualified: MutableList<Candidate> = mutableListOf()
for (item in candidates) {
val syllables = item.romanization.filterNot { it.isDigit() }.split(' ')
Expand Down Expand Up @@ -209,9 +220,9 @@ object Engine {
}
return rounds.flatten().distinct()
}
private fun process(text: String, segmentation: Segmentation, db: DatabaseHelper, limit: Int? = null): List<Candidate> {
private fun process(text: String, segmentation: Segmentation, db: DatabaseHelper, needsSymbols: Boolean, limit: Int? = null): List<Candidate> {
val textLength = text.length
val primary = query(text, segmentation, db, limit)
val primary = query(text = text, segmentation = segmentation, db = db, needsSymbols = needsSymbols)
val firstInputLength = primary.firstOrNull()?.input?.length ?: 0
if (firstInputLength == 0) return processVerbatim(text, db, limit)
if (firstInputLength == textLength) return primary
Expand Down Expand Up @@ -239,7 +250,7 @@ object Engine {
val tailText = text.drop(headInputLength)
if (db.canProcess(tailText).not()) continue
val tailSegmentation = Segmentor.segment(tailText, db)
val tailCandidates = process(text = tailText, segmentation = tailSegmentation, db = db, limit = 8).take(100)
val tailCandidates = process(text = tailText, segmentation = tailSegmentation, db = db, needsSymbols = false, limit = 8).take(100)
if (tailCandidates.isEmpty()) continue
val headCandidates = primary.takeWhile { it.input == headText }.take(8)
val combines = headCandidates.map { head -> tailCandidates.map { head + it } }
Expand All @@ -248,13 +259,25 @@ object Engine {
val preferredConcatenated = preferred(text, concatenated.flatten().distinct()).take(1)
return preferredConcatenated + primary
}
private fun query(text: String, segmentation: Segmentation, db: DatabaseHelper, limit: Int? = null): List<Candidate> {
private fun query(text: String, segmentation: Segmentation, db: DatabaseHelper, needsSymbols: Boolean, limit: Int? = null): List<Candidate> {
val textLength = text.length
val searches = search(text, segmentation, db, limit)
val preferredSearched = searches.filter { it.input.length == textLength }
val preferredSearches = searches.filter { it.input.length == textLength }
val matched = db.match(text = text, input = text, limit = limit)
val shortcut = db.shortcut(text, limit)
return (matched + preferredSearched + shortcut + searches).distinct()
val fallback by lazy { (matched + preferredSearches + shortcut + searches).distinct() }
val shouldNotContinue: Boolean = (!needsSymbols) || (limit != null) || (matched.isEmpty() && preferredSearches.isEmpty())
if (shouldNotContinue) return fallback
val symbols: List<Candidate> = searchSymbols(text = text, segmentation = segmentation, db = db)
if (symbols.isEmpty()) return fallback
val regular: MutableList<Candidate> = (matched + preferredSearches).toMutableList()
for (symbol in symbols.reversed()) {
val index = regular.indexOfFirst { it.lexiconText == symbol.lexiconText }
if (index != -1) {
regular.add(index = index + 1, element = symbol)
}
}
return (regular + shortcut + searches).distinct()
}
private fun search(text: String, segmentation: Segmentation, db: DatabaseHelper, limit: Int? = null): List<Candidate> {
val textLength = text.length
Expand Down Expand Up @@ -312,4 +335,17 @@ object Engine {
return candidates.sortedWith(comparator)
*/
}
private fun searchSymbols(text: String, segmentation: Segmentation, db: DatabaseHelper): List<Candidate> {
val regular = db.symbolMatch(text = text, input = text)
val textLength = text.length
val schemes = segmentation.filter { it.length() == textLength }
if (schemes.isEmpty()) return regular
val matches: MutableList<List<Candidate>> = mutableListOf()
for (scheme in schemes) {
val pingText = scheme.joinToString(separator = PresetString.EMPTY) { it.origin }
val matched = db.symbolMatch(text = pingText, input = text)
matches.add(matched)
}
return (regular + matches.flatten()).distinct()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import org.jyutping.jyutping.extensions.charcode
import org.jyutping.jyutping.extensions.convertedS2T
import org.jyutping.jyutping.extensions.generateSymbol
import org.jyutping.jyutping.extensions.intercode
import org.jyutping.jyutping.extensions.isIdeographicChar
import org.jyutping.jyutping.extensions.shortcutCharcode
import org.jyutping.jyutping.keyboard.Candidate
import org.jyutping.jyutping.keyboard.CandidateType
import org.jyutping.jyutping.keyboard.SegmentToken
import org.jyutping.jyutping.keyboard.ShapeLexicon
import org.jyutping.jyutping.presets.PresetString
Expand Down Expand Up @@ -578,4 +580,23 @@ class DatabaseHelper(context: Context, databaseName: String) : SQLiteOpenHelper(
cursor.close()
return items
}
fun symbolMatch(text: String, input: String): List<Candidate> {
val candidates: MutableList<Candidate> = mutableListOf()
val code = text.hashCode()
val command = "SELECT category, codepoint, cantonese, romanization FROM symboltable WHERE ping = ${code};"
val cursor = this.readableDatabase.rawQuery(command, null)
while (cursor.moveToNext()) {
val categoryCode = cursor.getInt(0)
val codepoint = cursor.getString(1)
val cantonese = cursor.getString(2)
val romanization = cursor.getString(3)
val symbolText = generateSymbol(codepoint)
val isEmoji: Boolean = (categoryCode > 0) && (categoryCode < 9)
val type: CandidateType = if (isEmoji) CandidateType.Emoji else CandidateType.Symbol
val instance = Candidate(type = type, text = symbolText, lexiconText = cantonese, romanization = romanization, input = input)
candidates.add(instance)
}
cursor.close()
return candidates
}
}

0 comments on commit 11273fc

Please sign in to comment.