Skip to content
Closed
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
2 changes: 1 addition & 1 deletion app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ class KeyHandler(
if (!ime.returnIsSubsequentRequired()) {
ime.handleConjugateKeys(code, false)
ime.moveToIdleState()
ime.saveConjugateModeType(language, isSubsequentArea = false)
ime.saveConjugateModeType("none")
} else {
val word = ime.handleConjugateKeys(code, true)
ime.setupConjugateSubView(ime.returnSubsequentData(), word)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ object KeyboardLanguageMappingConstants {
"objects" to ENInterfaceVariables.OBJECTS_EMOJI_HEADER,
"symbols" to ENInterfaceVariables.SYMBOLS_EMOJI_HEADER,
"flags" to ENInterfaceVariables.FLAGS_EMOJI_HEADER,
"kaomoji_recent" to ENInterfaceVariables.KAOMOJI_RECENT_HEADER,
"kaomoji_joy" to ENInterfaceVariables.KAOMOJI_JOY_HEADER,
"kaomoji_love" to ENInterfaceVariables.KAOMOJI_LOVE_HEADER,
"kaomoji_cool" to ENInterfaceVariables.KAOMOJI_COOL_HEADER,
"kaomoji_sad" to ENInterfaceVariables.KAOMOJI_SAD_HEADER,
"kaomoji_angry" to ENInterfaceVariables.KAOMOJI_ANGRY_HEADER,
"kaomoji_surprised" to ENInterfaceVariables.KAOMOJI_SURPRISED_HEADER,
"kaomoji_shrug" to ENInterfaceVariables.KAOMOJI_SHRUG_HEADER,
"kaomoji_animals" to ENInterfaceVariables.KAOMOJI_ANIMALS_HEADER,
),
"ES" to
mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,14 @@ object ENInterfaceVariables {
const val OBJECTS_EMOJI_HEADER = "Objects"
const val SYMBOLS_EMOJI_HEADER = "Symbols"
const val FLAGS_EMOJI_HEADER = "Flags"

const val KAOMOJI_RECENT_HEADER = "Recent"
const val KAOMOJI_JOY_HEADER = "Joy"
const val KAOMOJI_LOVE_HEADER = "Love"
const val KAOMOJI_COOL_HEADER = "Cool"
const val KAOMOJI_SAD_HEADER = "Sad"
const val KAOMOJI_ANGRY_HEADER = "Angry"
const val KAOMOJI_SURPRISED_HEADER = "Surprised"
const val KAOMOJI_SHRUG_HEADER = "Shrug"
const val KAOMOJI_ANIMALS_HEADER = "Animals"
}
219 changes: 188 additions & 31 deletions app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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.KaomojiData
import be.scri.helpers.KeyboardBase
import be.scri.helpers.KeyboardLanguageMappingConstants.conjugatePlaceholder
import be.scri.helpers.KeyboardLanguageMappingConstants.emojiCategoryHeaders
Expand All @@ -38,11 +39,17 @@ 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.getRecentKaomojis
import be.scri.helpers.parseRawEmojiSpecsFile
import be.scri.helpers.parseRawKaomojiSpecsFile
import be.scri.helpers.saveRecentKaomoji
import be.scri.models.ScribeState
import be.scri.services.GeneralKeyboardIME
import be.scri.views.KeyboardView

private const val PALETTE_MODE_EMOJI = 0
private const val PALETTE_MODE_KAOMOJI = 1

/**
* Manages the UI elements and state transitions for the GeneralKeyboardIME.
* This class handles View interactions, visibility toggling, and layout updates.
Expand Down Expand Up @@ -854,6 +861,7 @@ class KeyboardUIManager(

Thread {
val fullEmojiList = parseRawEmojiSpecsFile(context, EMOJI_SPEC_FILE_PATH)
val kaomojisList = parseRawKaomojiSpecsFile(context)
val systemFontPaint =
android.graphics.Paint().apply {
typeface = android.graphics.Typeface.DEFAULT
Expand All @@ -864,15 +872,64 @@ class KeyboardUIManager(
}

android.os.Handler(android.os.Looper.getMainLooper()).post {
setupEmojiAdapter(emojis, language)
setupPaletteModeTabs(emojis, kaomojisList, language)
}
}.start()
}

private var activePaletteMode = PALETTE_MODE_EMOJI

/**
* Sets up the emoji RecyclerView adapter and category strip.
*
* @param emojis The filtered list of emojis the device can render.
* Handles toggle switching between standard Emojis and Kaomojis via top_bar_emoji_toggle and top_bar_kaomoji_toggle.
*/
private fun setupPaletteModeTabs(
emojis: List<EmojiData>,
kaomojis: List<KaomojiData>,
language: String,
) {
fun updateToggleState() {
val isDarkMode = getIsDarkModeOrNot(context)
if (activePaletteMode == PALETTE_MODE_EMOJI) {
binding.topBarEmojiToggle.isSelected = true
binding.topBarEmojiToggle.imageTintList = ColorStateList.valueOf(Color.WHITE)

binding.topBarKaomojiToggle.isSelected = false
binding.topBarKaomojiToggle.setTextColor(if (isDarkMode) Color.LTGRAY else Color.DKGRAY)
} else {
binding.topBarKaomojiToggle.isSelected = true
binding.topBarKaomojiToggle.setTextColor(Color.WHITE)

binding.topBarEmojiToggle.isSelected = false
binding.topBarEmojiToggle.imageTintList = ColorStateList.valueOf(if (isDarkMode) Color.LTGRAY else Color.DKGRAY)
}
}

binding.topBarEmojiToggle.setOnClickListener {
if (activePaletteMode != PALETTE_MODE_EMOJI) {
activePaletteMode = PALETTE_MODE_EMOJI
updateToggleState()
setupEmojiAdapter(emojis, language)
}
}

binding.topBarKaomojiToggle.setOnClickListener {
if (activePaletteMode != PALETTE_MODE_KAOMOJI) {
activePaletteMode = PALETTE_MODE_KAOMOJI
updateToggleState()
setupKaomojiAdapter(kaomojis, language)
}
}

updateToggleState()
if (activePaletteMode == PALETTE_MODE_EMOJI) {
setupEmojiAdapter(emojis, language)
} else {
setupKaomojiAdapter(kaomojis, language)
}
}

/**
* Sets up the emoji RecyclerView adapter and category strip for standard emojis.
*/
private fun setupEmojiAdapter(
emojis: List<EmojiData>,
Expand All @@ -889,7 +946,7 @@ class KeyboardUIManager(
emojiLayoutManager.spanSizeLookup =
object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int =
if (emojiItems[position] is EmojiAdapter.Item.Category) {
if (emojiItems.getOrNull(position) is EmojiAdapter.Item.Category) {
emojiLayoutManager.spanCount
} else {
1
Expand All @@ -898,46 +955,91 @@ class KeyboardUIManager(

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

setupEmojiCategoryStrip(emojiCategories, emojiItems, emojiLayoutManager)
setupEmojiCategoryIconStrip(emojiCategories.keys, 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.
* Sets up the kaomoji RecyclerView adapter and category strip for Kaomojis mode.
*/
private fun prepareEmojiCategories(emojis: List<EmojiData>): Map<String, List<EmojiData>> = emojis.groupBy { it.category }
private fun setupKaomojiAdapter(
kaomojis: List<KaomojiData>,
language: String,
) {
val recentKaomojis = getRecentKaomojis(context)
val kaomojiCategories = mutableMapOf<String, List<KaomojiData>>()
if (recentKaomojis.isNotEmpty()) {
kaomojiCategories["kaomoji_recent"] = recentKaomojis
}
kaomojiCategories.putAll(prepareKaomojiCategories(kaomojis))

val categoryHeaders =
(emojiCategoryHeaders["EN"] ?: emptyMap()) + (emojiCategoryHeaders[getLanguageAlias(language)] ?: emptyMap())

val defaultCategoryKey = if (recentKaomojis.isNotEmpty()) "kaomoji_recent" else (kaomojiCategories.keys.firstOrNull() ?: "kaomoji_joy")
updateKaomojiGrid(defaultCategoryKey, kaomojiCategories, categoryHeaders)
setupKaomojiCategoryNameStrip(kaomojiCategories.keys, kaomojiCategories, categoryHeaders)
}

/**
* 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.
* Updates the Kaomoji grid to show ONLY items from the selected category (no section titles).
*/
private fun updateKaomojiGrid(
selectedCategoryKey: String,
kaomojiCategories: Map<String, List<KaomojiData>>,
categoryHeaders: Map<String, String>,
) {
val categoryKaomojis = kaomojiCategories[selectedCategoryKey] ?: emptyList()
val kaomojiItems = categoryKaomojis.map { EmojiAdapter.Item.Kaomoji(it) }

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 = (emojiLayoutManager.spanCount / 2).coerceAtLeast(2)
}

binding.emojisList.layoutManager = emojiLayoutManager
binding.emojisList.adapter =
EmojiAdapter(
context = context,
items = kaomojiItems,
categoryHeaders = categoryHeaders,
itemClick = {},
kaomojiClick = { kaomojiData ->
saveRecentKaomoji(context, kaomojiData)
listener.onEmojiSelected(kaomojiData.kaomoji)
},
)
}

private fun prepareEmojiCategories(emojis: List<EmojiData>): Map<String, List<EmojiData>> = emojis.groupBy { it.category }

private fun prepareKaomojiCategories(kaomojis: List<KaomojiData>): Map<String, List<KaomojiData>> = kaomojis.groupBy { it.category }

private fun prepareEmojiItems(categories: Map<String, List<EmojiData>>): List<EmojiAdapter.Item> {
val emojiItems = mutableListOf<EmojiAdapter.Item>()
val items = mutableListOf<EmojiAdapter.Item>()
categories.entries.forEach { (category, emojis) ->
emojiItems.add(EmojiAdapter.Item.Category(category))
emojis.forEach { emojiItems.add(EmojiAdapter.Item.Emoji(it)) }
items.add(EmojiAdapter.Item.Category(category))
emojis.forEach { items.add(EmojiAdapter.Item.Emoji(it)) }
}
return emojiItems
return items
}

/**
* 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.
* Populates standard emoji category icons in the bottom strip.
*/
private fun setupEmojiCategoryStrip(
categories: Map<String, List<EmojiData>>,
private fun setupEmojiCategoryIconStrip(
categoryKeys: Set<String>,
emojiItems: List<EmojiAdapter.Item>,
layoutManager: AutoGridLayoutManager,
) {
Expand All @@ -952,7 +1054,7 @@ class KeyboardUIManager(

var activeButton: android.widget.ImageButton? = null

categories.keys.forEachIndexed { index, category ->
categoryKeys.forEachIndexed { index, category ->
val button =
android.widget.ImageButton(context).apply {
setImageResource(getCategoryIconRes(category))
Expand Down Expand Up @@ -986,6 +1088,61 @@ class KeyboardUIManager(
}
}

/**
* Populates styled text chips with category names for Kaomojis mode in the bottom strip.
*/
private fun setupKaomojiCategoryNameStrip(
categoryKeys: Set<String>,
kaomojiCategories: Map<String, List<KaomojiData>>,
categoryHeaders: Map<String, String>,
) {
binding.emojiCategoriesStrip.removeAllViews()
val isDarkMode = getIsDarkModeOrNot(context)
val inactiveTextColor = if (isDarkMode) Color.LTGRAY else Color.DKGRAY

var activeChip: TextView? = null

categoryKeys.forEachIndexed { index, categoryKey ->
val headerTitle = categoryHeaders[categoryKey] ?: categoryKey.removePrefix("kaomoji_").replaceFirstChar { it.uppercase() }
val chip =
TextView(context).apply {
text = headerTitle
textSize = 13f
setPadding(28, 12, 28, 12)
layoutParams =
android.widget.LinearLayout
.LayoutParams(
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
).apply {
setMargins(8, 0, 8, 0)
}
background = ContextCompat.getDrawable(context, R.drawable.kaomoji_chip_background)
isSelected = (index == 0)
setTextColor(if (isSelected) Color.WHITE else inactiveTextColor)
setTypeface(null, if (isSelected) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)

setOnClickListener {
if (activeChip != this) {
activeChip?.isSelected = false
activeChip?.setTextColor(inactiveTextColor)
activeChip?.setTypeface(null, android.graphics.Typeface.NORMAL)

isSelected = true
setTextColor(Color.WHITE)
setTypeface(null, android.graphics.Typeface.BOLD)
activeChip = this

updateKaomojiGrid(categoryKey, kaomojiCategories, categoryHeaders)
}
}
}

if (index == 0) activeChip = chip
binding.emojiCategoriesStrip.addView(chip)
}
}

/**
* 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.
Expand Down
12 changes: 4 additions & 8 deletions app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -755,12 +755,8 @@ abstract class GeneralKeyboardIME(
* @param language The current keyboard language.
* @param isSubsequentArea true if this is for a secondary view.
*/
internal fun saveConjugateModeType(
language: String = this.language,
isSubsequentArea: Boolean = false,
) {
internal fun saveConjugateModeType(mode: String = "none") {
val sharedPref = applicationContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE)
val mode = if (!isSubsequentArea) defaultConjugateModeType else "none"
sharedPref.edit { putString("conjugate_mode_type", mode) }
}

Expand Down Expand Up @@ -1032,7 +1028,7 @@ abstract class GeneralKeyboardIME(
invalidCommandSource = ScribeState.CONJUGATE
ScribeState.INVALID
} else {
saveConjugateModeType(language)
saveConjugateModeType(defaultConjugateModeType)
ScribeState.SELECT_VERB_CONJUNCTION
}
refreshUI()
Expand Down Expand Up @@ -2024,7 +2020,7 @@ abstract class GeneralKeyboardIME(
val uniqueData = data.distinct()
val filteredData = uniqueData.filter { sublist -> sublist.contains(word) }
val flattenList = filteredData.flatten()
saveConjugateModeType(language = language, true)
saveConjugateModeType("2x1")
val prefs = applicationContext.getSharedPreferences("keyboard_preferences", MODE_PRIVATE)
prefs.edit(commit = true) { putString("conjugate_mode_type", "2x1") }
val keyboardXmlId = getKeyboardLayoutForState(currentState, true, flattenList.size)
Expand Down Expand Up @@ -2066,7 +2062,7 @@ abstract class GeneralKeyboardIME(
): Int =
when (state) {
ScribeState.SELECT_VERB_CONJUNCTION -> {
saveConjugateModeType(language)
saveConjugateModeType(defaultConjugateModeType)
if (!isSubsequentArea && dataSize == 0) {
defaultConjugateLayoutXML
} else {
Expand Down
Loading
Loading