@@ -87,6 +87,16 @@ abstract class GeneralKeyboardIME(
8787 private var emojiSpaceTablet2: View ? = null
8888 private var emojiBtnTablet3: Button ? = null
8989
90+ private var colonEmojiBtn1: Button ? = null
91+ private var colonEmojiBtn2: Button ? = null
92+ private var colonEmojiBtn3: Button ? = null
93+ private var colonEmojiBtn4: Button ? = null
94+ private var colonEmojiBtn5: Button ? = null
95+ private var colonEmojiBtn6: Button ? = null
96+ private var colonEmojiBtn7: Button ? = null
97+ private var colonEmojiBtn8: Button ? = null
98+ private var colonEmojiBtn9: Button ? = null
99+
90100 private var genderSuggestionLeft: Button ? = null
91101 private var genderSuggestionRight: Button ? = null
92102 private var isSingularAndPlural: Boolean = false
@@ -103,6 +113,8 @@ abstract class GeneralKeyboardIME(
103113 var emojiAutoSuggestionEnabled: Boolean = false
104114 var lastWord: String? = null
105115 var autoSuggestEmojis: MutableList <String >? = null
116+ private var isColonEmojiModeActive: Boolean = false
117+ private var colonEmojiQuery: String = " "
106118 var caseAnnotationSuggestion: MutableList <String >? = null
107119 var nounTypeSuggestion: List <String >? = null
108120 var checkIfPluralWord: Boolean = false
@@ -753,6 +765,15 @@ abstract class GeneralKeyboardIME(
753765 emojiBtnTablet2 = binding.emojiBtnTablet2
754766 emojiSpaceTablet2 = binding.emojiSpaceTablet2
755767 emojiBtnTablet3 = binding.emojiBtnTablet3
768+ colonEmojiBtn1 = binding.colonEmojiBtn1
769+ colonEmojiBtn2 = binding.colonEmojiBtn2
770+ colonEmojiBtn3 = binding.colonEmojiBtn3
771+ colonEmojiBtn4 = binding.colonEmojiBtn4
772+ colonEmojiBtn5 = binding.colonEmojiBtn5
773+ colonEmojiBtn6 = binding.colonEmojiBtn6
774+ colonEmojiBtn7 = binding.colonEmojiBtn7
775+ colonEmojiBtn8 = binding.colonEmojiBtn8
776+ colonEmojiBtn9 = binding.colonEmojiBtn9
756777 genderSuggestionLeft = binding.translateBtnLeft
757778 genderSuggestionRight = binding.translateBtnRight
758779 }
@@ -768,14 +789,51 @@ abstract class GeneralKeyboardIME(
768789 resources.configuration.screenLayout and
769790 Configuration .SCREENLAYOUT_SIZE_MASK
770791 ) >= Configuration .SCREENLAYOUT_SIZE_LARGE
792+ if (isColonEmojiModeActive) {
793+ // Hide all normal toolbar elements.
794+ binding.scribeKey.visibility = View .GONE
795+ binding.separator1.visibility = View .GONE
796+ binding.translateBtn.visibility = View .GONE
797+ binding.translateBtnLeft.visibility = View .GONE
798+ binding.translateBtnRightText.visibility = View .GONE
799+ binding.translateBtnRight.visibility = View .GONE
800+ binding.separator2.visibility = View .GONE
801+ binding.conjugateBtn.visibility = View .GONE
802+ binding.separator3.visibility = View .GONE
803+ pluralBtn?.visibility = View .GONE
804+ binding.separator4.visibility = View .GONE
805+ emojiBtnPhone1?.visibility = View .GONE
806+ emojiSpacePhone?.visibility = View .GONE
807+ emojiBtnPhone2?.visibility = View .GONE
808+ binding.separator5.visibility = View .GONE
809+ emojiBtnTablet1?.visibility = View .GONE
810+ emojiSpaceTablet1?.visibility = View .GONE
811+ emojiBtnTablet2?.visibility = View .GONE
812+ emojiSpaceTablet2?.visibility = View .GONE
813+ binding.separator6.visibility = View .GONE
814+ emojiBtnTablet3?.visibility = View .GONE
815+
816+ // Show dedicated colon emoji slots.
817+ val slotCount = if (isTablet) 9 else 6
818+ listOf (
819+ colonEmojiBtn1, colonEmojiBtn2, colonEmojiBtn3,
820+ colonEmojiBtn4, colonEmojiBtn5, colonEmojiBtn6,
821+ ).forEach { btn -> btn?.visibility = View .VISIBLE }
822+ listOf (colonEmojiBtn7, colonEmojiBtn8, colonEmojiBtn9).forEach { btn ->
823+ btn?.visibility = if (slotCount == 9 ) View .VISIBLE else View .GONE
824+ }
825+ return
826+ }
771827 if (isTablet) {
828+ binding.scribeKey.visibility = View .VISIBLE
772829 pluralBtn?.visibility = if (isAutoSuggestEnabled) View .INVISIBLE else View .VISIBLE
773830 emojiBtnTablet1?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
774831 emojiSpaceTablet1?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
775832 emojiBtnTablet2?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
776833 emojiSpaceTablet2?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
777834 emojiBtnTablet3?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
778835 } else {
836+ binding.scribeKey.visibility = View .VISIBLE
779837 pluralBtn?.visibility = if (isAutoSuggestEnabled) View .INVISIBLE else View .VISIBLE
780838 emojiBtnPhone1?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
781839 emojiSpacePhone?.visibility = if (isAutoSuggestEnabled) View .VISIBLE else View .INVISIBLE
@@ -805,6 +863,110 @@ abstract class GeneralKeyboardIME(
805863 return lastWord
806864 }
807865
866+ private fun getCurrentTokenBeforeCursor (): String {
867+ val textBeforeCursor = getText() ? : return " "
868+ if (textBeforeCursor.isEmpty()) return " "
869+ val lastWhitespaceIndex = textBeforeCursor.indexOfLast { it.isWhitespace() }
870+ return if (lastWhitespaceIndex == - 1 ) {
871+ textBeforeCursor
872+ } else {
873+ textBeforeCursor.substring(lastWhitespaceIndex + 1 )
874+ }
875+ }
876+
877+ fun isColonEmojiModeEnabled (): Boolean = isColonEmojiModeActive
878+
879+ fun handleColonEmojiMode (code : Int ) {
880+ if (code == KeyboardBase .KEYCODE_SPACE ) {
881+ exitColonEmojiMode()
882+ return
883+ }
884+
885+ val token = getCurrentTokenBeforeCursor()
886+
887+ when {
888+ // Token starts with ":" — enter or stay in colon mode.
889+ token.startsWith(" :" ) -> {
890+ isColonEmojiModeActive = true
891+ colonEmojiQuery = token.drop(1 ).lowercase()
892+ }
893+ // In colon mode but token no longer starts with ":" — exit.
894+ isColonEmojiModeActive && token.isNotBlank() -> {
895+ exitColonEmojiMode()
896+ return
897+ }
898+ // Not in colon mode and token doesn't start with ":" — nothing to do.
899+ else -> return
900+ }
901+
902+ if (! isColonEmojiModeActive) return
903+
904+ val isTablet =
905+ (
906+ resources.configuration.screenLayout and
907+ Configuration .SCREENLAYOUT_SIZE_MASK
908+ ) >= Configuration .SCREENLAYOUT_SIZE_LARGE
909+ val requiredCount = if (isTablet) 9 else 6
910+ val matched =
911+ if (colonEmojiQuery.isBlank()) {
912+ mutableListOf<String >()
913+ } else {
914+ findEmojisForQueryPrefix(colonEmojiQuery)
915+ }
916+ autoSuggestEmojis = fillWithFallbackEmojis(matched, requiredCount)
917+
918+ updateButtonVisibility(true )
919+ updateButtonText(true , autoSuggestEmojis)
920+ }
921+
922+ private fun findEmojisForQueryPrefix (query : String ): MutableList <String > {
923+ val matched = LinkedHashSet <String >()
924+ emojiKeywords
925+ .filterKeys { it.startsWith(query, ignoreCase = true ) }
926+ .values
927+ .forEach { emojiList ->
928+ emojiList.forEach { matched.add(it) }
929+ }
930+ return matched.take(MAX_COLON_EMOJI_RESULTS ).toMutableList()
931+ }
932+
933+ private fun fillWithFallbackEmojis (
934+ matched : List <String >,
935+ requiredCount : Int ,
936+ ): MutableList <String > {
937+ val orderedUnique = LinkedHashSet <String >()
938+ matched.forEach { emoji ->
939+ if (emoji.isNotBlank()) {
940+ orderedUnique.add(emoji)
941+ }
942+ }
943+ DEFAULT_COLON_EMOJIS .forEach { emoji ->
944+ if (orderedUnique.size < requiredCount) {
945+ orderedUnique.add(emoji)
946+ }
947+ }
948+ return orderedUnique.take(requiredCount).toMutableList()
949+ }
950+
951+ private fun exitColonEmojiMode () {
952+ isColonEmojiModeActive = false
953+ colonEmojiQuery = " "
954+ // Hide dedicated colon emoji buttons.
955+ listOf (
956+ colonEmojiBtn1, colonEmojiBtn2, colonEmojiBtn3,
957+ colonEmojiBtn4, colonEmojiBtn5, colonEmojiBtn6,
958+ colonEmojiBtn7, colonEmojiBtn8, colonEmojiBtn9,
959+ ).forEach { btn -> btn?.visibility = View .GONE }
960+ // Restore normal toolbar.
961+ binding.scribeKey.visibility = View .VISIBLE
962+ binding.separator1.visibility = View .VISIBLE
963+ if (currentState == ScribeState .IDLE || currentState == ScribeState .SELECT_COMMAND ) {
964+ setupIdleView()
965+ }
966+ updateButtonVisibility(emojiAutoSuggestionEnabled)
967+ disableAutoSuggest()
968+ }
969+
808970 /* *
809971 * Finds and returns a list of emojis that are relevant to the last word typed.
810972 *
@@ -909,6 +1071,33 @@ abstract class GeneralKeyboardIME(
9091071 autoSuggestEmojis : MutableList <String >? ,
9101072 ) {
9111073 if (isAutoSuggestEnabled) {
1074+ if (isColonEmojiModeActive) {
1075+ val emojis = (autoSuggestEmojis ? : mutableListOf ()).toList()
1076+ val isTablet =
1077+ (
1078+ resources.configuration.screenLayout and
1079+ Configuration .SCREENLAYOUT_SIZE_MASK
1080+ ) >= Configuration .SCREENLAYOUT_SIZE_LARGE
1081+ val slotCount = if (isTablet) 9 else 6
1082+ val slotText = fillWithFallbackEmojis(emojis, slotCount)
1083+
1084+ val colonBtns =
1085+ listOf (
1086+ colonEmojiBtn1, colonEmojiBtn2, colonEmojiBtn3,
1087+ colonEmojiBtn4, colonEmojiBtn5, colonEmojiBtn6,
1088+ colonEmojiBtn7, colonEmojiBtn8, colonEmojiBtn9,
1089+ ).take(slotCount)
1090+
1091+ colonBtns.forEachIndexed { index, btn ->
1092+ btn?.text = slotText.getOrElse(index) { " " }
1093+ btn?.setOnClickListener { insertEmoji(btn.text.toString()) }
1094+ }
1095+ return
1096+ }
1097+
1098+ binding.translateBtn.visibility = View .VISIBLE
1099+ binding.translateBtnLeft.visibility = View .INVISIBLE
1100+ binding.translateBtnRight.visibility = View .INVISIBLE
9121101 emojiBtnTablet1?.text = autoSuggestEmojis?.get(0 )
9131102 emojiBtnTablet2?.text = autoSuggestEmojis?.get(1 )
9141103 emojiBtnTablet3?.text = autoSuggestEmojis?.get(2 )
@@ -1292,11 +1481,21 @@ abstract class GeneralKeyboardIME(
12921481 * @param emoji The emoji character to be inserted.
12931482 */
12941483 private fun insertEmoji (emoji : String ) {
1484+ if (emoji.isBlank()) return
12951485 val inputConnection = currentInputConnection ? : return
12961486 val maxLookBack = emojiMaxKeywordLength.coerceAtLeast(1 )
12971487
12981488 inputConnection.beginBatchEdit()
12991489 try {
1490+ if (isColonEmojiModeActive) {
1491+ val token = getCurrentTokenBeforeCursor()
1492+ if (token.startsWith(" :" )) {
1493+ inputConnection.deleteSurroundingText(token.length, 0 )
1494+ }
1495+ inputConnection.commitText(emoji, 1 )
1496+ return
1497+ }
1498+
13001499 val previousText = inputConnection.getTextBeforeCursor(maxLookBack, 0 )?.toString() ? : " "
13011500
13021501 // Find last word boundary efficiently
@@ -1707,5 +1906,7 @@ abstract class GeneralKeyboardIME(
17071906 const val LIGHT_THEME = " #4b4b4b"
17081907 const val MAX_TEXT_LENGTH = 1000
17091908 const val COMMIT_TEXT_CURSOR_POSITION = 1
1909+ private val DEFAULT_COLON_EMOJIS = listOf (" 😂" , " ❤️" , " 😍" , " 🔥" , " 👍" , " 🙏" , " 😊" , " 🎉" , " 😭" )
1910+ private const val MAX_COLON_EMOJI_RESULTS = 9
17101911 }
17111912}
0 commit comments