Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/src/main/java/be/scri/helpers/EmojiUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import java.util.Locale
object EmojiUtils {
private const val DATA_SIZE_2 = 2

val COMMON_EMOJIS = listOf("😀", "❤️", "👍", "😂", "🎉", "✨", "🔥", "👋", "😊")

/**
* Checks if the end of a string is likely an emoji.
* This is a heuristic check based on common emoji Unicode ranges.
Expand Down Expand Up @@ -48,11 +50,22 @@ object EmojiUtils {
ic: InputConnection,
emojiKeywords: HashMap<String, MutableList<String>>?,
emojiMaxKeywordLength: Int,
emojiColonModeOn: Boolean,
) {
val maxLookBack = emojiMaxKeywordLength.coerceAtLeast(1)
ic.beginBatchEdit()
try {
val prevText = ic.getTextBeforeCursor(maxLookBack, 0)?.toString() ?: ""
// If emoji colon suggestion is on, look back to the : that triggered emoji suggestions
// Delete that text, and add the emoji
if (emojiColonModeOn) {
val colonIndex = prevText.lastIndexOf(':')
if (colonIndex != -1) {
ic.deleteSurroundingText(prevText.length - colonIndex, 0)
}
ic.commitText(emoji, 1)
return
}
val lastSpace = prevText.lastIndexOf(' ')
when {
prevText.isEmpty() ||
Expand Down
59 changes: 52 additions & 7 deletions app/src/main/java/be/scri/helpers/KeyHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class KeyHandler(
resetShiftIfNeeded(code)

val previousWasLastKeySpace = wasLastKeySpace
if (code != KeyboardBase.KEYCODE_SPACE) {
if (code != KeyboardBase.KEYCODE_SPACE && !ime.emojiColonModeOn) { // None to clear in emoji colon mode, causes unnecessary flash when called
suggestionHandler.clearLinguisticSuggestions()
}

Expand Down Expand Up @@ -134,13 +134,19 @@ class KeyHandler(

/**
* Handles the space key press and returns whether to reset wasLastKeySpace at the end.
* Switches emoji colon mode off, if on.
*
* @param previousWasLastKeySpace The previous state of wasLastKeySpace.
*
* @return False to preserve wasLastKeySpace state, true to reset it.
*/
private fun handleSpaceKeyPress(previousWasLastKeySpace: Boolean): Boolean {
wasLastKeySpace = spaceKeyProcessor.processKeycodeSpace(previousWasLastKeySpace)
// If we're suggesting emojis in colon mode, stop. Space should break emoji suggestions, and return to normal
if (ime.emojiColonModeOn) {
ime.emojiColonModeOn = false
ime.clearAutocomplete()
}
return false
}

Expand Down Expand Up @@ -198,15 +204,27 @@ class KeyHandler(
/**
* Handles the delete/backspace key press. It delegates the deletion logic to the IME
* and then triggers a re-evaluation of word suggestions based on the new text.
* Turns off emoji colon mode if colon is deleted that triggered it.
*/

private fun handleDeleteKey() {
val charToDelete = ime.currentInputConnection?.getTextBeforeCursor(1, 0)
ime.handleDelete(ime.isDeleteRepeating()) // pass the actual repeating status

if (ime.currentState == ScribeState.IDLE) {
val deletedChar = charToDelete?.takeIf { it.isNotEmpty() }?.last()
if (deletedChar == ':' && ime.emojiColonModeOn) {
ime.emojiColonModeOn = false
ime.clearAutocomplete()
}

val currentWord = ime.getLastWordBeforeCursor()
autocompletionHandler.processAutocomplete(currentWord)
suggestionHandler.processEmojiSuggestions(currentWord)
if (ime.emojiColonModeOn) {
suggestionHandler.processEmojiSuggestions(currentWord)
} else {
autocompletionHandler.processAutocomplete(currentWord)
suggestionHandler.processEmojiSuggestions(currentWord)
}
}
}

Expand Down Expand Up @@ -243,10 +261,15 @@ class KeyHandler(
/**
* Handles the mode change key press (e.g., switching to the symbol keyboard).
* It delegates the logic to the IME and clears any active suggestions.
* In emoji colon mode, restore emoji suggestions from before mode change.
*/
private fun handleModeChangeKey() {
ime.handleModeChange(ime.keyboardMode, ime.keyboardView, ime)
suggestionHandler.clearAllSuggestionsAndHideButtonUI()
if (ime.emojiColonModeOn) {
suggestionHandler.processEmojiSuggestions(ime.getLastWordBeforeCursor())
} else {
suggestionHandler.clearAllSuggestionsAndHideButtonUI()
}
}

/**
Expand Down Expand Up @@ -341,7 +364,7 @@ class KeyHandler(
/**
* Handles default key presses (regular characters, numbers, symbols).
* Commits the character to the input connection and processes suggestions.
*
* Toggles colon emoji mode on if ':' typed.
* @param code The key code representing the character to input.
*/

Expand All @@ -358,9 +381,31 @@ class KeyHandler(
ime.handleElseCondition(code, ime.keyboardMode, isCommandBarActive)

if (ime.currentState == ScribeState.IDLE) {
if (code == ':'.code && ime.getLastWordBeforeCursor() == ":") { // " :" triggers emoji colon mode
ime.emojiColonModeOn = true

val commonEmojis = EmojiUtils.COMMON_EMOJIS.toMutableList()
ime.autoSuggestEmojis = commonEmojis
ime.updateButtonVisibility(true)
ime.updateEmojiSuggestion(true, commonEmojis) // Show common emojis otherwise there's a small delay where words pop up
}

val currentWord = ime.getLastWordBeforeCursor()
autocompletionHandler.processAutocomplete(currentWord)
suggestionHandler.processEmojiSuggestions(currentWord)
if (ime.emojiColonModeOn) {
currentWord?.startsWith(":")?.let {
if (!it) { // Turn emoji colon mode off if there's no colon anymore (like if an emoji was just selected)
ime.emojiColonModeOn = false
ime.clearAutocomplete()
autocompletionHandler.processAutocomplete(currentWord)
suggestionHandler.processEmojiSuggestions(currentWord)
} else {
suggestionHandler.processEmojiSuggestions(currentWord)
}
}
} else { // Normal suggestions
autocompletionHandler.processAutocomplete(currentWord)
suggestionHandler.processEmojiSuggestions(currentWord)
}
} else if (isCommandBarActive) {
suggestionHandler.clearAllSuggestionsAndHideButtonUI()
autocompletionHandler.clearAutocomplete()
Expand Down
27 changes: 19 additions & 8 deletions app/src/main/java/be/scri/helpers/SuggestionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ class SuggestionHandler(
*/
fun processEmojiSuggestions(currentWord: String?) {
emojiSuggestionRunnable?.let { handler.removeCallbacks(it) }

emojiSuggestionRunnable =
Runnable {
if (ime.currentState != ScribeState.IDLE) {
Expand All @@ -131,19 +130,30 @@ class SuggestionHandler(
return@Runnable
}

val emojis =
if (ime.emojiAutoSuggestionEnabled) {
ime.findEmojisForLastWord(ime.emojiKeywords, currentWord)
} else {
null
}
var emojis: MutableList<String>?
if (ime.emojiColonModeOn) {
val currentWordCleaned = currentWord.removePrefix(":") // Drop colon that triggered emojiColonMode
emojis =
if (
currentWordCleaned.isEmpty()
) { // For no word typed yet, show common emojis
EmojiUtils.COMMON_EMOJIS.toMutableList()
} else {
ime.findEmojisForPrefix(ime.emojiKeywords, currentWordCleaned)
}
} else {
emojis = null
}

val hasEmojiSuggestion = !emojis.isNullOrEmpty()

if (hasEmojiSuggestion) {
ime.autoSuggestEmojis = emojis
ime.updateEmojiSuggestion(true, emojis)
ime.updateButtonVisibility(true)
ime.updateEmojiSuggestion(true, emojis)
} else if (ime.emojiColonModeOn) { // Show blank buttons when there are no matches
ime.autoSuggestEmojis = mutableListOf()
ime.updateEmojiSuggestion(true, mutableListOf())
} else {
ime.updateButtonVisibility(false)
}
Expand Down Expand Up @@ -172,6 +182,7 @@ class SuggestionHandler(
fun clearAllSuggestionsAndHideButtonUI() {
emojiSuggestionRunnable?.let { handler.removeCallbacks(it) }
linguisticSuggestionRunnable?.let { handler.removeCallbacks(it) }
wordSuggestionRunnable?.let { handler.removeCallbacks(it) }

ime.disableAutoSuggest()

Expand Down
110 changes: 94 additions & 16 deletions app/src/main/java/be/scri/helpers/ui/KeyboardUIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,33 @@ class KeyboardUIManager(
var genderSuggestionLeft: Button? = binding.translateBtnLeft
var genderSuggestionRight: Button? = binding.translateBtnRight

// 6-slot phone colon emoji row buttons
private val emojiColonPhoneButtons: List<Button> by lazy {
listOf(
binding.emojiColonPhone1,
binding.emojiColonPhone2,
binding.emojiColonPhone3,
binding.emojiColonPhone4,
binding.emojiColonPhone5,
binding.emojiColonPhone6,
)
}

// 9-slot tablet colon emoji row buttons
private val emojiColonTabletButtons: List<Button> by lazy {
listOf(
binding.emojiColonTablet1,
binding.emojiColonTablet2,
binding.emojiColonTablet3,
binding.emojiColonTablet4,
binding.emojiColonTablet5,
binding.emojiColonTablet6,
binding.emojiColonTablet7,
binding.emojiColonTablet8,
binding.emojiColonTablet9,
)
}

// State variables specific to UI rendering.
var currentCommandBarHint: String = ""
var commandBarHintColor: Int = Color.GRAY
Expand Down Expand Up @@ -752,30 +779,73 @@ class KeyboardUIManager(
currentState: ScribeState,
isAutoSuggestEnabled: Boolean,
autoSuggestEmojis: MutableList<String>?,
emojiColonModeOn: Boolean = false,
) {
if (currentState != ScribeState.IDLE) return

val isTablet =
(
context.resources.configuration.screenLayout
and Configuration.SCREENLAYOUT_SIZE_MASK
) >= Configuration.SCREENLAYOUT_SIZE_LARGE

val tabletButtons = listOf(binding.emojiBtnTablet1, binding.emojiBtnTablet2, binding.emojiBtnTablet3)
val phoneButtons = listOf(binding.emojiBtnPhone1, binding.emojiBtnPhone2)
val legacyPhoneButtons = listOf(binding.emojiBtnPhone1, binding.emojiBtnPhone2)

if (isAutoSuggestEnabled && autoSuggestEmojis != null) {
val emojiListener = { emoji: String ->
View.OnClickListener { listener.onEmojiSelected(emoji) }
}

tabletButtons.forEachIndexed { index, button ->
val emoji = autoSuggestEmojis.getOrNull(index) ?: ""
button.text = emoji
button.setOnClickListener(if (emoji.isNotEmpty()) emojiListener(emoji) else null)
}

phoneButtons.forEachIndexed { index, button ->
val emoji = autoSuggestEmojis.getOrNull(index) ?: ""
button.text = emoji
button.setOnClickListener(if (emoji.isNotEmpty()) emojiListener(emoji) else null)
val emojiListener = { emoji: String -> View.OnClickListener { listener.onEmojiSelected(emoji) } }

if (emojiColonModeOn && !isTablet) {
// Phone colon mode: show the dedicated 6-slot row, hide word buttons and separators.
binding.translateBtn.visibility = View.GONE
binding.conjugateBtn.visibility = View.GONE
binding.pluralBtn.visibility = View.GONE
binding.separator2.visibility = View.GONE
binding.separator3.visibility = View.GONE
legacyPhoneButtons.forEach { it.visibility = View.GONE }
binding.emojiColonRowPhone.visibility = View.VISIBLE

emojiColonPhoneButtons.forEachIndexed { index, button ->
val emoji = autoSuggestEmojis.getOrNull(index) ?: ""
button.text = emoji
button.setOnClickListener(if (emoji.isNotEmpty()) emojiListener(emoji) else null)
}
} else if (emojiColonModeOn && isTablet) {
// Tablet colon mode: show the dedicated 9-slot row, hide word buttons and separators.
binding.translateBtn.visibility = View.GONE
binding.conjugateBtn.visibility = View.GONE
binding.pluralBtn.visibility = View.GONE
binding.separator2.visibility = View.GONE
binding.separator3.visibility = View.GONE
legacyPhoneButtons.forEach { it.visibility = View.GONE }
tabletButtons.forEach { it.visibility = View.GONE }
binding.emojiColonRowPhone.visibility = View.GONE
binding.emojiColonRowTablet.visibility = View.VISIBLE

emojiColonTabletButtons.forEachIndexed { index, button ->
val emoji = autoSuggestEmojis.getOrNull(index) ?: ""
button.text = emoji
button.setOnClickListener(if (emoji.isNotEmpty()) emojiListener(emoji) else null)
}
} else {
// Non-colon mode: ensure both colon rows are hidden, use existing emoji buttons.
binding.emojiColonRowPhone.visibility = View.GONE
binding.emojiColonRowTablet.visibility = View.GONE
tabletButtons.forEachIndexed { index, button ->
val emoji = autoSuggestEmojis.getOrNull(index) ?: ""
button.text = emoji
button.setOnClickListener(if (emoji.isNotEmpty()) emojiListener(emoji) else null)
}
legacyPhoneButtons.forEachIndexed { index, button ->
val emoji = autoSuggestEmojis.getOrNull(index) ?: ""
button.text = emoji
button.setOnClickListener(if (emoji.isNotEmpty()) emojiListener(emoji) else null)
}
}
} else {
(tabletButtons + phoneButtons).forEach { button ->
binding.emojiColonRowPhone.visibility = View.GONE
binding.emojiColonRowTablet.visibility = View.GONE
(tabletButtons + legacyPhoneButtons).forEach { button ->
button.text = ""
button.setOnClickListener(null)
}
Expand All @@ -786,6 +856,14 @@ class KeyboardUIManager(
* Disables all auto-suggestions and resets the suggestion buttons to their default, inactive state.
*/
fun disableAutoSuggest(language: String) {
// Ensure both colon emoji rows are hidden and word buttons are fully restored.
binding.emojiColonRowPhone.visibility = View.GONE
binding.emojiColonRowTablet.visibility = View.GONE
binding.separator2.visibility = View.VISIBLE
binding.separator3.visibility = View.VISIBLE
binding.conjugateBtn.visibility = View.VISIBLE
binding.pluralBtn.visibility = View.VISIBLE

binding.translateBtnRight.visibility = View.INVISIBLE
binding.translateBtnLeft.visibility = View.INVISIBLE
binding.translateBtn.visibility = View.VISIBLE
Expand Down
Loading
Loading