diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java b/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java index b863ef1ba..f4a58015b 100644 --- a/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java +++ b/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java @@ -1046,6 +1046,14 @@ public final class RichInputConnection implements PrivateCommandPerformer { return mCommittedTextBeforeComposingText.indexOf(" ") != -1; } + public int getCharCountToDeleteBeforeCursor() { + final int lastCodePoint = getCodePointBeforeCursor(); + if (!Character.isSupplementaryCodePoint(lastCodePoint)) return 1; + if (!StringUtils.mightBeEmoji(lastCodePoint)) return 2; + final String text = mCommittedTextBeforeComposingText.toString() + mComposingText; + return StringUtilsKt.getFullEmojiAtEnd(text).length(); + } + public boolean hasLetterBeforeLastSpaceBeforeCursor() { return StringUtilsKt.hasLetterBeforeLastSpaceBeforeCursor(mCommittedTextBeforeComposingText); } diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.java b/app/src/main/java/helium314/keyboard/latin/common/Constants.java index e697cd0b2..e660745e1 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/Constants.java +++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.java @@ -191,6 +191,8 @@ public final class Constants { public static final String REGEXP_PERIOD = "\\."; public static final String STRING_SPACE = " "; + public static final int CODE_ZWNJ = '\u200C'; + public static final int CODE_ZWJ = '\u200D'; public static boolean isLetterCode(final int code) { return code >= CODE_SPACE; diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt index b5131d5fe..c017afa13 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt @@ -53,6 +53,29 @@ fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean { return letter } +/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */ +fun getFullEmojiAtEnd(s: CharSequence): String { + val text = if (s is String) s else s.toString() + var offset = text.length + while (offset > 0) { + val codepoint = text.codePointBefore(offset) + // stop if codepoint can't be emoji + if (!mightBeEmoji(codepoint)) return "" + offset -= Character.charCount(codepoint) + if (offset > 0 && text[offset - 1].code == Constants.CODE_ZWJ) { + // continue if ZWJ + offset -= 1 + continue + } + // check the whole text after offset + val textToCheck = text.substring(offset) + if (isEmoji(textToCheck)) { + return textToCheck + } + } + return "" +} + /** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */ fun String.splitOnFirstSpacesOnly(): List { val out = mutableListOf() diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 6d5147222..b15ab9afb 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -1253,31 +1253,26 @@ public final class InputLogic { // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } - if (StringUtils.mightBeEmoji(codePointBeforeCursor)) { - // emoji length varies, so we'd need to find out length to delete correctly - // the solution is not optimal, but a reasonable workaround for issues when trying to delete emojis - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - } else { - final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteTextBeforeCursor(lengthToDelete); - int totalDeletedLength = lengthToDelete; - if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { - // If this is an accelerated (i.e., double) deletion, then we need to - // consider unlearning here because we may have already reached - // the previous word, and will lose it after next deletion. - hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( - inputTransaction.getMSettingsValues(), currentKeyboardScript); - final int codePointBeforeCursorToDeleteAgain = - mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { - final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( - codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); - totalDeletedLength += lengthToDeleteAgain; - } + final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) + ? mConnection.getCharCountToDeleteBeforeCursor() : 1; + mConnection.deleteTextBeforeCursor(lengthToDelete); + int totalDeletedLength = lengthToDelete; + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here because we may have already reached + // the previous word, and will lose it after next deletion. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.getMSettingsValues(), currentKeyboardScript); + final int codePointBeforeCursorToDeleteAgain = + mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { + final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(codePointBeforeCursorToDeleteAgain) + ? mConnection.getCharCountToDeleteBeforeCursor() : 1; + mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); + totalDeletedLength += lengthToDeleteAgain; } - StatsUtils.onBackspacePressed(totalDeletedLength); } + StatsUtils.onBackspacePressed(totalDeletedLength); } } if (!hasUnlearnedWordBeingDeleted) { diff --git a/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt b/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt index 20e7fe2ed..cb5f0d9b9 100644 --- a/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/StringUtilsTest.kt @@ -2,6 +2,8 @@ package helium314.keyboard.latin import helium314.keyboard.latin.common.StringUtils +import helium314.keyboard.latin.common.getFullEmojiAtEnd +import org.junit.Assert.assertEquals import org.junit.Test // todo: actually this test could/should be significantly expanded... @@ -39,6 +41,18 @@ class StringUtilsTest { assert(StringUtils.isInsideDoubleQuoteOrAfterDigit("hello \"yes\", \"h")) } + @Test fun detectEmojisAtEnd() { + assertEquals("\uD83C\uDF83", getFullEmojiAtEnd("\uD83C\uDF83")) + assertEquals("ℹ️", getFullEmojiAtEnd("ℹ️")) + assertEquals("ℹ️", getFullEmojiAtEnd("ℹ️ℹ️")) + assertEquals("\uD83D\uDE22", getFullEmojiAtEnd("x\uD83D\uDE22")) + assertEquals("", getFullEmojiAtEnd("x\uD83D\uDE22 ")) + assertEquals("\uD83C\uDFF4\u200D☠️", getFullEmojiAtEnd("ok \uD83C\uDFF4\u200D☠️")) + assertEquals("\uD83C\uDFF3️\u200D\uD83C\uDF08", getFullEmojiAtEnd("\uD83C\uDFF3️\u200D\uD83C\uDF08")) + assertEquals("\uD83C\uDFF3️\u200D\uD83C\uDF08", getFullEmojiAtEnd("\uD83C\uDFF4\u200D☠️\uD83C\uDFF3️\u200D\uD83C\uDF08")) + assertEquals("\uD83C\uDFF3️\u200D⚧️", getFullEmojiAtEnd("hello there🏳️‍⚧️")) + } + // todo: add tests for emoji detection? // could help towards fully fixing https://github.com/Helium314/HeliBoard/issues/22 // though this might be tricky, as some emojis will show as one on new Android versions, and