diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java index 5b467fa9..660bcc84 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java @@ -133,18 +133,7 @@ public abstract class TypingHandler extends KeyPadHandler { } else { suggestionOps.commitCurrent(false); mInputMode.reset(); - - int charsToDelete; - - if (settings.getBackspaceAcceleration() && repeat > 0) { - charsToDelete = Math.max(textField.getPaddedWordBeforeCursorLength(), 1); - } else if (!textSelection.isEmpty()) { - charsToDelete = textSelection.length(); - textSelection.clear(false); - } else { - charsToDelete = 1; - } - textField.deleteChars(charsToDelete); + deleteText(settings.getBackspaceAcceleration() && repeat > 0); } if (settings.getBackspaceRecomposing() && repeat == 0 && suggestionOps.isEmpty() && !DictionaryLoader.getInstance(this).isRunning()) { @@ -251,6 +240,18 @@ public abstract class TypingHandler extends KeyPadHandler { } + private void deleteText(boolean deleteMany) { + int charsToDelete = 1; + if (deleteMany) { + charsToDelete = Math.max(textField.getPaddedWordBeforeCursorLength(), 1); + } else if (!textSelection.isEmpty()) { + charsToDelete = textSelection.length(); + textSelection.clear(false); + } + textField.deleteChars(charsToDelete); + } + + /** * determineLanguage * Restore the last language or auto-select a more appropriate one, if the application hints so. diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java index 846eda00..ed918fef 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -166,7 +166,7 @@ public class TextField extends InputField { return 0; } - int whitespaceShift = Math.max(before.lastBoundaryIndex(), 0); + int whitespaceShift = Math.max(before.lastBoundaryIndex(SettingsStore.BACKSPACE_ACCELERATION_MAX_CHARS_NO_SPACE), 0); return Math.min(before.length() - whitespaceShift, (int) (SettingsStore.BACKSPACE_ACCELERATION_MAX_CHARS * 1.5)); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java index 70747208..c9fd3ea3 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java @@ -7,7 +7,8 @@ public class SettingsStore extends SettingsHotkeys { public SettingsStore(Context context) { super(context); } /************* internal settings *************/ - public static final int BACKSPACE_ACCELERATION_MAX_CHARS = 20; + public static final int BACKSPACE_ACCELERATION_MAX_CHARS = 20; // maximum chars to be deleted at once in very long words + public static final int BACKSPACE_ACCELERATION_MAX_CHARS_NO_SPACE = 4; // maximum chars to be deleted at once for languages with no spaces public static final int BACKSPACE_ACCELERATION_REPEAT_DEBOUNCE = 5; public final static int CLIPBOARD_PREVIEW_LENGTH = 20; public final static int CUSTOM_WORDS_IMPORT_MAX_LINES = 250; diff --git a/app/src/main/java/io/github/sspanak/tt9/util/Text.java b/app/src/main/java/io/github/sspanak/tt9/util/Text.java index f0d337c5..fd5942f4 100644 --- a/app/src/main/java/io/github/sspanak/tt9/util/Text.java +++ b/app/src/main/java/io/github/sspanak/tt9/util/Text.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; @@ -14,6 +16,8 @@ public class Text extends TextTools { private final Language language; private final String text; + private final static Pattern QUICK_DELETE_GROUP = Pattern.compile("(?:([\\s\\u3000]{2,})|([.,、。,،]{2,})|([^、。,\\s\\u3000]*.))$"); + public Text(Language language, String text) { this.language = language; @@ -189,26 +193,38 @@ public class Text extends TextTools { } - public int lastBoundaryIndex() { + /** + * Returns the starting index of the last word boundary. This could be start of a word, of a whitespace + * block or of a punctuation block (e.g. "..."). In the case of languages that do not use spaces + * it is not possible to determine where a word starts, so in case the text ends with letters only, + * we assume the last word is at most MAX_WORD_LENGTH_NO_SPACE letters long. + */ + public int lastBoundaryIndex(final int MAX_WORD_LENGTH_NO_SPACE) { if (text == null || text.length() < 2) { return -1; } - char lastChar = text.charAt(text.length() - 1); - char penultimateChar = text.charAt(text.length() - 2); + Matcher matcher = QUICK_DELETE_GROUP.matcher(text); + for (int i = matcher.find() ? matcher.groupCount() : 0, nonLetterGroup = 0; i >= 0; i--, nonLetterGroup = 1) { + String group = matcher.group(i); + if (group == null) { + continue; + } - boolean endsWithWhitespaceBlock = Character.isWhitespace(lastChar) && Character.isWhitespace(penultimateChar); - boolean endsWithPunctuationBlock = (lastChar == '.' || lastChar == ',') && (penultimateChar == '.' || penultimateChar == ','); + if (nonLetterGroup == 1) { + return matcher.start(); + } - for (int i = text.length() - 1, firstChar = 1; i >= 0; i--, firstChar = 0) { - char currentChar = text.charAt(i); + Text gr = new Text(group); + int codePoints = gr.codePointLength(); - if ( - (endsWithPunctuationBlock && currentChar != '.' && currentChar != ',') - || (endsWithWhitespaceBlock && !Character.isWhitespace(currentChar)) - || (!endsWithWhitespaceBlock && !endsWithPunctuationBlock && firstChar == 0 && Character.isWhitespace(currentChar)) - ) { - return i + 1; + // In the writing systems that do not use spaces, the last group is not the last word, but + // it could be an entire sentence or a paragraph. That's why we assume an average word length + // of N letters + if (codePoints > MAX_WORD_LENGTH_NO_SPACE && (TextTools.isChinese(group) || TextTools.isJapanese(group) || TextTools.isThai(group))) { + return text.length() - gr.substringCodePoints(codePoints - 4, codePoints).length(); + } else { + return matcher.start(); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/util/TextTools.java b/app/src/main/java/io/github/sspanak/tt9/util/TextTools.java index 3f463290..b36e3e53 100644 --- a/app/src/main/java/io/github/sspanak/tt9/util/TextTools.java +++ b/app/src/main/java/io/github/sspanak/tt9/util/TextTools.java @@ -17,6 +17,7 @@ public class TextTools { private static final Pattern IS_CHINESE = Pattern.compile("\\p{script=Han}+"); private static final Pattern IS_JAPANESE = Pattern.compile("\\p{script=Hiragana}+|\\p{script=Katakana}+|\\p{script=Han}+"); private static final Pattern IS_HANGUL = Pattern.compile("[\u1100-\u11FF\u302E-\u302F\u3131-\u318F\u3200-\u321F\u3260-\u327E\uA960-\uA97F\uAC00-\uD7FB\uFFA0-\uFFDF]+"); + private static final Pattern IS_THAI = Pattern.compile("[\\u0E00-\\u0E7F]+"); private static final Pattern NEXT_TO_WORD = Pattern.compile("\\b$"); private static final Pattern PREVIOUS_IS_LETTER = Pattern.compile("[\\p{L}\\p{M}](?!\\n)$"); private static final Pattern START_OF_SENTENCE = Pattern.compile("(? list, String str) { for (int i = 0, size = list != null && str != null ? list.size() : 0; i < size; i++) { if (list.get(i).equalsIgnoreCase(str)) {