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
4 changes: 4 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ class KeyHandler(
handleCurrencyKey(language)
true
}
KeyboardBase.KEYCODE_EMOJI -> {
ime.openEmojiKeyboard()
true
}
else -> {
handleDefaultKey(code)
true
Expand Down
179 changes: 179 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import be.scri.R
import be.scri.R.color.white
import be.scri.databinding.InputMethodViewBinding
import be.scri.helpers.AutoGridLayoutManager
import be.scri.helpers.EMOJI_SPEC_FILE_PATH
import be.scri.helpers.EmojiAdapter
import be.scri.helpers.EmojiData
import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.pluralPlaceholder
Expand All @@ -29,6 +35,8 @@ import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
import be.scri.helpers.PreferencesHelper
import be.scri.helpers.PreferencesHelper.getIsDarkModeOrNot
import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG
import be.scri.helpers.getCategoryIconRes
import be.scri.helpers.parseRawEmojiSpecsFile
import be.scri.models.ScribeState
import be.scri.services.GeneralKeyboardIME
import be.scri.views.KeyboardView
Expand Down Expand Up @@ -754,6 +762,177 @@ class KeyboardUIManager(
binding.separator6.visibility = View.GONE
}

/**
* Displays the emoji palette and hides the keyboard view.
* Loads emojis from the emoji spec file on a background thread and populates the grid.
*/
fun showEmojiPalette() {
binding.keyboardView.post {
val keyboardHeight = binding.keyboardView.measuredHeight
val toolbarHeight =
binding.commandOptionsBar.measuredHeight.takeIf { it > 0 }
?: context.resources.getDimensionPixelSize(R.dimen.toolbar_height)

binding.emojiPaletteHolder.updateLayoutParams {
height = keyboardHeight + toolbarHeight
}
binding.emojiPaletteHolder.requestLayout()
}

binding.emojiPaletteHolder.visibility = View.VISIBLE

binding.keyboardView.visibility = View.GONE
binding.commandOptionsBar.visibility = View.GONE

binding.emojiPaletteClose.setOnClickListener {
hideEmojiPalette()
}

binding.emojiPaletteModeChange.setOnClickListener {
hideEmojiPalette()
}
binding.emojiPaletteModeChange.text = "ABC"
binding.emojiPaletteModeChange.setTextColor(
ContextCompat.getColor(context, R.color.emoji_palette_icons),
)

binding.emojiPaletteBackspace.setOnClickListener {
listener.onKeyboardActionListener().onKey(KeyboardBase.KEYCODE_DELETE)
}

Thread {
val fullEmojiList = parseRawEmojiSpecsFile(context, EMOJI_SPEC_FILE_PATH)
val systemFontPaint =
android.graphics.Paint().apply {
typeface = android.graphics.Typeface.DEFAULT
}
val emojis =
fullEmojiList.filter { emoji ->
systemFontPaint.hasGlyph(emoji.emoji)
}

android.os.Handler(android.os.Looper.getMainLooper()).post {
setupEmojiAdapter(emojis)
}
}.start()
}

/**
* Sets up the emoji RecyclerView adapter and category strip.
*
* @param emojis The filtered list of emojis the device can render.
*/
private fun setupEmojiAdapter(emojis: List<EmojiData>) {
val emojiCategories = prepareEmojiCategories(emojis)
val emojiItems = prepareEmojiItems(emojiCategories)

val emojiItemSize = context.resources.getDimensionPixelSize(R.dimen.emoji_item_size)
val emojiLayoutManager = AutoGridLayoutManager(context, emojiItemSize)

emojiLayoutManager.spanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int =
if (emojiItems[position] is EmojiAdapter.Item.Category) {
emojiLayoutManager.spanCount
} else {
1
}
}

binding.emojisList.layoutManager = emojiLayoutManager
binding.emojisList.adapter =
EmojiAdapter(context, emojiItems) { emojiData ->
listener.onEmojiSelected(emojiData.emoji)
}

setupEmojiCategoryStrip(emojiCategories, emojiItems, emojiLayoutManager)
}

/**
* Groups emojis by category.
*
* @param emojis The full list of emojis.
* @return A map of category name to list of emojis in corresponding category.
*/
private fun prepareEmojiCategories(emojis: List<EmojiData>): Map<String, List<EmojiData>> = emojis.groupBy { it.category }

/**
* Builds a list of category headers and emoji items for the RecyclerView.
*
* @param categories The map of categories to their emojis.
* @return A flat list of [EmojiAdapter.Item] objects.
*/
private fun prepareEmojiItems(categories: Map<String, List<EmojiData>>): List<EmojiAdapter.Item> {
val emojiItems = mutableListOf<EmojiAdapter.Item>()
categories.entries.forEach { (category, emojis) ->
emojiItems.add(EmojiAdapter.Item.Category(category))
emojis.forEach { emojiItems.add(EmojiAdapter.Item.Emoji(it)) }
}
return emojiItems
}

