package org.futo.inputmethod.v2keyboard import android.content.Context import android.text.InputType import android.util.Log import android.view.inputmethod.EditorInfo import androidx.compose.ui.unit.dp import kotlinx.serialization.Serializable import org.futo.inputmethod.keyboard.Keyboard import org.futo.inputmethod.keyboard.KeyboardId import org.futo.inputmethod.keyboard.internal.KeyboardLayoutElement import org.futo.inputmethod.keyboard.internal.KeyboardLayoutKind import org.futo.inputmethod.keyboard.internal.KeyboardLayoutPage import org.futo.inputmethod.keyboard.internal.KeyboardParams import org.futo.inputmethod.latin.settings.LongPressKeySettings import org.futo.inputmethod.latin.uix.KeyboardHeightMultiplierSetting import org.futo.inputmethod.latin.uix.actions.BugInfo import org.futo.inputmethod.latin.uix.actions.BugViewerState import org.futo.inputmethod.latin.uix.getSettingBlocking import org.futo.inputmethod.latin.utils.InputTypeUtils import org.futo.inputmethod.latin.utils.ResourceUtils import java.util.Locale import kotlin.math.roundToInt @Serializable enum class Script(val id: Int, val iso4letterCode: String) { Unknown(-1, ""), Arabic(0, "arab"), Armenian(1, "armn"), Bengali(2, "beng"), Cyrillic(3, "cyrl"), Devanagari(4, "deva"), Georgian(5, "geor"), Greek(6, "grek"), Hebrew(7, "hebr"), Kannada(8, "knda"), Khmer(9, "khmr"), Lao(10, "laoo"), Latin(11, "latn"), Malayalam(12, "mlym"), Myanmar(13, "mymr"), Sinhala(14, "sinh"), Tamil(15, "taml"), Telugu(16, "telu"), Thai(17, "thai"), } fun Locale.getKeyboardScript(): Script = script.lowercase().let { code -> Script.entries.firstOrNull { it.iso4letterCode == code } ?: Script.Unknown } private fun EditorInfo.getPrivateImeOptions(): Map<String, String> { val options = mutableMapOf<String, String>() val imeOptions = privateImeOptions ?: return options try { imeOptions.split(",").forEach { option -> if (option.contains('=') && option.split('=').size == 2) { val (key, value) = option.split("=") options[key.trim()] = value.trim() } } } catch(e: Exception) { e.printStackTrace() } return options } private fun EditorInfo.getPrimaryLayoutOverride(): String? = getPrivateImeOptions()["org.futo.inputmethod.latin.ForceLayout"] data class KeyboardLayoutSetV2Params( val width: Int, val height: Int?, val keyboardLayoutSet: String, val locale: Locale, val editorInfo: EditorInfo, val numberRow: Boolean, val gap: Float = 4.0f, val useSplitLayout: Boolean, val bottomActionKey: Int? ) class KeyboardLayoutSetV2 internal constructor( private val context: Context, private val params: KeyboardLayoutSetV2Params ) { val script = Script.Latin val privateParams = params.editorInfo.getPrivateImeOptions() val forcedLayout = privateParams["org.futo.inputmethod.latin.ForceLayout"] val forcedLocale = privateParams["org.futo.inputmethod.latin.ForceLocale"]?.let { Locale.forLanguageTag(it) } // Necessary for Java API fun getScriptId(): Int = script.id private val keyboardMode = getKeyboardMode(params.editorInfo) val layoutName = forcedLayout ?: params.keyboardLayoutSet val mainLayout = LayoutManager.getLayout(context, layoutName) val symbolsLayout = LayoutManager.getLayout(context, mainLayout.layoutSetOverrides.symbols) val symbolsShiftedLayout = LayoutManager.getLayout(context, mainLayout.layoutSetOverrides.symbolsShifted) val numberLayout = LayoutManager.getLayout(context, mainLayout.layoutSetOverrides.number) val numberShiftLayout = LayoutManager.getLayout(context, mainLayout.layoutSetOverrides.numberShifted) val phoneLayout = LayoutManager.getLayout(context, mainLayout.layoutSetOverrides.phone) val phoneSymbolsLayout = LayoutManager.getLayout(context, mainLayout.layoutSetOverrides.phoneShifted) val errorLayout = LayoutManager.getLayout(context, "error") val elements = mapOf( KeyboardLayoutElement( kind = KeyboardLayoutKind.Alphabet, page = KeyboardLayoutPage.Base ) to mainLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Alphabet, page = KeyboardLayoutPage.Shifted ) to mainLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Symbols, page = KeyboardLayoutPage.Base ) to symbolsLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Symbols, page = KeyboardLayoutPage.Shifted ) to symbolsShiftedLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Phone, page = KeyboardLayoutPage.Base ) to phoneLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Phone, page = KeyboardLayoutPage.Shifted ) to phoneSymbolsLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Number, page = KeyboardLayoutPage.Base ) to numberLayout, KeyboardLayoutElement( kind = KeyboardLayoutKind.Number, page = KeyboardLayoutPage.Shifted ) to numberShiftLayout, ) private fun getKeyboardLayoutForElement(element: KeyboardLayoutElement): org.futo.inputmethod.v2keyboard.Keyboard { return elements[element.normalize()] ?: run { // If this is an alt layout, try to get the matching alt element.page.altIdx?.let { altIdx -> val baseElement = element.copy(page = KeyboardLayoutPage.Base) val baseLayout = elements[baseElement] baseLayout?.altPages?.get(altIdx) }?.let { mainLayout.copy(rows = it) } } ?: run { // If all else fails, show the error layout BugViewerState.pushBug(BugInfo("KeyboardLayoutSet", "Keyboard $layoutName does not have element $element. Available elements: ${elements.keys}")) errorLayout } } private val isNumberRowActive: Boolean get() = when(mainLayout.numberRowMode) { NumberRowMode.UserConfigurable -> params.numberRow NumberRowMode.AlwaysEnabled -> true NumberRowMode.AlwaysDisabled -> false } private val singularRowHeight: Double get() = params.height?.let { it / 4.0 } ?: run { (ResourceUtils.getDefaultKeyboardHeight(context.resources) / 4.0) * keyboardHeightMultiplier } private fun getRecommendedKeyboardHeight(): Int { val numRows = 4.0 + ((mainLayout.effectiveRows.size - 5) / 2.0).coerceAtLeast(0.0) + if(isNumberRowActive) { 0.5 } else { 0.0 } // Clamp if necessary (disabled for now) if(false && params.height == null) { return ResourceUtils.clampKeyboardHeight(context.resources, (singularRowHeight * numRows).roundToInt()) } else { return (singularRowHeight * numRows).roundToInt() } } fun getKeyboard(element: KeyboardLayoutElement): Keyboard { val keyboardId = KeyboardId( params.keyboardLayoutSet, forcedLocale ?: params.locale, params.width, params.height ?: getRecommendedKeyboardHeight(), keyboardMode, element.elementId, params.editorInfo, false, params.bottomActionKey != null, params.bottomActionKey ?: -1, params.editorInfo.actionLabel?.toString() ?: "", false, false, isNumberRowActive, LongPressKeySettings(context) ) val layout = getKeyboardLayoutForElement(element) val keyboardParams = KeyboardParams().apply { mId = keyboardId mTextsSet.setLocale(keyboardId.locale, context) } val layoutParams = LayoutParams( gap = params.gap.dp, useSplitLayout = params.useSplitLayout, standardRowHeight = singularRowHeight, element = element ) try { return layout.build(context, keyboardParams, layoutParams) } catch(e: Exception) { Log.e("KeyboardLayoutSet", "Failed to load element $element for keyboard layout set $layoutName. Message: ${e.message}") Log.e("KeyboardLayoutSet", "LayoutSet params: $params, keyboardId: $keyboardId") e.printStackTrace() BugViewerState.pushBug(BugInfo( name = "KeyboardLayoutSet", details = """ Element $element for layout $layoutName could not be loaded Cause: ${e.message} Params: $params Stack trace: ${e.stackTrace.map { it.toString() }} """ )) return errorLayout.build(context, keyboardParams, layoutParams) } } companion object { var keyboardHeightMultiplier: Float = 1.0f @JvmStatic fun onSystemLocaleChanged() { } @JvmStatic fun onKeyboardThemeChanged(context: Context) { keyboardHeightMultiplier = context.getSettingBlocking(KeyboardHeightMultiplierSetting) // This is where we would clear all caches if we had any } } } public fun getKeyboardMode(editorInfo: EditorInfo): Int { val inputType = editorInfo.inputType val variation = inputType and InputType.TYPE_MASK_VARIATION return when (inputType and InputType.TYPE_MASK_CLASS) { InputType.TYPE_CLASS_NUMBER -> KeyboardId.MODE_NUMBER InputType.TYPE_CLASS_DATETIME -> when (variation) { InputType.TYPE_DATETIME_VARIATION_DATE -> KeyboardId.MODE_DATE InputType.TYPE_DATETIME_VARIATION_TIME -> KeyboardId.MODE_TIME else -> KeyboardId.MODE_DATETIME } InputType.TYPE_CLASS_PHONE -> KeyboardId.MODE_PHONE InputType.TYPE_CLASS_TEXT -> if (InputTypeUtils.isEmailVariation(variation)) { KeyboardId.MODE_EMAIL } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { KeyboardId.MODE_URL } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { KeyboardId.MODE_IM } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { KeyboardId.MODE_TEXT } else { KeyboardId.MODE_TEXT } else -> KeyboardId.MODE_TEXT } }