properly determine length of emojis when deleting

fixes issue described in https://github.com/Helium314/HeliBoard/issues/426#issuecomment-1944132643, but not the initial problem
This commit is contained in:
Helium314 2024-03-03 14:48:03 +01:00
parent fe5aa2c33c
commit 4b52f2d51d
5 changed files with 65 additions and 23 deletions

View file

@ -1046,6 +1046,14 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return mCommittedTextBeforeComposingText.indexOf(" ") != -1; 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() { public boolean hasLetterBeforeLastSpaceBeforeCursor() {
return StringUtilsKt.hasLetterBeforeLastSpaceBeforeCursor(mCommittedTextBeforeComposingText); return StringUtilsKt.hasLetterBeforeLastSpaceBeforeCursor(mCommittedTextBeforeComposingText);
} }

View file

@ -191,6 +191,8 @@ public final class Constants {
public static final String REGEXP_PERIOD = "\\."; public static final String REGEXP_PERIOD = "\\.";
public static final String STRING_SPACE = " "; 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) { public static boolean isLetterCode(final int code) {
return code >= CODE_SPACE; return code >= CODE_SPACE;

View file

@ -53,6 +53,29 @@ fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean {
return letter 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 */ /** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
fun String.splitOnFirstSpacesOnly(): List<String> { fun String.splitOnFirstSpacesOnly(): List<String> {
val out = mutableListOf<String>() val out = mutableListOf<String>()

View file

@ -1253,12 +1253,8 @@ public final class InputLogic {
// TODO: Add a new StatsUtils method onBackspaceWhenNoText() // TODO: Add a new StatsUtils method onBackspaceWhenNoText()
return; return;
} }
if (StringUtils.mightBeEmoji(codePointBeforeCursor)) { final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor)
// emoji length varies, so we'd need to find out length to delete correctly ? mConnection.getCharCountToDeleteBeforeCursor() : 1;
// 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); mConnection.deleteTextBeforeCursor(lengthToDelete);
int totalDeletedLength = lengthToDelete; int totalDeletedLength = lengthToDelete;
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
@ -1270,8 +1266,8 @@ public final class InputLogic {
final int codePointBeforeCursorToDeleteAgain = final int codePointBeforeCursorToDeleteAgain =
mConnection.getCodePointBeforeCursor(); mConnection.getCodePointBeforeCursor();
if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(codePointBeforeCursorToDeleteAgain)
codePointBeforeCursorToDeleteAgain) ? 2 : 1; ? mConnection.getCharCountToDeleteBeforeCursor() : 1;
mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); mConnection.deleteTextBeforeCursor(lengthToDeleteAgain);
totalDeletedLength += lengthToDeleteAgain; totalDeletedLength += lengthToDeleteAgain;
} }
@ -1279,7 +1275,6 @@ public final class InputLogic {
StatsUtils.onBackspacePressed(totalDeletedLength); StatsUtils.onBackspacePressed(totalDeletedLength);
} }
} }
}
if (!hasUnlearnedWordBeingDeleted) { if (!hasUnlearnedWordBeingDeleted) {
// Consider unlearning the word being deleted (if we have not done so already). // Consider unlearning the word being deleted (if we have not done so already).
unlearnWordBeingDeleted( unlearnWordBeingDeleted(

View file

@ -2,6 +2,8 @@
package helium314.keyboard.latin package helium314.keyboard.latin
import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.common.StringUtils
import helium314.keyboard.latin.common.getFullEmojiAtEnd
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
// todo: actually this test could/should be significantly expanded... // todo: actually this test could/should be significantly expanded...
@ -39,6 +41,18 @@ class StringUtilsTest {
assert(StringUtils.isInsideDoubleQuoteOrAfterDigit("hello \"yes\", \"h")) 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? // todo: add tests for emoji detection?
// could help towards fully fixing https://github.com/Helium314/HeliBoard/issues/22 // 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 // though this might be tricky, as some emojis will show as one on new Android versions, and