1
0
Fork 0

new dev bug: fixed issues with automatic word accepting

This commit is contained in:
sspanak 2025-05-20 11:36:37 +03:00 committed by Dimo Karaivanov
parent 38ca93ab9f
commit c8c484a570
12 changed files with 85 additions and 84 deletions

View file

@ -172,7 +172,6 @@ public abstract class TypingHandler extends KeyPadHandler {
protected boolean onNumber(int key, boolean hold, int repeat) { protected boolean onNumber(int key, boolean hold, int repeat) {
suggestionOps.cancelDelayedAccept(); suggestionOps.cancelDelayedAccept();
// In Korean, the next char may "steal" components from the previous one, in which case, // In Korean, the next char may "steal" components from the previous one, in which case,
// we must replace the previous char with a one containing less strokes. // we must replace the previous char with a one containing less strokes.
if (mInputMode.shouldReplaceLastLetter(key, hold)) { if (mInputMode.shouldReplaceLastLetter(key, hold)) {
@ -183,7 +182,7 @@ public abstract class TypingHandler extends KeyPadHandler {
// First pass, analyze the incoming key press and decide whether it could be the start of // First pass, analyze the incoming key press and decide whether it could be the start of
// a new word. In case we do accept it, we preserve the suggestion list instead of clearing, // a new word. In case we do accept it, we preserve the suggestion list instead of clearing,
// to prevent flashing while the next suggestions are being loaded. // to prevent flashing while the next suggestions are being loaded.
else if (mInputMode.shouldAcceptPreviousSuggestion(key, hold)) { else if (mInputMode.shouldAcceptPreviousSuggestion(suggestionOps.getCurrent(), key, hold)) {
// WARNING! Ensure the code after "acceptIncompleteAndKeepList()" does not depend on // WARNING! Ensure the code after "acceptIncompleteAndKeepList()" does not depend on
// the suggestions in SuggestionOps, since we don't clear that list. // the suggestions in SuggestionOps, since we don't clear that list.
String lastWord = suggestionOps.acceptIncompleteAndKeepList(); String lastWord = suggestionOps.acceptIncompleteAndKeepList();

View file

@ -153,7 +153,7 @@ abstract public class InputMode {
// Interaction with the IME. Return "true" if it should perform the respective action. // Interaction with the IME. Return "true" if it should perform the respective action.
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) { return false; } public boolean shouldAcceptPreviousSuggestion(String unacceptedText) { return false; }
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { return false; } public boolean shouldAcceptPreviousSuggestion(String currentWord, int nextKey, boolean hold) { return false; }
public boolean shouldReplacePreviousSuggestion(@Nullable String currentWord) { return Characters.PLACEHOLDER.equals(currentWord); } public boolean shouldReplacePreviousSuggestion(@Nullable String currentWord) { return Characters.PLACEHOLDER.equals(currentWord); }
public boolean shouldAddTrailingSpace(boolean isWordAcceptedManually, int nextKey) { return false; } public boolean shouldAddTrailingSpace(boolean isWordAcceptedManually, int nextKey) { return false; }
public boolean shouldAddPrecedingSpace() { return false; } public boolean shouldAddPrecedingSpace() { return false; }

View file

@ -16,7 +16,7 @@ class Mode123 extends ModePassthrough {
@Override @NonNull public String toString() { return "123"; } @Override @NonNull public String toString() { return "123"; }
@Override public int getSequenceLength() { return digitSequence.length(); } @Override public int getSequenceLength() { return digitSequence.length(); }
@Override public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { return true; } @Override public boolean shouldAcceptPreviousSuggestion(String currentWord, int nextKey, boolean hold) { return true; }
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>(); private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();

View file

@ -94,10 +94,10 @@ public class ModeBopomofo extends ModePinyin {
* In Bopomofo mode, the 0-key is not Spacebar, so we do not rely on the parents to handle accepting * In Bopomofo mode, the 0-key is not Spacebar, so we do not rely on the parents to handle accepting
*/ */
@Override @Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { public boolean shouldAcceptPreviousSuggestion(String currentWord, int nextDigit, boolean hold) {
String newSequence = digitSequence + (char)(nextKey + '0'); String newSequence = digitSequence + (char)(nextDigit + '0');
return hold return hold
|| newSequence.startsWith(seq.CHARS_0_SEQUENCE) || newSequence.startsWith(seq.CHARS_0_SEQUENCE)
|| (newSequence.startsWith(seq.CHARS_1_SEQUENCE) && nextKey != Sequences.CHARS_1_KEY); || (newSequence.startsWith(seq.CHARS_1_SEQUENCE) && nextDigit != Sequences.CHARS_1_KEY);
} }
} }

View file

@ -340,7 +340,7 @@ class ModeCheonjiin extends InputMode {
* Used for analysis before processing the incoming pressed key. * Used for analysis before processing the incoming pressed key.
*/ */
@Override @Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { public boolean shouldAcceptPreviousSuggestion(String currentWord, int nextKey, boolean hold) {
return return
(hold && !digitSequence.isEmpty()) (hold && !digitSequence.isEmpty())
|| (nextKey != Sequences.CHARS_0_KEY && digitSequence.startsWith(seq.CHARS_0_SEQUENCE)) || (nextKey != Sequences.CHARS_0_KEY && digitSequence.startsWith(seq.CHARS_0_SEQUENCE))

View file

@ -11,7 +11,6 @@ import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.Logger;
import io.github.sspanak.tt9.util.Text; import io.github.sspanak.tt9.util.Text;
import io.github.sspanak.tt9.util.TextTools;
public class ModeIdeograms extends ModeWords { public class ModeIdeograms extends ModeWords {
private static final String LOG_TAG = ModeIdeograms.class.getSimpleName(); private static final String LOG_TAG = ModeIdeograms.class.getSimpleName();
@ -107,7 +106,7 @@ public class ModeIdeograms extends ModeWords {
final Text text = new Text(currentWord); final Text text = new Text(currentWord);
if (text.isEmpty() || text.startsWithWhitespace() || text.isNumeric() || !language.isValidWord(currentWord)) { if (text.isEmpty() || text.startsWithWhitespace() || text.isNumeric() || !language.isValidWord(currentWord)) {
reset(); reset();
Logger.i(LOG_TAG, "Current word is empty, numeric or invalid. Nothing to accept."); Logger.i(LOG_TAG, "Current word: '" + currentWord + "' is empty, numeric or invalid. Nothing to accept.");
return; return;
} }
@ -122,7 +121,7 @@ public class ModeIdeograms extends ModeWords {
boolean lastDigitBelongsToNewWord = preserveWords && initialLength >= 2; boolean lastDigitBelongsToNewWord = preserveWords && initialLength >= 2;
try { try {
if (!digitSequence.equals(seq.CHARS_0_SEQUENCE) && !digitSequence.equals(seq.CHARS_1_SEQUENCE)) { if (!seq.isAnySpecialCharSequence(digitSequence)) {
lastAcceptedWord = currentWord; lastAcceptedWord = currentWord;
lastAcceptedSequence = lastDigitBelongsToNewWord ? digitSequence.substring(0, initialLength - 1) : digitSequence; lastAcceptedSequence = lastDigitBelongsToNewWord ? digitSequence.substring(0, initialLength - 1) : digitSequence;
@ -148,30 +147,8 @@ public class ModeIdeograms extends ModeWords {
return return
digitSequence.length() > 1 digitSequence.length() > 1
&& predictions.noDbWords() && predictions.noDbWords()
&& !digitSequence.equals(seq.EMOJI_SEQUENCE) && !seq.isAnySpecialCharSequence(digitSequence)
&& !digitSequence.equals(seq.CHARS_1_SEQUENCE) && !digitSequence.startsWith(seq.EMOJI_SEQUENCE);
&& !digitSequence.equals(seq.CHARS_0_SEQUENCE);
}
@Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) {
if (digitSequence.isEmpty()) {
return false;
}
if (super.shouldAcceptPreviousSuggestion(nextKey, hold)) {
return true;
}
String nextSequence = digitSequence + (char)(nextKey + '0');
return
TextTools.containsOtherThan1(nextSequence)
&& (
nextSequence.endsWith(seq.EMOJI_SEQUENCE) || nextSequence.startsWith(seq.EMOJI_SEQUENCE) ||
nextSequence.endsWith(seq.CHARS_1_SEQUENCE) || nextSequence.startsWith(seq.CHARS_1_SEQUENCE)
);
} }

View file

@ -40,21 +40,6 @@ public class ModeKanji extends ModePinyin {
} }
@Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) {
if (digitSequence.isEmpty()) {
return false;
}
String nextSequence = digitSequence + (char)(nextKey + '0');
if (seq.isAnySpecialCharSequence(nextSequence)) {
return false;
}
return super.shouldAcceptPreviousSuggestion(nextKey, hold);
}
@Override @Override
public boolean validateLanguage(@Nullable Language newLanguage) { public boolean validateLanguage(@Nullable Language newLanguage) {
return LanguageKind.isJapanese(newLanguage); return LanguageKind.isJapanese(newLanguage);

View file

@ -56,14 +56,14 @@ public class ModePinyin extends ModeIdeograms {
@Override @Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { public boolean shouldAcceptPreviousSuggestion(String currentWord, int nextDigit, boolean hold) {
// In East Asian languages, Space must accept the current word, or type a space when there is no word. // In East Asian languages, Space must accept the current word, or type a space when there is no word.
// Here, we handle the case when 0-key is Space, unlike the Space hotkey in HotkeyHandler, // Here, we handle the case when 0-key is Space, unlike the Space hotkey in HotkeyHandler,
// which could be a different key, assigned by the user. // which could be a different key, assigned by the user.
if (!digitSequence.isEmpty() && !digitSequence.equals(seq.CHARS_0_SEQUENCE) && nextKey == Sequences.CHARS_0_KEY) { if (nextDigit == Sequences.CHARS_0_KEY && !digitSequence.isEmpty() && !seq.isAnySpecialCharSequence(digitSequence)) {
ignoreNextSpace = true; ignoreNextSpace = true;
} }
return super.shouldAcceptPreviousSuggestion(nextKey, hold); return super.shouldAcceptPreviousSuggestion(currentWord, nextDigit, hold);
} }
} }

View file

@ -395,25 +395,36 @@ class ModeWords extends ModeCheonjiin {
* automatically. This is used for analysis before processing the incoming pressed key. * automatically. This is used for analysis before processing the incoming pressed key.
*/ */
@Override @Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { public boolean shouldAcceptPreviousSuggestion(String currentWord, int nextDigit, boolean hold) {
if (hold) { if (hold) {
return true; return true;
} }
if (digitSequence.isEmpty()) {
return false;
} else if (
digitSequence.equals(seq.CUSTOM_EMOJI_SEQUENCE) ||
(seq.startsWithEmojiSequence(digitSequence) && nextDigit != Sequences.CHARS_1_KEY && nextDigit != Sequences.CUSTOM_EMOJI_KEY)
) {
return true;
}
// Prevent typing the preferred character when the user has scrolled the special char suggestions. // Prevent typing the preferred character when the user has scrolled the special char suggestions.
// For example, it makes more sense to allow typing "+ " with 0 + scroll + 0, instead of clearing // For example, it makes more sense to allow typing "+ " with 0 + scroll + 0, instead of clearing
// the "+" and replacing it with the preferred character. // the "+" and replacing it with the preferred character.
boolean specialOrCurrency = digitSequence.equals(seq.CHARS_GROUP_0_SEQUENCE) || digitSequence.equals(seq.CHARS_GROUP_1_SEQUENCE); // Also don't type the preferred character when viewing a group. In that case we obviously want to
boolean isWhitespaceAndScrolled = digitSequence.equals(seq.CHARS_0_SEQUENCE) && !stem.isEmpty(); // type a space after the character from the group.
if (nextKey == Sequences.CHARS_0_KEY && (isWhitespaceAndScrolled || specialOrCurrency)) { boolean inGroup = digitSequence.equals(seq.CHARS_GROUP_0_SEQUENCE) || digitSequence.equals(seq.CHARS_GROUP_1_SEQUENCE);
boolean isWhitespaceAndScrolled = digitSequence.equals(seq.CHARS_0_SEQUENCE) && !suggestions.isEmpty() && !suggestions.get(0).equals(currentWord);
if (nextDigit == Sequences.CHARS_0_KEY && (isWhitespaceAndScrolled || inGroup)) {
return true; return true;
} }
final char lastDigit = digitSequence.charAt(digitSequence.length() - 1);
return return
!digitSequence.isEmpty() && ( (nextDigit == Sequences.CHARS_0_KEY && lastDigit != Sequences.CHARS_0_CODE)
(nextKey == Sequences.CHARS_0_KEY && digitSequence.charAt(digitSequence.length() - 1) != Sequences.CHARS_0_CODE) || (nextDigit != Sequences.CHARS_0_KEY && lastDigit == Sequences.CHARS_0_CODE);
|| (nextKey != Sequences.CHARS_0_KEY && digitSequence.charAt(digitSequence.length() - 1) == Sequences.CHARS_0_CODE)
);
} }
@ -432,13 +443,15 @@ class ModeWords extends ModeCheonjiin {
return false; return false;
} }
// punctuation breaks words, unless there are database matches ('s, qu', по-, etc...)
return return
!digitSequence.isEmpty() !digitSequence.isEmpty()
&& predictions.noDbWords() && predictions.noDbWords()
&& digitSequence.contains(seq.CHARS_1_SEQUENCE) && (
&& !digitSequence.startsWith(seq.EMOJI_SEQUENCE) // when no custom emoji, assume the last digit is the beginning of a new word
&& Text.containsOtherThan1(digitSequence); digitSequence.equals(seq.CUSTOM_EMOJI_SEQUENCE)
// punctuation breaks words, unless there are database matches ('s, qu', по-, etc...)
|| (digitSequence.contains(seq.CHARS_1_SEQUENCE) && !digitSequence.equals(seq.CHARS_1_SEQUENCE))
);
} }

View file

@ -11,7 +11,7 @@ abstract class TranscribedLanguage extends Language implements Comparable<Transc
} }
return return
(LanguageKind.isKorean(this) && TextTools.isHangul(word)) (LanguageKind.isKorean(this) && TextTools.isHangulText(word)) // because of the way Korean works, we only need to check if it's text.
|| (LanguageKind.isChinese(this) && TextTools.isChinese(word)) || (LanguageKind.isChinese(this) && TextTools.isChinese(word))
|| (LanguageKind.isJapanese(this) && TextTools.isJapanese(word)); || (LanguageKind.isJapanese(this) && TextTools.isJapanese(word));
} }

View file

@ -255,7 +255,7 @@ public class Text extends TextTools {
// In the writing systems that do not use spaces, the last group is not the last word, but // 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 // it could be an entire sentence or a paragraph. That's why we assume an average word length
// of N letters // of N letters
if (codePoints > MAX_WORD_LENGTH_NO_SPACE && (TextTools.isChinese(group) || TextTools.isJapanese(group) || TextTools.isThai(group))) { if (codePoints > MAX_WORD_LENGTH_NO_SPACE && (TextTools.isChineseText(group) || TextTools.isJapaneseText(group) || TextTools.isThaiText(group))) {
return text.length() - gr.substringCodePoints(codePoints - 4, codePoints).length(); return text.length() - gr.substringCodePoints(codePoints - 4, codePoints).length();
} else { } else {
return matcher.start(); return matcher.start();

View file

@ -11,28 +11,27 @@ import java.util.regex.Pattern;
import io.github.sspanak.tt9.util.chars.Characters; import io.github.sspanak.tt9.util.chars.Characters;
public class TextTools { public class TextTools {
private static final Pattern CONTAINS_OTHER_THAN_1 = Pattern.compile("[02-9]");
private static final Pattern COMBINING_STRING = Pattern.compile("^\\p{M}+$"); private static final Pattern COMBINING_STRING = Pattern.compile("^\\p{M}+$");
private static final Pattern NEXT_IS_PUNCTUATION = Pattern.compile("^\\p{Punct}"); private static final Pattern NEXT_IS_PUNCTUATION = Pattern.compile("^\\p{Punct}");
private static final Pattern IS_CHINESE = Pattern.compile("\\p{script=Han}+"); private static final Pattern IS_CHINESE = Pattern.compile("[\\p{script=Han}" + String.join("", Characters.PunctuationChinese) + "]+");
private static final Pattern IS_JAPANESE = Pattern.compile("\\p{script=Hiragana}+|\\p{script=Katakana}+|\\p{script=Han}+"); private static final Pattern IS_HANGUL_TEXT = Pattern.compile("[\u1100-\u11FF\u302E-\u302F\u3131-\u318F\u3200-\u321F\u3260-\u327E\uA960-\uA97F\uAC00-\uD7FB\uFFA0-\uFFDF]+");
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_JAPANESE = Pattern.compile("[\\p{script=Hiragana}\\p{script=Katakana}\\p{script=Han}" + String.join("", Characters.PunctuationChinese) + "]+");
private static final Pattern IS_THAI = Pattern.compile("[\\u0E00-\\u0E7F]+"); private static final Pattern IS_CHINESE_TEXT = Pattern.compile("\\p{script=Han}+");
private static final Pattern IS_JAPANESE_TEXT = Pattern.compile("[\\p{script=Hiragana}\\p{script=Katakana}\\p{script=Han}]+");
private static final Pattern IS_THAI_TEXT = Pattern.compile("[\\u0E00-\\u0E7F]+");
private static final Pattern NEXT_TO_WORD = Pattern.compile("\\b$"); 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 PREVIOUS_IS_LETTER = Pattern.compile("[\\p{L}\\p{M}](?!\\n)$");
private static final Pattern START_OF_SENTENCE = Pattern.compile("(?<!\\.)(^|[.?!؟¿¡])\\s+$"); private static final Pattern START_OF_SENTENCE = Pattern.compile("(?<!\\.)(^|[.?!؟¿¡])\\s+$");
public static boolean containsOtherThan1(String str) {
return str != null && CONTAINS_OTHER_THAN_1.matcher(str).find();
}
public static boolean isCombining(String str) { public static boolean isCombining(String str) {
return str != null && COMBINING_STRING.matcher(str).find(); return str != null && COMBINING_STRING.matcher(str).find();
} }
/**
* Validates if the string contains only graphic characters (e.g. emojis)
*/
public static boolean isGraphic(String str) { public static boolean isGraphic(String str) {
if (str == null || str.isEmpty()) { if (str == null || str.isEmpty()) {
return false; return false;
@ -48,23 +47,51 @@ public class TextTools {
} }
/**
* Validates Chinese text and punctuation.
*/
public static boolean isChinese(String str) { public static boolean isChinese(String str) {
return str != null && IS_CHINESE.matcher(str).find(); return str != null && IS_CHINESE.matcher(str).find();
} }
public static boolean isHangul(String str) { /**
return str != null && IS_HANGUL.matcher(str).find(); * Validates Japanese text and punctuation.
} */
public static boolean isJapanese(String str) { public static boolean isJapanese(String str) {
return str != null && IS_JAPANESE.matcher(str).find(); return str != null && IS_JAPANESE.matcher(str).find();
} }
public static boolean isThai(String str) { /**
return str != null && IS_THAI.matcher(str).find(); * Validates Chinese text only, but no punctuation.
*/
public static boolean isChineseText(String str) {
return str != null && IS_CHINESE_TEXT.matcher(str).find();
}
/**
* Validates Japanese text only, but no punctuation.
*/
public static boolean isJapaneseText(String str) {
return str != null && IS_JAPANESE_TEXT.matcher(str).find();
}
/**
* Validates Korean text only, but no punctuation.
*/
public static boolean isHangulText(String str) {
return str != null && IS_HANGUL_TEXT.matcher(str).find();
}
/**
* Validates Thai text only, but no punctuation.
*/
public static boolean isThaiText(String str) {
return str != null && IS_THAI_TEXT.matcher(str).find();
} }