/**
* Populates the emoji category strip at the bottom of the palette.
* Tapping a category icon scrolls the emoji list to that category.
*
* @param categories The map of category names to their emojis.
* @param emojiItems The full flat list used to find category positions.
* @param layoutManager The AutoGridLayoutManager used to scroll to positions.
*/
private fun setupEmojiCategoryStrip(
categories: Map<String, List<EmojiData>>,
emojiItems: List<EmojiAdapter.Item>,
layoutManager: AutoGridLayoutManager,
) {
binding.emojiCategoriesStrip.removeAllViews()

var activeButton: android.widget.ImageButton? = null

categories.keys.forEachIndexed { index, category ->
val button =
android.widget.ImageButton(context).apply {
setImageResource(getCategoryIconRes(category))
background = null
imageAlpha = if (index == 0) 255 else 128 // 50% opacity for inactive
layoutParams =
android.widget.LinearLayout.LayoutParams(
0,
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
1f,
)
setOnClickListener {
activeButton?.imageAlpha = 128 // dim previous.
imageAlpha = 255 // full opacity for active.
activeButton = this

val position =
emojiItems.indexOfFirst {
it is EmojiAdapter.Item.Category && it.value == category
}
if (position != -1) {
(layoutManager as androidx.recyclerview.widget.LinearLayoutManager)
.scrollToPositionWithOffset(position, 0)
}
}
}

if (index == 0) activeButton = button
binding.emojiCategoriesStrip.addView(button)
}
}

/**
* Hides the emoji palette and restores the normal keyboard view and command options bar.
* Called when the user taps the close button, the ABC button, or finishes emoji selection.
*/
fun hideEmojiPalette() {
binding.emojiPaletteHolder.visibility = View.GONE
binding.keyboardView.visibility = View.VISIBLE
binding.commandOptionsBar.visibility = View.VISIBLE
binding.emojisList.scrollToPosition(0)
binding.emojiCategoriesStrip.removeAllViews()
}

/**
* Updates the text of the suggestion buttons, primarily for displaying emoji suggestions.
*
Expand Down
37 changes: 35 additions & 2 deletions app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ abstract class GeneralKeyboardIME(
val inputConnection = currentInputConnection
if (inputConnection != null) {
when (code) {
KeyboardBase.KEYCODE_EMOJI -> openEmojiKeyboard()
KeyboardBase.KEYCODE_DELETE -> handleDelete()
KeyboardBase.KEYCODE_SHIFT -> {
if (keyboardMode == keyboardLetters) {
Expand Down Expand Up @@ -527,6 +528,10 @@ abstract class GeneralKeyboardIME(

// MARK: Helper Methods

fun openEmojiKeyboard() {
uiManager.showEmojiPalette()
}

protected fun isPeriodAndCommaEnabled(): Boolean {
val isPreferenceEnabled = PreferencesHelper.getEnablePeriodAndCommaABC(this, language)
val isInSearchBar = isSearchBar()
Expand Down Expand Up @@ -1006,15 +1011,43 @@ abstract class GeneralKeyboardIME(
// MARK: Deletion Logic

/**
* Handles the logic for the Delete/Backspace key. It deletes characters from either
* Handles the logic for the Delete key. It deletes characters from either
* the main input field or the command bar, depending on the context.
* Delegated to BackspaceHandler.
*
* @param isCommandBar true` if the deletion should happen in the command bar.
* @param isLongPress true` if this is a long press/repeat action, false for single tap.
*/
fun handleDelete(isLongPress: Boolean = false) {
val effectiveIsCommandBar = currentState != ScribeState.IDLE && currentState != ScribeState.SELECT_COMMAND
val inputConnection = currentInputConnection ?: return
val effectiveIsCommandBar =
currentState != ScribeState.IDLE &&
currentState != ScribeState.SELECT_COMMAND

if (!effectiveIsCommandBar) {
val selectedText = inputConnection.getSelectedText(0)
if (selectedText.isNullOrEmpty()) {
// Use BreakIterator to delete full emoji characters.
val prevText = inputConnection.getTextBeforeCursor(8, 0)
if (!prevText.isNullOrEmpty()) {
val breakIterator =
android.icu.text.BreakIterator
.getCharacterInstance()
breakIterator.setText(prevText.toString())
val end = breakIterator.last()
val start = breakIterator.previous()
val count =
if (start == android.icu.text.BreakIterator.DONE) {
1
} else {
(end - start).coerceAtLeast(1)
}
inputConnection.deleteSurroundingText(count, 0)
return
}
}
}

backspaceHandler.handleBackspace(effectiveIsCommandBar, isLongPress)
}

Expand Down
Loading
Loading