diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4eecaf70..75145fee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) + implementation(libs.androidx.emoji2.emojipicker) implementation(libs.material) implementation(libs.guava) implementation(libs.protobuf.java) diff --git a/app/src/main/java/ee/oyatl/ime/fusion/DimensionUtil.kt b/app/src/main/java/ee/oyatl/ime/fusion/DimensionUtil.kt new file mode 100644 index 00000000..dacec3de --- /dev/null +++ b/app/src/main/java/ee/oyatl/ime/fusion/DimensionUtil.kt @@ -0,0 +1,42 @@ +package ee.oyatl.ime.fusion + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.util.TypedValue +import androidx.preference.PreferenceManager +import kotlin.math.roundToInt + +object DimensionUtil { + fun getOrientationSuffix(context: Context): String { + val landscape = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val suffix = if(landscape) "_landscape" else "_portrait" + return suffix + } + + fun getOrientationInteger(context: Context, key: String): Float { + val preference = PreferenceManager.getDefaultSharedPreferences(context) + val suffix = getOrientationSuffix(context) + @SuppressLint("DiscouragedApi") + val defaultId = context.resources.getIdentifier("${key}${suffix}_default", "integer", context.packageName) + val defaultValue = context.resources.getInteger(defaultId).toFloat() + val value = preference.getFloat("${key}${suffix}", defaultValue) + return value + } + + fun getOrientationBoolean(context: Context, key: String): Boolean { + val preference = PreferenceManager.getDefaultSharedPreferences(context) + val suffix = getOrientationSuffix(context) + @SuppressLint("DiscouragedApi") + val defaultId = context.resources.getIdentifier("${key}${suffix}_default", "bool", context.packageName) + val defaultValue = context.resources.getBoolean(defaultId) + val value = preference.getBoolean("${key}${suffix}", defaultValue) + return value + } + + fun getKeyboardHeight(context: Context): Int { + val rowHeightDIP = getOrientationInteger(context, "keyboard_height") + val height = (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rowHeightDIP, context.resources.displayMetrics) * 4).roundToInt() + return height + } +} \ No newline at end of file diff --git a/app/src/main/java/ee/oyatl/ime/fusion/KeyEventUtil.kt b/app/src/main/java/ee/oyatl/ime/fusion/KeyEventUtil.kt index b4f2a73e..9efaa998 100644 --- a/app/src/main/java/ee/oyatl/ime/fusion/KeyEventUtil.kt +++ b/app/src/main/java/ee/oyatl/ime/fusion/KeyEventUtil.kt @@ -1,5 +1,6 @@ package ee.oyatl.ime.fusion +import android.os.Build import android.os.SystemClock import android.view.KeyCharacterMap import android.view.KeyEvent @@ -115,4 +116,11 @@ class KeyEventUtil( } } + fun deleteSurroundingText(beforeLength: Int, afterLength: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + currentInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength) + } else { + currentInputConnection.deleteSurroundingText(beforeLength, afterLength) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ee/oyatl/ime/fusion/mode/CommonIMEMode.kt b/app/src/main/java/ee/oyatl/ime/fusion/mode/CommonIMEMode.kt index 05000adf..f8ef8428 100644 --- a/app/src/main/java/ee/oyatl/ime/fusion/mode/CommonIMEMode.kt +++ b/app/src/main/java/ee/oyatl/ime/fusion/mode/CommonIMEMode.kt @@ -1,8 +1,6 @@ package ee.oyatl.ime.fusion.mode -import android.annotation.SuppressLint import android.content.Context -import android.content.res.Configuration import android.util.TypedValue import android.view.KeyCharacterMap import android.view.KeyEvent @@ -12,6 +10,7 @@ import android.view.inputmethod.InputConnection import androidx.preference.PreferenceManager import ee.oyatl.ime.candidate.CandidateView import ee.oyatl.ime.candidate.ScrollingCandidateView +import ee.oyatl.ime.fusion.DimensionUtil import ee.oyatl.ime.fusion.Feature import ee.oyatl.ime.fusion.FlickAction import ee.oyatl.ime.fusion.KeyEventUtil @@ -155,41 +154,14 @@ abstract class CommonIMEMode( submitCandidates(emptyList()) } - private fun getOrientationSuffix(context: Context): String { - val landscape = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val suffix = if(landscape) "_landscape" else "_portrait" - return suffix - } - - private fun getOrientationInteger(context: Context, key: String): Float { - val preference = PreferenceManager.getDefaultSharedPreferences(context) - val suffix = getOrientationSuffix(context) - @SuppressLint("DiscouragedApi") - val defaultId = context.resources.getIdentifier("${key}${suffix}_default", "integer", context.packageName) - val defaultValue = context.resources.getInteger(defaultId).toFloat() - val value = preference.getFloat("${key}${suffix}", defaultValue) - return value - } - - private fun getOrientationBoolean(context: Context, key: String): Boolean { - val preference = PreferenceManager.getDefaultSharedPreferences(context) - val suffix = getOrientationSuffix(context) - @SuppressLint("DiscouragedApi") - val defaultId = context.resources.getIdentifier("${key}${suffix}_default", "bool", context.packageName) - val defaultValue = context.resources.getBoolean(defaultId) - val value = preference.getBoolean("${key}${suffix}", defaultValue) - return value - } - override fun createInputView(context: Context): View { val preference = PreferenceManager.getDefaultSharedPreferences(context) val defaultScreenMode = context.resources.getString(R.string.screen_mode_default) val screenMode = KeyboardState.ScreenMode.valueOf(preference.getString("screen_mode", null) ?: defaultScreenMode) - val rowHeightDIP = getOrientationInteger(context, "keyboard_height") - val height = (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rowHeightDIP, context.resources.displayMetrics) * 4).roundToInt() - val split = Feature.SplitKeyboard.availableInCurrentVersion && getOrientationBoolean(context, "split_keyboard") - val splitRatio = if(split) getOrientationInteger(context, "split_ratio") else 0f + val height = DimensionUtil.getKeyboardHeight(context) + val split = Feature.SplitKeyboard.availableInCurrentVersion && DimensionUtil.getOrientationBoolean(context, "split_keyboard") + val splitRatio = if(split) DimensionUtil.getOrientationInteger(context, "split_ratio") else 0f val splitWidthDIP = context.resources.configuration.screenWidthDp / 100f * splitRatio val splitWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, splitWidthDIP, context.resources.displayMetrics).roundToInt() val showPreviewPopup = preference.getBoolean("preview_popup", true) diff --git a/app/src/main/java/ee/oyatl/ime/fusion/mode/EmojiIMEMode.kt b/app/src/main/java/ee/oyatl/ime/fusion/mode/EmojiIMEMode.kt new file mode 100644 index 00000000..73edb615 --- /dev/null +++ b/app/src/main/java/ee/oyatl/ime/fusion/mode/EmojiIMEMode.kt @@ -0,0 +1,136 @@ +package ee.oyatl.ime.fusion.mode + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.core.util.Consumer +import androidx.emoji2.emojipicker.EmojiViewItem +import ee.oyatl.ime.candidate.CandidateView +import ee.oyatl.ime.candidate.ScrollingCandidateView +import ee.oyatl.ime.fusion.DimensionUtil +import ee.oyatl.ime.fusion.KeyEventUtil +import ee.oyatl.ime.fusion.R +import ee.oyatl.ime.fusion.databinding.EmojiImeModeBinding + +class EmojiIMEMode( + private val listener: IMEMode.Listener +): IMEMode, Consumer, CandidateView.Listener { + + private var inputView: View? = null + private var candidateView: CandidateView? = null + private var util: KeyEventUtil? = null + private val currentInputConnection: InputConnection? get() = util?.currentInputConnection + + override suspend fun onLoad(context: Context) { + } + + override fun onStart( + inputConnection: InputConnection, + editorInfo: EditorInfo + ) { + util = KeyEventUtil(inputConnection, editorInfo) + currentInputConnection?.finishComposingText() + } + + override fun onFinish() { + currentInputConnection?.finishComposingText() + } + + override fun createInputView(context: Context): View { + val height = DimensionUtil.getKeyboardHeight(context) + val binding = EmojiImeModeBinding.inflate(LayoutInflater.from(context)) + binding.emojiPickerView.setOnEmojiPickedListener(this) + binding.buttonDelete.setOnClickListener { + onDelete() + } + binding.root.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + height + ) + this.inputView = binding.root + return binding.root + } + + override fun createCandidateView(context: Context): View { + candidateView = ScrollingCandidateView(context, null).apply { + listener = this@EmojiIMEMode + } + return candidateView as View + } + + override fun getInputView(): View? { + return inputView + } + + override fun accept(value: EmojiViewItem) { + currentInputConnection?.finishComposingText() + currentInputConnection?.setComposingText(value.emoji, 1) + val candidates = value.variants.map { Candidate(it) } + submitCandidates(candidates) + } + + private fun submitCandidates(candidates: List) { + candidateView?.submitList(candidates) + listener.onCandidateViewVisibilityChange(candidates.isNotEmpty()) + } + + override fun onCandidateSelected(candidate: CandidateView.Candidate) { + currentInputConnection?.setComposingText(candidate.text, 1) + currentInputConnection?.finishComposingText() + submitCandidates(emptyList()) + } + + private fun onDelete() { + currentInputConnection?.finishComposingText() + util?.deleteSurroundingText(1, 0) + } + + override fun onKeyDown(keyCode: Int, metaState: Int) = Unit + + override fun onKeyUp(keyCode: Int, metaState: Int) = Unit + + override fun updateSelection( + oldSelStart: Int, + oldSelEnd: Int, + newSelStart: Int, + newSelEnd: Int, + candidatesStart: Int, + candidatesEnd: Int + ) = Unit + + data class Candidate( + override val text: CharSequence + ): CandidateView.Candidate + + class Params: IMEMode.Params { + override val type: String = TYPE + + override fun create(listener: IMEMode.Listener): IMEMode { + return EmojiIMEMode(listener) + } + + override fun getLabel(context: Context): String { + return context.getString(R.string.input_mode_emoji) + } + + override fun getShortLabel( + context: Context, + params: List + ): String { + return "\uD83D\uDE00" + } + + companion object { + fun parse(map: Map): Params { + return Params() + } + } + } + + companion object { + const val TYPE: String = "emoji" + } +} \ No newline at end of file diff --git a/app/src/main/java/ee/oyatl/ime/fusion/mode/IMEMode.kt b/app/src/main/java/ee/oyatl/ime/fusion/mode/IMEMode.kt index 20e221ef..c9ced0bc 100644 --- a/app/src/main/java/ee/oyatl/ime/fusion/mode/IMEMode.kt +++ b/app/src/main/java/ee/oyatl/ime/fusion/mode/IMEMode.kt @@ -52,6 +52,7 @@ interface IMEMode { ZhuyinIMEMode.TYPE -> ZhuyinIMEMode.Params.parse(map) CangjieIMEMode.TYPE -> CangjieIMEMode.Params.parse(map) VietIMEMode.TYPE -> VietIMEMode.Params.parse(map) + EmojiIMEMode.TYPE -> EmojiIMEMode.Params.parse(map) else -> null } } diff --git a/app/src/main/java/ee/oyatl/ime/fusion/mode/KoreanIMEMode.kt b/app/src/main/java/ee/oyatl/ime/fusion/mode/KoreanIMEMode.kt index 112fd6aa..35ad84ab 100644 --- a/app/src/main/java/ee/oyatl/ime/fusion/mode/KoreanIMEMode.kt +++ b/app/src/main/java/ee/oyatl/ime/fusion/mode/KoreanIMEMode.kt @@ -109,7 +109,7 @@ abstract class KoreanIMEMode( } else if(wordComposer.composingText.isNotEmpty()) { wordComposer.delete(1) } else { - currentInputConnection?.deleteSurroundingText(1, 0) + util?.deleteSurroundingText(1, 0) } renderInputView() } diff --git a/app/src/main/java/ee/oyatl/ime/fusion/mode/PinyinIMEMode.kt b/app/src/main/java/ee/oyatl/ime/fusion/mode/PinyinIMEMode.kt index 734c43a3..f854ee67 100644 --- a/app/src/main/java/ee/oyatl/ime/fusion/mode/PinyinIMEMode.kt +++ b/app/src/main/java/ee/oyatl/ime/fusion/mode/PinyinIMEMode.kt @@ -325,7 +325,7 @@ class PinyinIMEMode( if (SIMULATE_KEY_DELETE) { simulateKeyEventDownUp(keyCode) } else { - currentInputConnection?.deleteSurroundingText(1, 0) + util?.deleteSurroundingText(1, 0) } return true } @@ -360,7 +360,7 @@ class PinyinIMEMode( if (SIMULATE_KEY_DELETE) { simulateKeyEventDownUp(keyCode) } else { - currentInputConnection?.deleteSurroundingText(1, 0) + util?.deleteSurroundingText(1, 0) } return true } else if (keyCode == KeyEvent.KEYCODE_ENTER) { diff --git a/app/src/main/java/ee/oyatl/ime/fusion/mode/ZhuyinIMEMode.kt b/app/src/main/java/ee/oyatl/ime/fusion/mode/ZhuyinIMEMode.kt index 082e7b6f..e968ec1f 100644 --- a/app/src/main/java/ee/oyatl/ime/fusion/mode/ZhuyinIMEMode.kt +++ b/app/src/main/java/ee/oyatl/ime/fusion/mode/ZhuyinIMEMode.kt @@ -136,14 +136,13 @@ class ZhuyinIMEMode( } private fun handleBackspace() { - val ic = currentInputConnection ?: return var deleteChar = false if (wordComposer.typedWord?.isNotEmpty() == true) { val length: Int = wordComposer.typedWord.length if (length > 0) { wordComposer.deleteLast() } else { - ic.deleteSurroundingText(1, 0) + util?.deleteSurroundingText(1, 0) } } else { deleteChar = true diff --git a/app/src/main/res/layout/emoji_ime_mode.xml b/app/src/main/res/layout/emoji_ime_mode.xml new file mode 100644 index 00000000..aac31043 --- /dev/null +++ b/app/src/main/res/layout/emoji_ime_mode.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a7e03d0e..e0d5e09c 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -45,6 +45,7 @@ @string/input_mode_zhuyin @string/input_mode_cangjie @string/input_mode_viet + @string/input_mode_emoji @@ -55,6 +56,7 @@ zhuyin cangjie viet + emoji diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f4cc7bb..a8d16414 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ Bopomofo (Traditional Chinese) Cangjie Vietnamese + Emoji German English diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0889c1d5..ddd87d5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ legacySupportV4 = "1.0.0" annotation = "1.10.0" preference = "1.2.1" desugarJdkLibs = "2.1.5" +emoji2Emojipicker = "1.5.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -33,6 +34,7 @@ androidx-legacy-support-v4 = { group = "androidx.legacy", name = "legacy-support androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" } desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs" } +androidx-emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emoji2Emojipicker" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }