1
0
Fork 0
* added Korean language

* fokin context no more messing up everything in the InputModes

* no more unnecessary textField and inputType passing in the InputModes

* a single source of truth for the InputMode kind

* ModePredictive -> ModeWords

* no more db queries to increase the priority of emojis and special chars

* Korean virtual keypad

* more consistent displaying of the ABC string

* sorted out the labels of 1-key and 0-key in numeric modes

* documentation update
This commit is contained in:
Dimo Karaivanov 2024-11-28 13:20:49 +02:00 committed by GitHub
parent f3c701fd55
commit 5a108dcda9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 13010 additions and 609 deletions

View file

@ -8,13 +8,13 @@ ext.convertDictionaries = { definitionsInputDir, dictionariesInputDir, dictionar
int errorCount = 0
def errorStream = fileTree(dir: definitionsInputDir).getFiles().parallelStream().map { definition ->
def (_, sounds, __, locale, dictionaryFile, langFileErrorCount, langFileErrorMsg) = parseLanguageDefintion(definition, dictionariesInputDir)
def (_, sounds, noSyllables, locale, dictionaryFile, langFileErrorCount, langFileErrorMsg) = parseLanguageDefintion(definition, dictionariesInputDir)
errorCount += langFileErrorCount
if (!langFileErrorMsg.isEmpty()) {
return langFileErrorMsg
}
def (conversionErrorCount, conversionErrorMessages) = convertDictionary(definition, dictionaryFile, dictionariesOutputDir, dictionariesMetaDir, DICTIONARY_OUTPUT_EXTENSION, sounds, locale, MAX_ERRORS, CSV_DELIMITER)
def (conversionErrorCount, conversionErrorMessages) = convertDictionary(definition, dictionaryFile, dictionariesOutputDir, dictionariesMetaDir, DICTIONARY_OUTPUT_EXTENSION, sounds, noSyllables, locale, MAX_ERRORS, CSV_DELIMITER)
errorCount += conversionErrorCount
if (!conversionErrorMessages.isEmpty()) {
return conversionErrorMessages
@ -31,7 +31,7 @@ ext.convertDictionaries = { definitionsInputDir, dictionariesInputDir, dictionar
// this cannot be static, because DictionaryTools will not be visible
def convertDictionary(File definition, File csvDictionary, String dictionariesOutputDir, String dictionariesMetaDir, String outputDictionaryExtension, HashMap<String, String> sounds, Locale locale, int maxErrors, String csvDelimiter) {
def convertDictionary(File definition, File csvDictionary, String dictionariesOutputDir, String dictionariesMetaDir, String outputDictionaryExtension, HashMap<String, String> sounds, boolean noSyllables, Locale locale, int maxErrors, String csvDelimiter) {
if (isDictionaryUpToDate(definition, csvDictionary, dictionariesMetaDir)) {
return [0, ""]
}
@ -70,7 +70,7 @@ def convertDictionary(File definition, File csvDictionary, String dictionariesOu
outputDictionary = sortDictionary(outputDictionary)
def (assetError, zippedDictionary) = writeZippedDictionary(dictionariesOutputDir, csvDictionary, outputDictionary, outputDictionaryExtension)
def (assetError, zippedDictionary) = writeZippedDictionary(dictionariesOutputDir, csvDictionary, outputDictionary, outputDictionaryExtension, noSyllables)
if (assetError) {
errorCount++
errorMsg += assetError
@ -88,12 +88,12 @@ def convertDictionary(File definition, File csvDictionary, String dictionariesOu
//////////////////// DICTIONARY PROCESSING ////////////////////
static byte[] compressDictionaryLine(String digitSequence, List<String> words) {
static byte[] compressDictionaryLine(String digitSequence, List<String> words, boolean noSyllables) {
if (words.isEmpty()) {
throw new IllegalArgumentException("No words for digit sequence: ${digitSequence}")
}
boolean shouldSeparateWords = false
boolean shouldSeparateWords = !noSyllables
for (def i = 0; i < words.size(); i++) {
if (words.get(i).length() != digitSequence.length()) {
@ -104,7 +104,7 @@ static byte[] compressDictionaryLine(String digitSequence, List<String> words) {
return (
digitSequence +
(shouldSeparateWords ? ' ' : '') +
(shouldSeparateWords && noSyllables ? ' ' : '') + // if the language definition has sounds (aka the characters are syllables), we separate the words for sure, so the initial hint is not needed
words.join(shouldSeparateWords ? ' ' : null)
).getBytes(StandardCharsets.UTF_8)
}
@ -166,7 +166,7 @@ static getZipDictionaryFile(dictionariesOutputDir, csvDictionary, outputDictiona
/**
* Zipping the text files results in a smaller APK in comparison to the uncompressed text files.
*/
static def writeZippedDictionary(dictionariesOutputDir, csvDictionaryFile, outputDictionary, outputDictionaryExtension) {
static def writeZippedDictionary(dictionariesOutputDir, csvDictionaryFile, outputDictionary, outputDictionaryExtension, noSyllables) {
def fileName = getDictionaryFileName(csvDictionaryFile)
def outputFile = getZipDictionaryFile(dictionariesOutputDir, csvDictionaryFile, outputDictionaryExtension)
@ -174,7 +174,7 @@ static def writeZippedDictionary(dictionariesOutputDir, csvDictionaryFile, outpu
def zipOutputStream = new ZipOutputStream(new FileOutputStream(outputFile))
zipOutputStream.putNextEntry(new ZipEntry("${fileName}.txt"))
outputDictionary.each { digitSequence, words ->
zipOutputStream.write(compressDictionaryLine(digitSequence, words))
zipOutputStream.write(compressDictionaryLine(digitSequence, words, noSyllables))
}
zipOutputStream.closeEntry()
zipOutputStream.close()

View file

@ -17,7 +17,7 @@ class Wrapper {
static def wordToDigitSequence(Locale locale, String word, HashMap<String, String> sounds, boolean isTranscribed) {
String sequence = ""
def sequence = new StringBuilder()
final String normalizedWord = isTranscribed ? word : word.toUpperCase(locale)
String currentSound = ""
@ -32,7 +32,7 @@ class Wrapper {
// charAt(i) returns "ΐ" as three separate characters, but they must be treated as one.
if (
locale.getLanguage() == "el"
&& (nextCharType == Character.NON_SPACING_MARK || nextCharType == Character.ENCLOSING_MARK || nextCharType == Character.COMBINING_SPACING_MARK)
&& (nextCharType == Character.NON_SPACING_MARK || nextCharType == Character.ENCLOSING_MARK || nextCharType == Character.COMBINING_SPACING_MARK)
) {
continue
}
@ -41,7 +41,7 @@ class Wrapper {
if (!sounds.containsKey(currentSound)) {
throw new IllegalArgumentException("Sound or layout entry '${currentSound}' does not belong to the language sound list: ${sounds}.")
} else {
sequence += sounds.get(currentSound)
sequence << sounds.get(currentSound)
currentSound = ""
}
}
@ -51,7 +51,7 @@ class Wrapper {
throw new IllegalArgumentException("The word does not contain any valid sounds.")
}
return sequence
return sequence.toString()
}

View file

@ -0,0 +1,72 @@
locale: ko-KR
dictionaryFile: ko-utf8.csv
hasUpperCase: no
layout: # only used for the virtual key labels
- [ㅇ,ㅁ,SPECIAL] # 0
- [ㅣ,PUNCTUATION_KR] # 1
- [ㆍ,] # 2
- [ㅡ] # 3
- [ㄱ,ㅋ,ㄲ] # 4
- [ㄴ,ㄹ] # 5
- [ㄷ,ㅌ,ㄸ] # 6
- [ㅂ,ㅍ,ㅃ] # 7
- [ㅅ,ㅎ,ㅆ] # 8
- [ㅈ,ㅊ,ㅉ] # 9
sounds:
########## initial consonants ##########
- [G,4]
- [K,44]
- [Gg,444]
- [N,5]
- [L,55] # also: "R", but we need a unique identifier, so we use "L"
- [D,6]
- [T,66]
- [Dd,666]
- [B,7]
- [P,77]
- [Bb,777]
- [S,8]
- [H,88]
- [Ss,888]
- [J,9]
- [C,99] # also transcribed as "Ch"
- [Jj,999]
- [Z,0] # "ㅇ" when zero initial consonant
- [Ng,0]
- [M,00]
########## vowels ##########
- [I,1]
- [A,12]
- [Ae,121]
- [Ya,122]
- [Yae,1221]
- [Q,2] # "Q" is just some random unique identifier
- [Qq, 22]
- [Eo,21]
- [E,211]
- [Yeo,221]
- [Ye,2211]
- [Yo,223]
- [O,23]
- [Oe,231]
- [Wa,2312]
- [Wae,23121]
- [Eu,3]
- [Ui,31]
- [U,32]
- [Wi,321]
- [Yu,322]
- [Wo,3221]
- [We,32211]
########## final consonants ##########
- [Gs,48]
- [Lg,554]
- [Lb,557]
- [Ls,558]
- [Lt,5566]
- [Lp,5577]
- [Lh,5588]
- [Lm,5500]
- [Nh,588]
- [Nj,59]
- [Bs,78]

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
android:versionCode="775"
android:versionName="40.1"
android:versionCode="799"
android:versionName="40.25"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- allows displaying notifications on Android >= 13 -->

View file

@ -112,7 +112,7 @@ public class DataStore {
private static void getWordsSync(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) {
try {
ArrayList<String> data = words.getSimilar(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords);
ArrayList<String> data = words.getMany(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords);
asyncReturn.post(() -> dataHandler.accept(data));
} catch (Exception e) {
Logger.e(LOG_TAG, "Error fetching words: " + e.getMessage());

View file

@ -93,7 +93,7 @@ public class CustomWordsImporter extends AbstractFileProcessor {
Timer.start(getClass().getSimpleName());
sendStart(resources.getString(R.string.dictionary_import_running));
if (isFileValid() && isThereRoomForMoreWords(activity) && insertWords(activity)) {
if (isFileValid() && isThereRoomForMoreWords(activity) && insertWords()) {
sendSuccess();
Logger.i(getClass().getSimpleName(), "Imported " + file.getName() + " in " + Timer.get(getClass().getSimpleName()) + " ms");
} else {
@ -114,7 +114,7 @@ public class CustomWordsImporter extends AbstractFileProcessor {
}
private boolean insertWords(Context context) {
private boolean insertWords() {
ReadOps readOps = new ReadOps();
int ignoredWords = 0;
int lineCount = 1;
@ -128,13 +128,13 @@ public class CustomWordsImporter extends AbstractFileProcessor {
return false;
}
CustomWord customWord = createCustomWord(context, line, lineCount);
CustomWord customWord = createCustomWord(line, lineCount);
if (customWord == null) {
sqlite.failTransaction();
return false;
}
if (readOps.exists(sqlite.getDb(), customWord.language, customWord.word)) {
if (customWord.language == null || customWord.language.isSyllabary() || readOps.exists(sqlite.getDb(), customWord.language, customWord.word)) {
ignoredWords++;
} else {
InsertOps.insertCustomWord(sqlite.getDb(), customWord.language, customWord.sequence, customWord.word);
@ -154,7 +154,10 @@ public class CustomWordsImporter extends AbstractFileProcessor {
}
if (ignoredWords > 0) {
Logger.i(getClass().getSimpleName(), "Skipped " + ignoredWords + " word(s) that are already in the dictionary.");
Logger.i(
getClass().getSimpleName(),
"Skipped " + ignoredWords + " word(s) that are already in the dictionary or do not belong to an alphabetic language."
);
}
return true;
@ -196,11 +199,11 @@ public class CustomWordsImporter extends AbstractFileProcessor {
}
private CustomWord createCustomWord(Context context, String line, int lineCount) {
private CustomWord createCustomWord(String line, int lineCount) {
try {
return new CustomWord(
CustomWordFile.getWord(line),
CustomWordFile.getLanguage(context, line)
CustomWordFile.getLanguage(line)
);
} catch (Exception e) {
String linePreview = line.length() > 50 ? line.substring(0, 50) + "..." : line;

View file

@ -1,7 +1,6 @@
package io.github.sspanak.tt9.db.entities;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
@ -65,7 +64,7 @@ public class CustomWordFile {
}
public static NaturalLanguage getLanguage(@NonNull Context context, String line) {
public static NaturalLanguage getLanguage(String line) {
if (line == null) {
return null;
}
@ -75,7 +74,7 @@ public class CustomWordFile {
return null;
}
return LanguageCollection.getLanguage(context, parts[1]);
return LanguageCollection.getLanguage(parts[1]);
}
@NonNull public static String getWord(String line) {

View file

@ -3,6 +3,8 @@ package io.github.sspanak.tt9.db.entities;
import android.content.Context;
import android.content.res.AssetManager;
import androidx.annotation.NonNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
@ -17,6 +19,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.AssetFile;
import io.github.sspanak.tt9.util.Logger;
@ -25,6 +28,7 @@ public class WordFile extends AssetFile {
private static final String LOG_TAG = WordFile.class.getSimpleName();
private final Context context;
private final boolean hasSyllables;
private int lastCharCode;
private BufferedReader reader;
@ -36,9 +40,10 @@ public class WordFile extends AssetFile {
private int sequences = -1;
public WordFile(Context context, String path, AssetManager assets) {
super(assets, path);
public WordFile(@NonNull Context context, Language language, AssetManager assets) {
super(assets, language != null ? language.getDictionaryFile() : "");
this.context = context;
hasSyllables = language != null && language.isSyllabary();
lastCharCode = 0;
reader = null;
@ -138,15 +143,6 @@ public class WordFile extends AssetFile {
}
public int getSequences() {
if (sequences < 0) {
loadProperties();
}
return sequences;
}
private void setSequences(String rawProperty, String rawValue) {
if (!rawProperty.equals("sequences")) {
return;
@ -278,7 +274,7 @@ public class WordFile extends AssetFile {
return words;
}
boolean areWordsSeparated = false;
boolean areWordsSeparated = hasSyllables; // if the language chars are syllables, there is no leading space to hint word separation
StringBuilder word = new StringBuilder();
// If the word string starts with a space, it means there are words longer than the sequence.

View file

@ -17,6 +17,7 @@ import io.github.sspanak.tt9.db.entities.WordList;
import io.github.sspanak.tt9.db.entities.WordPositionsStringBuilder;
import io.github.sspanak.tt9.db.wordPairs.WordPair;
import io.github.sspanak.tt9.db.words.SlowQueryStats;
import io.github.sspanak.tt9.db.words.WordStore;
import io.github.sspanak.tt9.languages.EmojiLanguage;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.util.Logger;
@ -127,6 +128,9 @@ public class ReadOps {
return new WordList();
}
// EXACT_MATCHES concerns only the positions query
filter = filter.equals(WordStore.FILTER_EXACT_MATCHES_ONLY) ? "" : filter;
String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput);
if (wordsQuery.isEmpty() || (cancel != null && cancel.isCanceled())) {
return new WordList();
@ -151,11 +155,17 @@ public class ReadOps {
public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) {
int generations = switch (sequence.length()) {
case 2 -> wordFilter.isEmpty() ? 1 : 10;
case 3, 4 -> wordFilter.isEmpty() ? 2 : 10;
default -> 10;
};
int generations;
if (wordFilter.equals(WordStore.FILTER_EXACT_MATCHES_ONLY)) {
generations = 0;
} else {
generations = switch (sequence.length()) {
case 2 -> wordFilter.isEmpty() ? 1 : 10;
case 3, 4 -> wordFilter.isEmpty() ? 2 : 10;
default -> 10;
};
}
return getWordPositions(db, cancel, language, sequence, generations, minPositions, wordFilter);
}
@ -215,26 +225,33 @@ public class ReadOps {
}
@NonNull private String getFactoryWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
/**
* Generates a query to search for positions in the dictionary words table. It supports sequences
* that start with a "0" (searches them as strings).
*/
@NonNull
private String getFactoryWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
StringBuilder sql = new StringBuilder("SELECT `start`, `end` FROM ")
.append(Tables.getWordPositions(language.getId()))
.append(" WHERE ");
if (generations >= 0 && generations < 10) {
sql.append(" sequence IN(").append(sequence);
sql.append(" sequence IN('").append(sequence);
int lastChild = (int)Math.pow(10, generations) - 1;
for (int seqEnd = 1; seqEnd <= lastChild; seqEnd++) {
if (seqEnd % 10 != 0) {
sql.append(",").append(sequence).append(seqEnd);
sql.append("','").append(sequence).append(seqEnd);
}
}
sql.append(")");
sql.append("')");
} else {
String rangeEnd = generations == 10 ? "9" : "999999";
sql.append(" sequence = ").append(sequence).append(" OR sequence BETWEEN ").append(sequence).append("1 AND ").append(sequence).append(rangeEnd);
sql.append(" sequence = '")
.append(sequence)
.append("' OR sequence BETWEEN '").append(sequence).append("1' AND '").append(sequence).append(rangeEnd).append("'");
sql.append(" ORDER BY `start` ");
sql.append(" LIMIT 100");
}
@ -245,7 +262,12 @@ public class ReadOps {
}
@NonNull private String getCustomWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
/**
* Generates a query to search for custom word positions. This does NOT support sequences that
* start with a "0" (searches them as integers).
*/
@NonNull
private String getCustomWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
String sql = "SELECT -id as `start`, -id as `end` FROM " + Tables.CUSTOM_WORDS +
" WHERE langId = " + language.getId() +
" AND (sequence = " + sequence;

View file

@ -24,7 +24,7 @@ public class SQLiteOpener extends SQLiteOpenHelper {
private SQLiteOpener(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
allLanguages = new ArrayList<>(LanguageCollection.getAll(context));
allLanguages = new ArrayList<>(LanguageCollection.getAll());
allLanguages.add(new EmojiLanguage());
}

View file

@ -137,6 +137,11 @@ public class WordPairStore extends BaseSyncStore {
int totalPairs = 0;
for (Language language : languages) {
if (language.isSyllabary()) {
Logger.d(LOG_TAG, "Not loading word pairs for syllabary language: " + language.getId());
continue;
}
HashMap<WordPair, WordPair> wordPairs = pairs.get(language.getId());
if (wordPairs == null) {
wordPairs = new HashMap<>();

View file

@ -23,6 +23,7 @@ import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
import io.github.sspanak.tt9.db.sqlite.Tables;
import io.github.sspanak.tt9.languages.EmojiLanguage;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageException;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
@ -108,7 +109,7 @@ public class DictionaryLoader {
public static void load(Context context, Language language) {
DictionaryLoadingBar progressBar = DictionaryLoadingBar.getInstance(context);
getInstance(context).setOnStatusChange(status -> progressBar.show(context, status));
getInstance(context).setOnStatusChange(progressBar::show);
self.load(context, new ArrayList<>() {{ add(language); }});
}
@ -133,7 +134,7 @@ public class DictionaryLoader {
load(context, language);
}
// or if the database is outdated, compared to the dictionary file, ask for confirmation and load
else if (!hash.equals(new WordFile(context, language.getDictionaryFile(), self.assets).getHash())) {
else if (!hash.equals(new WordFile(context, language, self.assets).getHash())) {
new DictionaryUpdateNotification(context, language).show();
}
},
@ -235,8 +236,12 @@ public class DictionaryLoader {
private int importLetters(Language language) throws InvalidLanguageCharactersException {
if (language.isSyllabary()) {
return 0;
}
int lettersCount = 0;
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
boolean isEnglish = LanguageKind.isEnglish(language);
WordBatch letters = new WordBatch(language);
for (int key = 2; key <= 9; key++) {
@ -254,7 +259,7 @@ public class DictionaryLoader {
private void importWordFile(Context context, Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
WordFile wordFile = new WordFile(context, language.getDictionaryFile(), assets);
WordFile wordFile = new WordFile(context, language, assets);
WordBatch batch = new WordBatch(language, SettingsStore.DICTIONARY_IMPORT_BATCH_SIZE + 1);
float progressRatio = (maxProgress - minProgress) / wordFile.getWords();
int wordCount = 0;

View file

@ -26,6 +26,7 @@ import io.github.sspanak.tt9.util.Timer;
public class WordStore extends BaseSyncStore {
public static final String FILTER_EXACT_MATCHES_ONLY = "__exact__";
private final String LOG_TAG = "sqlite.WordStore";
private final ReadOps readOps;
@ -59,9 +60,10 @@ public class WordStore extends BaseSyncStore {
/**
* Loads words matching and similar to a given digit sequence
* For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ...
* and other similar.
* and other similar. When "wordFilter" is set to FILTER_EXACT_MATCHES_ONLY, the word list is
* constrained only to the words with length equal to the digit sequence length (exact matches).
*/
public ArrayList<String> getSimilar(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) {
public ArrayList<String> getMany(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) {
if (!checkOrNotify()) {
return new ArrayList<>();
}
@ -89,7 +91,7 @@ public class WordStore extends BaseSyncStore {
long wordsTime = Timer.stop("get_words");
printLoadingSummary(sequence, words, positionsTime, wordsTime);
if (!cancel.isCanceled()) { // do not store empty results from aborted queries in the cache
if (!cancel.isCanceled()) { // do not cache empty results from aborted queries
SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions);
}

View file

@ -18,7 +18,7 @@ public class DeviceInfo {
return context.getResources().getDisplayMetrics().heightPixels;
}
public static boolean noBackspaceKey(Context context) {
public static boolean noBackspaceKey() {
return !KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_DEL) && !KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_CLEAR);
}

View file

@ -6,6 +6,7 @@ import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DataStore;
import io.github.sspanak.tt9.db.words.DictionaryLoader;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.ui.UI;
import io.github.sspanak.tt9.ui.dialogs.AddWordDialog;
@ -88,6 +89,11 @@ abstract public class CommandHandler extends TextEditingHandler {
return;
}
if (mLanguage.isSyllabary()) {
UI.toastShortSingle(this, R.string.function_add_word_not_available);
return;
}
if (DictionaryLoader.getInstance(this).isRunning()) {
UI.toastShortSingle(this, R.string.dictionary_loading_please_wait);
return;
@ -134,10 +140,10 @@ abstract public class CommandHandler extends TextEditingHandler {
protected void nextInputMode() {
if (mInputMode.isPassthrough() || voiceInputOps.isListening()) {
if (InputModeKind.isPassthrough(mInputMode) || voiceInputOps.isListening()) {
return;
} else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) {
mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, textField, InputMode.MODE_123) : mInputMode;
mInputMode = !InputModeKind.is123(mInputMode) ? InputMode.getInstance(settings, mLanguage, inputType, textField, InputMode.MODE_123) : mInputMode;
} else {
suggestionOps.cancelDelayedAccept();
mInputMode.onAcceptSuggestion(suggestionOps.acceptIncomplete());
@ -159,30 +165,34 @@ abstract public class CommandHandler extends TextEditingHandler {
// select the next language
int previous = mEnabledLanguages.indexOf(mLanguage.getId());
int next = (previous + 1) % mEnabledLanguages.size();
mLanguage = LanguageCollection.getLanguage(getApplicationContext(), mEnabledLanguages.get(next));
mLanguage = LanguageCollection.getLanguage(mEnabledLanguages.get(next));
// validate and save it for the next time
validateLanguages();
}
protected void nextTextCase() {
protected boolean nextTextCase() {
if (suggestionOps.isEmpty() || mInputMode.getSuggestions().isEmpty()) {
// When there are no suggestions, there is no need to execute the code for
// adjusting them below.
if (mInputMode.nextTextCase()) {
settings.saveTextCase(mInputMode.getTextCase());
return true;
} else {
return false;
}
return;
}
// When we are in AUTO mode and current dictionary word is in uppercase,
// the mode would switch to UPPERCASE, but visually, the word would not change.
// This is why we retry, until there is a visual change.
boolean isChanged = false;
String before = suggestionOps.get(0);
for (int retries = 0; retries < 2 && mInputMode.nextTextCase(); retries++) {
String after = mInputMode.getSuggestions().get(0);
if (!after.equals(before)) {
isChanged = true;
break;
}
}
@ -201,6 +211,8 @@ abstract public class CommandHandler extends TextEditingHandler {
textField.setComposingText(suggestionOps.getCurrent());
settings.saveTextCase(mInputMode.getTextCase());
return isChanged;
}

View file

@ -2,9 +2,11 @@ package io.github.sspanak.tt9.ime;
import android.view.KeyEvent;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.words.DictionaryLoader;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.modes.ModePredictive;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
import io.github.sspanak.tt9.ui.UI;
import io.github.sspanak.tt9.util.Ternary;
@ -103,7 +105,15 @@ public abstract class HotkeyHandler extends CommandHandler {
}
if (keyCode == settings.getKeyShift()) {
return onKeyNextTextCase(validateOnly);
return
onKeyNextTextCase(validateOnly)
// when "Shift" and "Korean Space" share the same key, allow typing a space, when there
// are no special characters to shift
|| (keyCode == settings.getKeySpaceKorean() && onKeySpaceKorean(validateOnly));
}
if (keyCode == settings.getKeySpaceKorean()) {
return onText(" ", validateOnly);
}
if (keyCode == settings.getKeyShowSettings()) {
@ -176,7 +186,7 @@ public abstract class HotkeyHandler extends CommandHandler {
public boolean onKeyFilterClear(boolean validateOnly) {
if (suggestionOps.isEmpty()) {
if (suggestionOps.isEmpty() || mLanguage.isSyllabary()) {
return false;
}
@ -205,6 +215,11 @@ public abstract class HotkeyHandler extends CommandHandler {
return false;
}
if (mLanguage.isSyllabary()) {
UI.toastShortSingle(this, R.string.function_filter_suggestions_not_available);
return true; // prevent the default key action to acknowledge we have processed the event
}
if (validateOnly) {
return true;
}
@ -247,7 +262,7 @@ public abstract class HotkeyHandler extends CommandHandler {
public boolean onKeyNextLanguage(boolean validateOnly) {
if (mInputMode.isNumeric() || mEnabledLanguages.size() < 2) {
if (InputModeKind.isNumeric(mInputMode) || mEnabledLanguages.size() < 2) {
return false;
}
@ -257,17 +272,21 @@ public abstract class HotkeyHandler extends CommandHandler {
suggestionOps.cancelDelayedAccept();
nextLang();
mInputMode.changeLanguage(mLanguage);
mInputMode.clearWordStem();
getSuggestions();
// for languages that do not have ABC or Predictive, make sure we remain in valid state
if (!mInputMode.changeLanguage(mLanguage)) {
mInputMode = InputMode.getInstance(settings, mLanguage, inputType, textField, determineInputModeId());
}
mInputMode.clearWordStem();
getSuggestions();
statusBar.setText(mInputMode);
mainView.render();
if (!suggestionOps.isEmpty() || settings.isMainLayoutStealth()) {
UI.toastShortSingle(this, mInputMode.getClass().getSimpleName(), mInputMode.toString());
}
if (mInputMode instanceof ModePredictive) {
if (InputModeKind.isPredictive(mInputMode)) {
DictionaryLoader.autoLoad(this, mLanguage);
}
@ -309,7 +328,9 @@ public abstract class HotkeyHandler extends CommandHandler {
}
suggestionOps.scheduleDelayedAccept(mInputMode.getAutoAcceptTimeout()); // restart the timer
nextTextCase();
if (!nextTextCase()) {
return false;
}
statusBar.setText(mInputMode);
mainView.render();
@ -333,6 +354,7 @@ public abstract class HotkeyHandler extends CommandHandler {
return true;
}
private boolean onKeyShowSettings(boolean validateOnly) {
if (!isInputViewShown() || shouldBeOff()) {
return false;
@ -345,6 +367,26 @@ public abstract class HotkeyHandler extends CommandHandler {
return true;
}
public boolean onKeySpaceKorean(boolean validateOnly) {
if (shouldBeOff() || !InputModeKind.isCheonjiin(mInputMode)) {
return false;
}
// type a space when there is nothing to accept
if (suggestionOps.isEmpty() && !onText(" ", validateOnly)) {
return false;
}
// simulate accept with OK when there are suggestions
if (!suggestionOps.isEmpty()) {
onAcceptSuggestionManually(suggestionOps.acceptCurrent(), KeyEvent.KEYCODE_ENTER);
}
return true;
}
private boolean onKeyVoiceInput(boolean validateOnly) {
if (!isInputViewShown() || shouldBeOff() || !voiceInputOps.isAvailable()) {
return false;

View file

@ -1,9 +1,10 @@
package io.github.sspanak.tt9.ime;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.sspanak.tt9.ime.helpers.OrientationListener;
import io.github.sspanak.tt9.ime.modes.ModeABC;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.ime.voice.VoiceInputOps;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
@ -35,32 +36,28 @@ abstract public class MainViewHandler extends HotkeyHandler {
}
}
public int getTextCase() {
return mInputMode.getTextCase();
}
public boolean isInputLimited() {
return inputType.isLimited();
}
public boolean isInputModeABC() {
return mInputMode.getClass().equals(ModeABC.class);
return InputModeKind.isABC(mInputMode);
}
public boolean isInputModeNumeric() {
return mInputMode.is123();
return InputModeKind.isNumeric(mInputMode);
}
public boolean isNumericModeStrict() {
return mInputMode.is123() && inputType.isNumeric() && !inputType.isPhoneNumber();
return InputModeKind.is123(mInputMode) && inputType.isNumeric() && !inputType.isPhoneNumber();
}
public boolean isNumericModeSigned() {
return mInputMode.is123() && inputType.isSignedNumber();
return InputModeKind.is123(mInputMode) && inputType.isSignedNumber();
}
public boolean isInputModePhone() {
return mInputMode.is123() && inputType.isPhoneNumber();
return InputModeKind.is123(mInputMode) && inputType.isPhoneNumber();
}
public boolean isTextEditingActive() {
@ -75,6 +72,29 @@ abstract public class MainViewHandler extends HotkeyHandler {
return !(new VoiceInputOps(this, null, null, null)).isAvailable();
}
public boolean notLanguageSyllabary() {
return mLanguage == null || !mLanguage.isSyllabary();
}
public String getABCString() {
return mLanguage == null || mLanguage.isSyllabary() ? "ABC" : mLanguage.getAbcString().toUpperCase(mLanguage.getLocale());
}
@NonNull
public String getInputModeName() {
if (InputModeKind.isPredictive(mInputMode)) {
return "T9";
} else if (InputModeKind.isNumeric(mInputMode)){
return "123";
} else {
return getABCString();
}
}
public int getTextCase() {
return mInputMode.getTextCase();
}
@Nullable
public Language getLanguage() {
return mLanguage;

View file

@ -3,6 +3,7 @@ package io.github.sspanak.tt9.ime;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.util.Clipboard;
@ -14,7 +15,7 @@ abstract public class TextEditingHandler extends VoiceHandler {
@Override
protected boolean onStart(InputConnection connection, EditorInfo field) {
isSystemRTL = LanguageKind.isRTL(LanguageCollection.getDefault(this));
isSystemRTL = LanguageKind.isRTL(LanguageCollection.getDefault());
return super.onStart(connection, field);
}
@ -42,7 +43,7 @@ abstract public class TextEditingHandler extends VoiceHandler {
private void onCommand(int key) {
switch (key) {
case 0:
if (!mInputMode.isNumeric()) {
if (!InputModeKind.isNumeric(mInputMode)) {
onText(" ", false);
}
break;

View file

@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
import io.github.sspanak.tt9.db.DataStore;
import io.github.sspanak.tt9.db.words.DictionaryLoader;
import io.github.sspanak.tt9.hacks.InputType;
import io.github.sspanak.tt9.ime.modes.ModePredictive;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.ui.UI;
@ -119,6 +119,7 @@ public class TraditionalT9 extends MainViewHandler {
isDead = false;
settings.setDemoMode(false);
Logger.setLevel(settings.getLogLevel());
LanguageCollection.init(this);
DataStore.init(this);
super.onInit();
}
@ -137,7 +138,7 @@ public class TraditionalT9 extends MainViewHandler {
Logger.setLevel(settings.getLogLevel());
if (mInputMode.isPassthrough()) {
if (InputModeKind.isPassthrough(mInputMode)) {
onStop();
} else {
backgroundTasks.removeCallbacksAndMessages(null);
@ -147,7 +148,7 @@ public class TraditionalT9 extends MainViewHandler {
InputType newInputType = new InputType(connection, field);
if (newInputType.isText()) {
DataStore.loadWordPairs(DictionaryLoader.getInstance(this), LanguageCollection.getAll(this, settings.getEnabledLanguageIds()));
DataStore.loadWordPairs(DictionaryLoader.getInstance(this), LanguageCollection.getAll(settings.getEnabledLanguageIds()));
}
if (newInputType.isNotUs(this)) {
@ -238,7 +239,7 @@ public class TraditionalT9 extends MainViewHandler {
@Override
protected boolean onNumber(int key, boolean hold, int repeat) {
if (mInputMode instanceof ModePredictive && DictionaryLoader.autoLoad(this, mLanguage)) {
if (InputModeKind.isPredictive(mInputMode) && DictionaryLoader.autoLoad(this, mLanguage)) {
return true;
}
return super.onNumber(key, hold, repeat);

View file

@ -17,9 +17,10 @@ import io.github.sspanak.tt9.ime.helpers.SuggestionOps;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.helpers.TextSelection;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.ime.modes.ModePredictive;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.ui.UI;
import io.github.sspanak.tt9.util.Text;
@ -48,7 +49,7 @@ public abstract class TypingHandler extends KeyPadHandler {
protected boolean shouldBeOff() {
return getCurrentInputConnection() == null || mInputMode.isPassthrough();
return getCurrentInputConnection() == null || InputModeKind.isPassthrough(mInputMode);
}
@Override
@ -93,8 +94,8 @@ public abstract class TypingHandler extends KeyPadHandler {
protected void validateLanguages() {
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(getApplicationContext(), mEnabledLanguages);
mLanguage = InputModeValidator.validateLanguage(getApplicationContext(), mLanguage, mEnabledLanguages);
mEnabledLanguages = InputModeValidator.validateEnabledLanguages(mEnabledLanguages);
mLanguage = InputModeValidator.validateLanguage(mLanguage, mEnabledLanguages);
settings.saveInputLanguage(mLanguage.getId());
settings.saveEnabledLanguageIds(mEnabledLanguages);
}
@ -111,7 +112,7 @@ public abstract class TypingHandler extends KeyPadHandler {
public boolean onBackspace(int repeat) {
// Dialer fields seem to handle backspace on their own and we must ignore it,
// otherwise, keyDown race condition occur for all keys.
if (mInputMode.isPassthrough()) {
if (InputModeKind.isPassthrough(mInputMode)) {
return false;
}
@ -172,11 +173,17 @@ public abstract class TypingHandler extends KeyPadHandler {
protected boolean onNumber(int key, boolean hold, int repeat) {
suggestionOps.cancelDelayedAccept();
// 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.
if (mInputMode.shouldReplaceLastLetter(key, hold)) {
mInputMode.replaceLastLetter();
}
// Automatically accept the previous word, when the next one is a space or punctuation,
// instead of requiring "OK" before that.
// First pass, analyze the incoming key press and decide whether it could be the start of
// a new word.
if (mInputMode.shouldAcceptPreviousSuggestion(key, hold)) {
else if (mInputMode.shouldAcceptPreviousSuggestion(key, hold)) {
String lastWord = suggestionOps.acceptIncomplete();
mInputMode.onAcceptSuggestion(lastWord);
autoCorrectSpace(lastWord, false, key);
@ -230,15 +237,15 @@ public abstract class TypingHandler extends KeyPadHandler {
private void autoCorrectSpace(String currentWord, boolean isWordAcceptedManually, int nextKey) {
if (!inputType.isRustDesk() && mInputMode.shouldDeletePrecedingSpace(inputType, textField)) {
if (!inputType.isRustDesk() && mInputMode.shouldDeletePrecedingSpace()) {
textField.deletePrecedingSpace(currentWord);
}
if (mInputMode.shouldAddPrecedingSpace(inputType, textField)) {
if (mInputMode.shouldAddPrecedingSpace()) {
textField.addPrecedingSpace(currentWord);
}
if (mInputMode.shouldAddTrailingSpace(inputType, textField, isWordAcceptedManually, nextKey)) {
if (mInputMode.shouldAddTrailingSpace(isWordAcceptedManually, nextKey)) {
textField.setText(" ");
}
}
@ -253,10 +260,10 @@ public abstract class TypingHandler extends KeyPadHandler {
mEnabledLanguages = settings.getEnabledLanguageIds();
int oldLang = mLanguage != null ? mLanguage.getId() : -1;
mLanguage = LanguageCollection.getLanguage(getApplicationContext(), settings.getInputLanguage());
mLanguage = LanguageCollection.getLanguage(settings.getInputLanguage());
validateLanguages();
Language appLanguage = textField.getLanguage(getApplicationContext(), mEnabledLanguages);
Language appLanguage = textField.getLanguage(mEnabledLanguages);
if (appLanguage != null) {
mLanguage = appLanguage;
}
@ -270,7 +277,6 @@ public abstract class TypingHandler extends KeyPadHandler {
* Restore the last used text case or auto-select a new one based on the input field properties.
*/
protected void determineTextCase() {
mInputMode.setTextFieldCase(inputType.determineTextCase());
InputModeValidator.validateTextCase(mInputMode, settings.getTextCase());
}
@ -287,7 +293,11 @@ public abstract class TypingHandler extends KeyPadHandler {
return InputMode.MODE_PASSTHROUGH;
}
allowedInputModes = inputType.determineInputModes(this);
allowedInputModes = new ArrayList<>(inputType.determineInputModes(getApplicationContext()));
if (LanguageKind.isKorean(mLanguage) && allowedInputModes.contains(InputMode.MODE_ABC)) {
allowedInputModes.remove(InputMode.MODE_ABC);
}
return InputModeValidator.validateMode(settings.getInputMode(), allowedInputModes);
}
@ -351,7 +361,7 @@ public abstract class TypingHandler extends KeyPadHandler {
}
protected void getSuggestions() {
if (mInputMode instanceof ModePredictive && DictionaryLoader.getInstance(this).isRunning()) {
if (InputModeKind.isPredictive(mInputMode) && DictionaryLoader.getInstance(this).isRunning()) {
mInputMode.reset();
UI.toastShortSingle(this, R.string.dictionary_loading_please_wait);
} else {
@ -378,7 +388,7 @@ public abstract class TypingHandler extends KeyPadHandler {
// but there are no words for the new language, we'll get only generated suggestions, consisting
// of the last word of the previous language + endings from the new language. These words are invalid,
// so we discard them.
if (mInputMode instanceof ModePredictive && !mLanguage.isValidWord(suggestionOps.getCurrent()) && !Text.isGraphic(suggestionOps.getCurrent())) {
if (InputModeKind.isPredictive(mInputMode) && !mLanguage.isValidWord(suggestionOps.getCurrent()) && !Text.isGraphic(suggestionOps.getCurrent())) {
mInputMode.reset();
suggestionOps.set(null);
}

View file

@ -6,6 +6,7 @@ import android.view.inputmethod.InputMethodManager;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.hacks.DeviceInfo;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.ime.modes.InputModeKind;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.ui.main.ResizableMainView;
import io.github.sspanak.tt9.ui.tray.StatusBar;
@ -56,7 +57,7 @@ abstract class UiHandler extends AbstractHandler {
protected void setStatusIcon(InputMode mode) {
if (!mode.isPassthrough() && settings.isStatusIconEnabled()) {
if (!InputModeKind.isPassthrough(mode) && settings.isStatusIconEnabled()) {
showStatusIcon(R.drawable.ic_status);
} else {
hideStatusIcon();

View file

@ -1,6 +1,5 @@
package io.github.sspanak.tt9.ime.helpers;
import android.content.Context;
import android.os.Build;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@ -85,13 +84,13 @@ public class InputField {
* it's a text field where the language doesn't matter, the function returns null.
*/
@Nullable
public Language getLanguage(Context context, ArrayList<Integer> allowedLanguageIds) {
public Language getLanguage(ArrayList<Integer> allowedLanguageIds) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return null;
}
for (int i = 0; field.hintLocales != null && i < field.hintLocales.size(); i++) {
Language lang = LanguageCollection.getByLanguageCode(context, field.hintLocales.get(i).getLanguage());
Language lang = LanguageCollection.getByLanguageCode(field.hintLocales.get(i).getLanguage());
if (lang != null && allowedLanguageIds.contains(lang.getId())) {
return lang;
}

View file

@ -1,38 +1,36 @@
package io.github.sspanak.tt9.ime.helpers;
import android.content.Context;
import java.util.ArrayList;
import io.github.sspanak.tt9.util.Logger;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.util.Logger;
public class InputModeValidator {
public static ArrayList<Integer> validateEnabledLanguages(Context context, ArrayList<Integer> enabledLanguageIds) {
ArrayList<Language> validLanguages = LanguageCollection.getAll(context, enabledLanguageIds);
public static ArrayList<Integer> validateEnabledLanguages(ArrayList<Integer> enabledLanguageIds) {
ArrayList<Language> validLanguages = LanguageCollection.getAll(enabledLanguageIds);
ArrayList<Integer> validLanguageIds = new ArrayList<>();
for (Language lang : validLanguages) {
validLanguageIds.add(lang.getId());
}
if (validLanguageIds.isEmpty()) {
validLanguageIds.add(LanguageCollection.getDefault(context).getId());
validLanguageIds.add(LanguageCollection.getDefault().getId());
Logger.e("validateEnabledLanguages", "The language list seems to be corrupted. Resetting to first language only.");
}
return validLanguageIds;
}
public static Language validateLanguage(Context context, Language language, ArrayList<Integer> validLanguageIds) {
public static Language validateLanguage(Language language, ArrayList<Integer> validLanguageIds) {
if (language != null && validLanguageIds.contains(language.getId())) {
return language;
}
String error = language != null ? "Language: " + language.getId() + " is not enabled." : "Invalid language.";
Language validLanguage = LanguageCollection.getLanguage(context, validLanguageIds.get(0));
validLanguage = validLanguage != null ? validLanguage : LanguageCollection.getDefault(context);
Language validLanguage = LanguageCollection.getLanguage(validLanguageIds.get(0));
validLanguage = validLanguage != null ? validLanguage : LanguageCollection.getDefault();
Logger.d("validateLanguage", error + " Enforcing language: " + validLanguage.getId());

View file

@ -5,7 +5,6 @@ import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@ -130,14 +129,14 @@ abstract public class StandardInputType {
* determineInputModes
* Determine the typing mode based on the input field being edited. Returns an ArrayList of the allowed modes.
*
* @return ArrayList<SettingsStore.MODE_ABC | SettingsStore.MODE_123 | SettingsStore.MODE_PREDICTIVE>
* @return Set<InputMode.MODE_PASSTHROUGH | InputMode.MODE_ABC | InputMode.MODE_123 | InputMode.MODE_PREDICTIVE>
*/
public ArrayList<Integer> determineInputModes(Context context) {
public Set<Integer> determineInputModes(Context context) {
Set<Integer> allowedModes = new HashSet<>();
if (field == null) {
allowedModes.add(InputMode.MODE_PASSTHROUGH);
return new ArrayList<>(allowedModes);
return allowedModes;
}
// Calculators (only 0-9 and math) and Dialer (0-9, "#" and "*") fields
@ -145,7 +144,7 @@ abstract public class StandardInputType {
// Note: A Dialer field is not a Phone number field.
if (isSpecialNumeric(context)) {
allowedModes.add(InputMode.MODE_PASSTHROUGH);
return new ArrayList<>(allowedModes);
return allowedModes;
}
switch (field.inputType & InputType.TYPE_MASK_CLASS) {
@ -155,7 +154,7 @@ abstract public class StandardInputType {
// Numbers, dates and phone numbers default to the numeric keyboard,
// with no extra features.
allowedModes.add(InputMode.MODE_123);
return new ArrayList<>(allowedModes);
return allowedModes;
case InputType.TYPE_CLASS_TEXT:
// This is general text editing. We will default to the
@ -179,7 +178,7 @@ abstract public class StandardInputType {
allowedModes.add(InputMode.MODE_123);
allowedModes.add(InputMode.MODE_ABC);
return new ArrayList<>(allowedModes);
return allowedModes;
}
}

View file

@ -8,6 +8,7 @@ import java.util.ArrayList;
import io.github.sspanak.tt9.hacks.InputType;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.languages.NaturalLanguage;
import io.github.sspanak.tt9.languages.NullLanguage;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
@ -28,11 +29,11 @@ abstract public class InputMode {
public static final int CASE_DICTIONARY = 3; // do not force it, but use the dictionary word as-is
protected final ArrayList<Integer> allowedTextCases = new ArrayList<>();
protected int textCase = CASE_LOWER;
protected int textFieldTextCase = CASE_UNDEFINED;
// data
protected int autoAcceptTimeout = -1;
@NonNull protected String digitSequence = "";
protected final boolean isEmailMode;
@NonNull protected Language language = new NullLanguage();
protected final SettingsStore settings;
@NonNull protected final ArrayList<String> suggestions = new ArrayList<>();
@ -40,7 +41,8 @@ abstract public class InputMode {
protected int specialCharSelectedGroup = 0;
protected InputMode(SettingsStore settings) {
protected InputMode(SettingsStore settings, InputType inputType) {
isEmailMode = inputType != null && inputType.isEmail();
this.settings = settings;
}
@ -48,15 +50,15 @@ abstract public class InputMode {
public static InputMode getInstance(SettingsStore settings, @Nullable Language language, InputType inputType, TextField textField, int mode) {
switch (mode) {
case MODE_PREDICTIVE:
return new ModePredictive(settings, inputType, textField, language);
return (LanguageKind.isKorean(language) ? new ModeCheonjiin(settings, inputType, textField) : new ModeWords(settings, language, inputType, textField));
case MODE_ABC:
return new ModeABC(settings, inputType, language);
return new ModeABC(settings, language, inputType);
case MODE_PASSTHROUGH:
return new ModePassthrough(settings);
return new ModePassthrough(settings, inputType);
default:
Logger.w("InputMode", "Defaulting to mode: " + Mode123.class.getName() + " for unknown InputMode: " + mode);
case MODE_123:
return new Mode123(settings, inputType, language);
return new Mode123(settings, language, inputType);
}
}
@ -92,11 +94,6 @@ abstract public class InputMode {
return this;
}
// Numeric mode identifiers. "instanceof" cannot be used in all cases, because they inherit each other.
public boolean is123() { return false; }
public boolean isPassthrough() { return false; }
public boolean isNumeric() { return false; }
// Utility
abstract public int getId();
public boolean containsGeneratedSuggestions() { return false; }
@ -105,19 +102,36 @@ abstract public class InputMode {
public int getAutoAcceptTimeout() {
return autoAcceptTimeout;
}
public void changeLanguage(@Nullable Language newLanguage) {
/**
* Switches to a new language if the input mode supports it. If the InputMode return "false",
* it does not support that language, so you must obtain a compatible alternative using the
* getInstance() method and the same ID.
* The default implementation is to switch to the new language (including NullLanguage) and
* return "true".
*/
public boolean changeLanguage(@Nullable Language newLanguage) {
setLanguage(newLanguage);
return true;
}
protected void setLanguage(@Nullable Language newLanguage) {
language = newLanguage != null ? newLanguage : new NullLanguage();
}
// Interaction with the IME. Return "true" if it should perform the respective action.
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) { return false; }
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { return false; }
public boolean shouldAddTrailingSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int nextKey) { return false; }
public boolean shouldAddPrecedingSpace(InputType inputType, TextField textField) { return false; }
public boolean shouldDeletePrecedingSpace(InputType inputType, TextField textField) { return false; }
public boolean shouldAddTrailingSpace(boolean isWordAcceptedManually, int nextKey) { return false; }
public boolean shouldAddPrecedingSpace() { return false; }
public boolean shouldDeletePrecedingSpace() { return false; }
public boolean shouldIgnoreText(String text) { return text == null || text.isEmpty(); }
public boolean shouldReplaceLastLetter(int nextKey, boolean hold) { return false; }
public boolean shouldSelectNextSuggestion() { return false; }
public boolean recompose(String word) { return false; }
public void replaceLastLetter() {}
public void reset() {
autoAcceptTimeout = -1;
@ -137,10 +151,6 @@ abstract public class InputMode {
return true;
}
public void setTextFieldCase(int newTextCase) {
textFieldTextCase = allowedTextCases.contains(newTextCase) ? newTextCase : CASE_UNDEFINED;
}
public void defaultTextCase() {
textCase = allowedTextCases.get(0);
}
@ -166,6 +176,11 @@ abstract public class InputMode {
protected String adjustSuggestionTextCase(String word, int newTextCase) { return word; }
protected boolean shouldSelectNextSpecialCharacters() {
return !digitSequence.isEmpty();
}
/**
* This is used in nextTextCase() for switching to the next set of characters. Obviously,
* special chars do not have a text case, but we use this trick to alternate the char groups.
@ -175,16 +190,13 @@ abstract public class InputMode {
specialCharSelectedGroup++;
return
loadSpecialCharacters() // validates specialCharSelectedGroup
shouldSelectNextSpecialCharacters() // check if the operation makes sense at all
&& loadSpecialCharacters() // validates specialCharSelectedGroup and advances, if possible
&& previousGroup != specialCharSelectedGroup; // verifies validation has passed
}
protected boolean loadSpecialCharacters() {
if (digitSequence.isEmpty()) {
return false;
}
int key = digitSequence.charAt(0) - '0';
ArrayList<String> chars = settings.getOrderedKeyChars(language, key, specialCharSelectedGroup);

View file

@ -0,0 +1,27 @@
package io.github.sspanak.tt9.ime.modes;
public class InputModeKind {
public static boolean isPassthrough(InputMode mode) {
return mode != null && mode.getId() == InputMode.MODE_PASSTHROUGH;
}
public static boolean is123(InputMode mode) {
return mode != null && mode.getId() == InputMode.MODE_123;
}
public static boolean isNumeric(InputMode mode) {
return isPassthrough(mode) || is123(mode);
}
public static boolean isABC(InputMode mode) {
return mode != null && mode.getId() == InputMode.MODE_ABC;
}
public static boolean isPredictive(InputMode mode) {
return mode != null && mode.getId() == InputMode.MODE_PREDICTIVE;
}
public static boolean isCheonjiin(InputMode mode) {
return mode != null && mode.getClass().equals(ModeCheonjiin.class);
}
}

View file

@ -10,25 +10,20 @@ import io.github.sspanak.tt9.languages.NaturalLanguage;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Characters;
public class Mode123 extends ModePassthrough {
class Mode123 extends ModePassthrough {
@Override public int getId() { return MODE_123; }
@Override @NonNull public String toString() { return "123"; }
@Override public final boolean is123() { return true; }
@Override public boolean isPassthrough() { return false; }
@Override public int getSequenceLength() { return digitSequence.length(); }
@Override public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) { return true; }
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
private final boolean isEmailMode;
public Mode123(SettingsStore settings, InputType inputType, Language language) {
super(settings);
protected Mode123(SettingsStore settings, Language language, InputType inputType) {
super(settings, inputType);
changeLanguage(language);
isEmailMode = inputType.isEmail();
if (inputType.isPhoneNumber()) {
setSpecificSpecialCharacters(Characters.Phone, false);
} else if (inputType.isNumeric()) {
@ -59,8 +54,14 @@ public class Mode123 extends ModePassthrough {
}
@Override
protected boolean shouldSelectNextSpecialCharacters() {
return !isEmailMode && digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY);
}
@Override protected boolean nextSpecialCharacters() {
if (isEmailMode || !digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY) || !super.nextSpecialCharacters()) {
if (!super.nextSpecialCharacters()) {
return false;
}

View file

@ -12,18 +12,18 @@ import io.github.sspanak.tt9.languages.NaturalLanguage;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Characters;
public class ModeABC extends InputMode {
class ModeABC extends InputMode {
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
private boolean shouldSelectNextLetter = false;
@Override public int getId() { return MODE_ABC; }
ModeABC(SettingsStore settings, InputType inputType, Language lang) {
super(settings);
protected ModeABC(SettingsStore settings, Language lang, InputType inputType) {
super(settings, inputType);
changeLanguage(lang);
if (inputType.isEmail()) {
if (isEmailMode) {
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(0), 0));
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(1), 1));
}
@ -76,18 +76,27 @@ public class ModeABC extends InputMode {
}
@Override
protected boolean nextSpecialCharacters() {
if (KEY_CHARACTERS.isEmpty() && digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY) && super.nextSpecialCharacters()) {
suggestions.add(language.getKeyNumber(digitSequence.charAt(0) - '0'));
return true;
}
return false;
protected boolean shouldSelectNextSpecialCharacters() {
return KEY_CHARACTERS.isEmpty() && digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY);
}
@Override
public void changeLanguage(@Nullable Language newLanguage) {
super.changeLanguage(newLanguage);
protected boolean nextSpecialCharacters() {
if (!super.nextSpecialCharacters()) {
return false;
}
suggestions.add(language.getKeyNumber(digitSequence.charAt(0) - '0'));
return true;
}
@Override
public boolean changeLanguage(@Nullable Language newLanguage) {
if (newLanguage != null && newLanguage.isSyllabary()) {
return false;
}
setLanguage(newLanguage);
allowedTextCases.clear();
allowedTextCases.add(CASE_LOWER);
@ -97,6 +106,8 @@ public class ModeABC extends InputMode {
refreshSuggestions();
shouldSelectNextLetter = true; // do not accept any previous suggestions after loading the new ones
return true;
}
@Override public void onAcceptSuggestion(@NonNull String w) { reset(); }

View file

@ -0,0 +1,407 @@
package io.github.sspanak.tt9.ime.modes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import io.github.sspanak.tt9.hacks.InputType;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
import io.github.sspanak.tt9.ime.modes.helpers.Cheonjiin;
import io.github.sspanak.tt9.ime.modes.predictions.Predictions;
import io.github.sspanak.tt9.ime.modes.predictions.SyllablePredictions;
import io.github.sspanak.tt9.languages.EmojiLanguage;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.languages.NaturalLanguage;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Characters;
class ModeCheonjiin extends InputMode {
// used when we want do display a different set of characters for a given key, for example
// in email fields
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
// special chars and emojis
private static String SPECIAL_CHAR_SEQUENCE_PREFIX;
protected String CUSTOM_EMOJI_SEQUENCE;
protected String EMOJI_SEQUENCE;
protected String PUNCTUATION_SEQUENCE;
protected String SPECIAL_CHAR_SEQUENCE;
// predictions
protected boolean disablePredictions = false;
protected Predictions predictions;
@NonNull private String previousJamoSequence = "";
// text analysis
protected final AutoSpace autoSpace;
protected final InputType inputType;
protected final TextField textField;
protected ModeCheonjiin(SettingsStore settings, InputType inputType, TextField textField) {
super(settings, inputType);
SPECIAL_CHAR_SEQUENCE_PREFIX = settings.holdForPunctuationInKorean() ? "11" : "1";
digitSequence = "";
allowedTextCases.add(CASE_LOWER);
this.inputType = inputType;
this.textField = textField;
initPredictions();
setLanguage(LanguageCollection.getLanguage(LanguageKind.KOREAN));
setSpecialCharacterConstants();
if (isEmailMode) {
// Note: applyPunctuationOrder() requires the language to be set
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(0), 0));
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(1), 1));
} else {
setCustomSpecialCharacters();
}
autoSpace = new AutoSpace(settings).setLanguage(language);
}
protected void setCustomSpecialCharacters() {
if (settings.holdForPunctuationInKorean()) {
ArrayList<String> specialChars = new ArrayList<>(applyPunctuationOrder(Characters.Special, 0));
specialChars.add(0, "0");
KEY_CHARACTERS.add(specialChars);
}
}
protected void setSpecialCharacterConstants() {
CUSTOM_EMOJI_SEQUENCE = SPECIAL_CHAR_SEQUENCE_PREFIX + EmojiLanguage.CUSTOM_EMOJI_SEQUENCE;
EMOJI_SEQUENCE = SPECIAL_CHAR_SEQUENCE_PREFIX + EmojiLanguage.EMOJI_SEQUENCE;
PUNCTUATION_SEQUENCE = SPECIAL_CHAR_SEQUENCE_PREFIX + NaturalLanguage.PUNCTUATION_KEY;
SPECIAL_CHAR_SEQUENCE = "000";
}
protected void initPredictions() {
predictions = new SyllablePredictions(settings);
predictions
.setOnlyExactMatches(true)
.setMinWords(0)
.setWordsChangedHandler(this::onPredictions);
}
@Override
public boolean onBackspace() {
if (settings.holdForPunctuationInKorean() && digitSequence.equals(PUNCTUATION_SEQUENCE)) {
digitSequence = "";
} else if (digitSequence.equals(SPECIAL_CHAR_SEQUENCE) || (!digitSequence.startsWith(PUNCTUATION_SEQUENCE) && Cheonjiin.isSingleJamo(digitSequence))) {
digitSequence = "";
} else if (!digitSequence.isEmpty()) {
digitSequence = digitSequence.substring(0, digitSequence.length() - 1);
}
return !digitSequence.isEmpty();
}
@Override
public boolean onNumber(int number, boolean hold, int repeat) {
if (hold) {
reset();
digitSequence = String.valueOf(number);
disablePredictions = true;
onNumberHold(number);
} else {
basicReset();
disablePredictions = false;
onNumberPress(number);
}
return true;
}
protected void onNumberHold(int number) {
if (settings.holdForPunctuationInKorean() && number == 0) {
disablePredictions = false;
digitSequence = SPECIAL_CHAR_SEQUENCE;
} else if (settings.holdForPunctuationInKorean() && number == 1) {
disablePredictions = false;
digitSequence = PUNCTUATION_SEQUENCE;
} else {
autoAcceptTimeout = 0;
suggestions.add(language.getKeyNumber(number));
}
}
protected void onNumberPress(int nextNumber) {
int rewindAmount = shouldRewindRepeatingNumbers(nextNumber);
if (rewindAmount > 0) {
digitSequence = digitSequence.substring(0, digitSequence.length() - rewindAmount);
}
if (digitSequence.startsWith(PUNCTUATION_SEQUENCE)) {
digitSequence = SPECIAL_CHAR_SEQUENCE_PREFIX + EmojiLanguage.validateEmojiSequence(digitSequence.substring(SPECIAL_CHAR_SEQUENCE_PREFIX.length()), nextNumber);
} else {
digitSequence += String.valueOf(nextNumber);
}
}
private int shouldRewindRepeatingNumbers(int nextNumber) {
final int nextChar = nextNumber + '0';
final int repeatingDigits = digitSequence.length() > 1 && digitSequence.charAt(digitSequence.length() - 1) == nextChar ? Cheonjiin.getRepeatingEndingDigits(digitSequence) : 0;
final int keyCharsCount = nextNumber == 0 ? 2 : language.getKeyCharacters(nextNumber).size();
if (!settings.holdForPunctuationInKorean() && SPECIAL_CHAR_SEQUENCE.equals(digitSequence + nextNumber)) {
return 0;
}
if (SPECIAL_CHAR_SEQUENCE.equals(digitSequence)) {
return SPECIAL_CHAR_SEQUENCE.length();
}
if (repeatingDigits == 0 || keyCharsCount < 2) {
return 0;
}
return keyCharsCount < repeatingDigits + 1 ? repeatingDigits : 0;
}
@Override
public boolean changeLanguage(@Nullable Language newLanguage) {
return LanguageKind.isKorean(newLanguage);
}
@Override
public void reset() {
basicReset();
digitSequence = "";
previousJamoSequence = "";
disablePredictions = false;
}
protected void basicReset() {
super.reset();
}
@Override
public void loadSuggestions(String ignored) {
if (disablePredictions || loadSpecialCharacters() || loadEmojis()) {
onSuggestionsUpdated.run();
return;
}
String seq = digitSequence;
if (shouldDisplayCustomEmojis()) {
seq = digitSequence.substring(SPECIAL_CHAR_SEQUENCE_PREFIX.length());
} else if (!previousJamoSequence.isEmpty()) {
seq = previousJamoSequence;
}
predictions
.setLanguage(shouldDisplayCustomEmojis() ? new EmojiLanguage() : language)
.setDigitSequence(seq)
.load();
}
protected boolean loadEmojis() {
if (shouldDisplayEmojis()) {
suggestions.clear();
suggestions.addAll(new EmojiLanguage().getKeyCharacters(digitSequence.charAt(0) - '0', getEmojiGroup()));
return true;
}
return false;
}
protected int getEmojiGroup() {
return digitSequence.length() - EMOJI_SEQUENCE.length();
}
protected boolean shouldDisplayEmojis() {
return !isEmailMode && digitSequence.startsWith(EMOJI_SEQUENCE) && !digitSequence.equals(CUSTOM_EMOJI_SEQUENCE);
}
protected boolean shouldDisplayCustomEmojis() {
return !isEmailMode && digitSequence.equals(CUSTOM_EMOJI_SEQUENCE);
}
@Override
protected boolean loadSpecialCharacters() {
if (!shouldDisplaySpecialCharacters()) {
return false;
}
int number = digitSequence.isEmpty() ? Integer.MAX_VALUE : digitSequence.charAt(0) - '0';
if (KEY_CHARACTERS.size() > number) {
suggestions.clear();
suggestions.addAll(KEY_CHARACTERS.get(number));
return true;
} else {
return super.loadSpecialCharacters();
}
}
protected boolean shouldDisplaySpecialCharacters() {
return digitSequence.equals(PUNCTUATION_SEQUENCE) || digitSequence.equals(SPECIAL_CHAR_SEQUENCE);
}
/**
* onPredictions
* Gets the currently available Predictions and sends them over to the external caller.
*/
protected void onPredictions() {
// in case the user hasn't added any custom emoji, do not allow advancing to the empty character group
if (predictions.getList().isEmpty() && digitSequence.startsWith(EMOJI_SEQUENCE)) {
digitSequence = EMOJI_SEQUENCE;
return;
}
suggestions.clear();
suggestions.addAll(predictions.getList());
onSuggestionsUpdated.run();
}
private void onReplacementPredictions() {
autoAcceptTimeout = 0;
onPredictions();
predictions.setWordsChangedHandler(this::onPredictions);
autoAcceptTimeout = -1;
loadSuggestions(null);
}
@Override
public boolean containsGeneratedSuggestions() {
return predictions.containsGeneratedWords();
}
@Override
public void replaceLastLetter() {
previousJamoSequence = Cheonjiin.stripRepeatingEndingDigits(digitSequence);
if (previousJamoSequence.isEmpty() || previousJamoSequence.length() == digitSequence.length()) {
previousJamoSequence = "";
return;
}
digitSequence = digitSequence.substring(previousJamoSequence.length());
predictions.setWordsChangedHandler(this::onReplacementPredictions);
}
@Override
public boolean shouldReplaceLastLetter(int nextKey, boolean hold) {
return !hold && !shouldDisplayEmojis() && Cheonjiin.isThereMediaVowel(digitSequence) && Cheonjiin.isVowelDigit(nextKey);
}
/**
* shouldAcceptPreviousSuggestion
* Used for analysis before processing the incoming pressed key.
*/
@Override
public boolean shouldAcceptPreviousSuggestion(int nextKey, boolean hold) {
return
(hold && !digitSequence.isEmpty())
|| (digitSequence.equals(SPECIAL_CHAR_SEQUENCE) && nextKey != 0)
|| (digitSequence.startsWith(PUNCTUATION_SEQUENCE) && nextKey != 1);
}
/**
* shouldAcceptPreviousSuggestion
* Used for analysis after loading the suggestions.
*/
@Override
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) {
return
!digitSequence.isEmpty()
&& !disablePredictions && !shouldDisplayEmojis() && !shouldDisplaySpecialCharacters() && predictions.noDbWords()
&& (Cheonjiin.endsWithDashVowel(digitSequence) || Cheonjiin.endsWithTwoConsonants(digitSequence));
}
@Override
public void onAcceptSuggestion(@NonNull String word, boolean preserveWordList) {
if (shouldDisplaySpecialCharacters() || shouldDisplayEmojis()) {
reset();
return;
}
String digitSequenceStash = "";
boolean mustReload = false;
if (predictions.noDbWords() && digitSequence.length() >= 2) {
digitSequenceStash = digitSequence.substring(digitSequence.length() - 1);
mustReload = true;
} else if (!previousJamoSequence.isEmpty()) {
digitSequenceStash = digitSequence;
}
reset();
digitSequence = digitSequenceStash;
if (mustReload) {
loadSuggestions(null);
}
}
protected boolean shouldSelectNextSpecialCharacters() {
return digitSequence.equals(SPECIAL_CHAR_SEQUENCE);
}
@Override
public boolean shouldAddTrailingSpace(boolean isWordAcceptedManually, int nextKey) {
return autoSpace.shouldAddTrailingSpace(textField, inputType, isWordAcceptedManually, nextKey);
}
@Override
public boolean shouldAddPrecedingSpace() {
return autoSpace.shouldAddBeforePunctuation(inputType, textField);
}
@Override
public boolean shouldDeletePrecedingSpace() {
return autoSpace.shouldDeletePrecedingSpace(inputType, textField);
}
@Override
public int getId() {
return MODE_PREDICTIVE;
}
@NonNull
@Override
public String toString() {
return language.getName();
}
}

View file

@ -2,12 +2,13 @@ package io.github.sspanak.tt9.ime.modes;
import androidx.annotation.NonNull;
import io.github.sspanak.tt9.hacks.InputType;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
// see: InputType.isSpecialNumeric()
public class ModePassthrough extends InputMode {
ModePassthrough(SettingsStore settings) {
super(settings);
class ModePassthrough extends InputMode {
protected ModePassthrough(SettingsStore settings, InputType inputType) {
super(settings, inputType);
reset();
allowedTextCases.add(CASE_LOWER);
}
@ -16,9 +17,6 @@ public class ModePassthrough extends InputMode {
@Override public int getSequenceLength() { return 0; }
@Override @NonNull public String toString() { return "--"; }
@Override public boolean isNumeric() { return true; }
@Override public boolean isPassthrough() { return true; }
@Override public boolean onNumber(int number, boolean hold, int repeat) { return false; }
@Override public boolean shouldIgnoreText(String text) { return true; }
}

View file

@ -7,58 +7,58 @@ import java.util.ArrayList;
import io.github.sspanak.tt9.hacks.InputType;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
import io.github.sspanak.tt9.ime.modes.helpers.AutoTextCase;
import io.github.sspanak.tt9.ime.modes.helpers.Predictions;
import io.github.sspanak.tt9.ime.modes.predictions.WordPredictions;
import io.github.sspanak.tt9.languages.EmojiLanguage;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.languages.NaturalLanguage;
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Characters;
import io.github.sspanak.tt9.util.Logger;
import io.github.sspanak.tt9.util.Text;
import io.github.sspanak.tt9.util.TextTools;
public class ModePredictive extends InputMode {
class ModeWords extends ModeCheonjiin {
private final String LOG_TAG = getClass().getSimpleName();
private final ArrayList<ArrayList<String>> KEY_CHARACTERS = new ArrayList<>();
public int getId() { return MODE_PREDICTIVE; }
private String lastAcceptedWord = "";
// stem filter
private boolean isStemFuzzy = false;
private String stem = "";
// async suggestion handling
private boolean disablePredictions = false;
// text analysis tools
private final AutoSpace autoSpace;
private final AutoTextCase autoTextCase;
private final Predictions predictions;
private boolean isCursorDirectionForward = false;
private int textFieldTextCase;
ModePredictive(SettingsStore settings, InputType inputType, TextField textField, Language lang) {
super(settings);
protected ModeWords(SettingsStore settings, Language lang, InputType inputType, TextField textField) {
super(settings, inputType, textField);
autoSpace = new AutoSpace(settings).setLanguage(lang);
autoTextCase = new AutoTextCase(settings);
digitSequence = "";
predictions = new Predictions(settings, textField);
changeLanguage(lang);
defaultTextCase();
determineTextFieldTextCase();
}
if (inputType.isEmail()) {
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(0), 0));
KEY_CHARACTERS.add(applyPunctuationOrder(Characters.Email.get(1), 1));
}
@Override protected void setCustomSpecialCharacters() {} // we use the default ones
protected void setSpecialCharacterConstants() {
PUNCTUATION_SEQUENCE = NaturalLanguage.PUNCTUATION_KEY;
EMOJI_SEQUENCE = EmojiLanguage.EMOJI_SEQUENCE;
CUSTOM_EMOJI_SEQUENCE = EmojiLanguage.CUSTOM_EMOJI_SEQUENCE;
SPECIAL_CHAR_SEQUENCE = NaturalLanguage.SPECIAL_CHAR_KEY;
}
@Override
protected void initPredictions() {
predictions = new WordPredictions(settings, textField);
predictions.setWordsChangedHandler(this::onPredictions);
}
@ -85,31 +85,34 @@ public class ModePredictive extends InputMode {
@Override
public boolean onNumber(int number, boolean hold, int repeat) {
isCursorDirectionForward = true;
if (hold) {
// hold to type any digit
reset();
autoAcceptTimeout = 0;
disablePredictions = true;
digitSequence = String.valueOf(number);
suggestions.add(language.getKeyNumber(number));
} else {
super.reset();
digitSequence = EmojiLanguage.validateEmojiSequence(digitSequence, number);
disablePredictions = false;
if (digitSequence.equals(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
autoAcceptTimeout = 0;
}
}
return true;
return super.onNumber(number, hold, repeat);
}
@Override
public void changeLanguage(@Nullable Language newLanguage) {
super.changeLanguage(newLanguage);
protected void onNumberHold(int number) {
autoAcceptTimeout = 0;
suggestions.add(language.getKeyNumber(number));
}
@Override
protected void onNumberPress(int number) {
digitSequence = EmojiLanguage.validateEmojiSequence(digitSequence, number);
if (digitSequence.equals(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
autoAcceptTimeout = 0;
}
}
@Override
public boolean changeLanguage(@Nullable Language newLanguage) {
if (newLanguage != null && newLanguage.isSyllabary()) {
return false;
}
super.setLanguage(newLanguage);
autoSpace.setLanguage(language);
@ -119,12 +122,14 @@ public class ModePredictive extends InputMode {
allowedTextCases.add(CASE_CAPITALIZE);
allowedTextCases.add(CASE_UPPER);
}
return true;
}
@Override
public boolean recompose(String word) {
if (!language.hasSpaceBetweenWords()) {
if (!language.hasSpaceBetweenWords() || language.isSyllabary()) {
return false;
}
@ -149,7 +154,7 @@ public class ModePredictive extends InputMode {
@Override
public void reset() {
super.reset();
basicReset();
digitSequence = "";
disablePredictions = false;
stem = "";
@ -252,60 +257,33 @@ public class ModePredictive extends InputMode {
}
@Override
public boolean containsGeneratedSuggestions() {
return predictions.containsGeneratedWords();
}
/**
* loadSuggestions
* Loads the possible list of suggestions for the current digitSequence. "currentWord" is used
* for generating suggestions when there are no results.
* See: Predictions.generatePossibleCompletions()
* See: WordPredictions.generatePossibleCompletions()
*/
@Override
public void loadSuggestions(String currentWord) {
if (disablePredictions) {
super.loadSuggestions(currentWord);
if (disablePredictions || loadPreferredChar() || loadSpecialCharacters() || loadEmojis()) {
onSuggestionsUpdated.run();
return;
}
if (loadStaticSuggestions()) {
return;
}
Language searchLanguage = digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE) ? new EmojiLanguage() : language;
predictions
.setDigitSequence(digitSequence)
((WordPredictions) predictions)
.setInputWord(currentWord.isEmpty() ? stem : currentWord)
.setIsStemFuzzy(isStemFuzzy)
.setStem(stem)
.setLanguage(searchLanguage)
.setInputWord(currentWord.isEmpty() ? stem : currentWord)
.setWordsChangedHandler(this::onPredictions)
.setDigitSequence(digitSequence)
.setLanguage(shouldDisplayCustomEmojis() ? new EmojiLanguage() : language)
.load();
}
/**
* loadStatic
* Loads words that are not in the database and are supposed to be in the same order, such as
* emoji or the preferred character for double "0". Returns "false", when there are no static
* options for the current digitSequence.
*/
private boolean loadStaticSuggestions() {
if (digitSequence.equals(NaturalLanguage.PUNCTUATION_KEY) || digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY)) {
loadSpecialCharacters();
onSuggestionsUpdated.run();
return true;
} else if (!digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE) && digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE)) {
suggestions.clear();
suggestions.addAll(new EmojiLanguage().getKeyCharacters(digitSequence.charAt(0) - '0', digitSequence.length() - 2));
onSuggestionsUpdated.run();
return true;
} else if (digitSequence.startsWith(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
private boolean loadPreferredChar() {
if (digitSequence.startsWith(NaturalLanguage.PREFERRED_CHAR_SEQUENCE)) {
suggestions.clear();
suggestions.add(settings.getDoubleZeroChar());
onSuggestionsUpdated.run();
return true;
}
@ -313,37 +291,6 @@ public class ModePredictive extends InputMode {
}
@Override
protected boolean loadSpecialCharacters() {
int number = digitSequence.charAt(0) - '0';
if (KEY_CHARACTERS.size() > number) {
suggestions.clear();
suggestions.addAll(KEY_CHARACTERS.get(number));
return true;
} else {
return super.loadSpecialCharacters();
}
}
/**
* onPredictions
* Gets the currently available Predictions and sends them over to the external caller.
*/
private void onPredictions() {
// in case the user hasn't added any custom emoji, do not allow advancing to the empty character group
if (predictions.getList().isEmpty() && digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE)) {
digitSequence = EmojiLanguage.EMOJI_SEQUENCE;
return;
}
suggestions.clear();
suggestions.addAll(predictions.getList());
onSuggestionsUpdated.run();
}
/**
* onAcceptSuggestion
* Bring this word up in the suggestions list next time and if necessary preserves the suggestion list
@ -365,26 +312,21 @@ public class ModePredictive extends InputMode {
return;
}
if (Characters.isStaticEmoji(currentWord)) {
// emojis and special chars are not in the database, so there is no point in wasting resources
// running queries on them
if (!new Text(currentWord).isAlphabetic()) {
return;
}
// increment the frequency of the given word
try {
Language workingLanguage = TextTools.isGraphic(currentWord) ? new EmojiLanguage() : language;
String sequence = workingLanguage.getDigitSequenceForWord(currentWord);
// punctuation and special chars are not in the database, so there is no point in
// running queries that would update nothing
if (!sequence.equals(NaturalLanguage.PUNCTUATION_KEY) && !sequence.startsWith(NaturalLanguage.SPECIAL_CHAR_KEY)) {
predictions.onAccept(currentWord, sequence);
}
// increment the frequency of the given word
predictions.onAccept(currentWord, language.getDigitSequenceForWord(currentWord));
} catch (Exception e) {
Logger.e(LOG_TAG, "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage());
}
}
@Override
protected String adjustSuggestionTextCase(String word, int newTextCase) {
return autoTextCase.adjustSuggestionTextCase(new Text(language, word), newTextCase);
@ -395,17 +337,17 @@ public class ModePredictive extends InputMode {
textCase = autoTextCase.determineNextWordTextCase(textCase, textFieldTextCase, textBeforeCursor, digitSequence);
}
private void determineTextFieldTextCase() {
int fieldCase = inputType.determineTextCase();
textFieldTextCase = allowedTextCases.contains(fieldCase) ? fieldCase : CASE_UNDEFINED;
}
@Override
public int getTextCase() {
// Filter out the internally used text cases. They have no meaning outside this class.
return (textCase == CASE_UPPER || textCase == CASE_LOWER) ? textCase : CASE_CAPITALIZE;
}
@Override
protected boolean nextSpecialCharacters() {
return digitSequence.equals(NaturalLanguage.SPECIAL_CHAR_KEY) && super.nextSpecialCharacters();
}
@Override
public boolean nextTextCase() {
int before = textCase;
@ -424,6 +366,11 @@ public class ModePredictive extends InputMode {
}
@Override
public boolean shouldReplaceLastLetter(int n, boolean h) {
return false;
}
/**
* shouldAcceptPreviousSuggestion
@ -436,7 +383,7 @@ public class ModePredictive extends InputMode {
return true;
}
final char SPECIAL_CHAR_KEY_CODE = NaturalLanguage.SPECIAL_CHAR_KEY.charAt(0);
final char SPECIAL_CHAR_KEY_CODE = SPECIAL_CHAR_SEQUENCE.charAt(0);
final int SPECIAL_CHAR_KEY = SPECIAL_CHAR_KEY_CODE - '0';
// Prevent typing the preferred character when the user has scrolled the special char suggestions.
@ -454,9 +401,9 @@ public class ModePredictive extends InputMode {
}
/**
/**
* shouldAcceptPreviousSuggestion
* Variant for post suggestion load analysis.
* Used for analysis after loading the suggestions.
*/
@Override
public boolean shouldAcceptPreviousSuggestion(String unacceptedText) {
@ -473,8 +420,8 @@ public class ModePredictive extends InputMode {
return
!digitSequence.isEmpty()
&& predictions.noDbWords()
&& digitSequence.contains(NaturalLanguage.PUNCTUATION_KEY)
&& !digitSequence.startsWith(EmojiLanguage.EMOJI_SEQUENCE)
&& digitSequence.contains(PUNCTUATION_SEQUENCE)
&& !digitSequence.startsWith(EMOJI_SEQUENCE)
&& Text.containsOtherThan1(digitSequence);
}
@ -498,24 +445,6 @@ public class ModePredictive extends InputMode {
}
@Override
public boolean shouldAddTrailingSpace(InputType inputType, TextField textField, boolean isWordAcceptedManually, int nextKey) {
return autoSpace.shouldAddTrailingSpace(textField, inputType, isWordAcceptedManually, nextKey);
}
@Override
public boolean shouldAddPrecedingSpace(InputType inputType, TextField textField) {
return autoSpace.shouldAddBeforePunctuation(inputType, textField);
}
@Override
public boolean shouldDeletePrecedingSpace(InputType inputType, TextField textField) {
return autoSpace.shouldDeletePrecedingSpace(inputType, textField);
}
@NonNull
@Override
public String toString() {

View file

@ -21,11 +21,13 @@ public class AutoSpace {
private final SettingsStore settings;
private boolean isLanguageFrench;
private boolean isLanguageWithAlphabet;
private boolean isLanguageWithSpaceBetweenWords;
public AutoSpace(SettingsStore settingsStore) {
settings = settingsStore;
isLanguageWithAlphabet = false;
isLanguageFrench = false;
isLanguageWithSpaceBetweenWords = true;
}
@ -33,6 +35,7 @@ public class AutoSpace {
public AutoSpace setLanguage(Language language) {
isLanguageFrench = LanguageKind.isFrench(language);
isLanguageWithAlphabet = language != null && !language.isSyllabary();
isLanguageWithSpaceBetweenWords = language != null && language.hasSpaceBetweenWords();
return this;
}
@ -120,6 +123,7 @@ public class AutoSpace {
private boolean shouldAddAfterWord(boolean isWordAcceptedManually, String previousChars, Text nextChars, int nextKey) {
return
isWordAcceptedManually // Do not add space when auto-accepting words, because it feels very confusing when typing.
&& isLanguageWithAlphabet
&& nextKey != 1
&& nextChars.isEmpty()
&& Text.previousIsLetter(previousChars);

View file

@ -0,0 +1,75 @@
package io.github.sspanak.tt9.ime.modes.helpers;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
public class Cheonjiin {
private static final Pattern MEDIAL_VOWEL = Pattern.compile("^[4-9|0]+[1-3]+[4-9|0]+$");
public static boolean isThereMediaVowel(@NonNull String digitSequence) {
return !digitSequence.isEmpty() && MEDIAL_VOWEL.matcher(digitSequence).find();
}
private static boolean isVowelDigit(char digit) {
return digit == '1' || digit == '2' || digit == '3';
}
public static boolean isSingleJamo(@NonNull String digitSequence) {
int digits = digitSequence.length();
if (digits == 0 || digits > 3) {
return false;
}
char firstDigit = digitSequence.charAt(0);
for (int i = 1; i < digits; i++) {
if (digitSequence.charAt(i) != firstDigit) {
return false;
}
}
return true;
}
public static boolean isVowelDigit(int digit) {
return digit == 1 || digit == 2 || digit == 3;
}
public static boolean endsWithTwoConsonants(@NonNull String digitSequence) {
if (digitSequence.length() < 2) {
return false;
}
char consonant1 = digitSequence.charAt(digitSequence.length() - 1);
for (int i = digitSequence.length() - 2; i >= 0; i--) {
if (!isVowelDigit(digitSequence.charAt(i))) {
return consonant1 != digitSequence.charAt(i);
}
}
return false;
}
public static boolean endsWithDashVowel(@NonNull String digitSequence) {
int lastDigit = digitSequence.isEmpty() ? -1 : digitSequence.charAt(digitSequence.length() - 1) - '0';
return lastDigit == 1 || lastDigit == 3;
}
public static int getRepeatingEndingDigits(@NonNull String digitSequence) {
int count = 0;
for (int i = digitSequence.length() - 1; i >= 0; i--) {
if (digitSequence.charAt(i) == digitSequence.charAt(digitSequence.length() - 1)) {
count++;
} else {
break;
}
}
return count;
}
public static String stripRepeatingEndingDigits(@NonNull String digitSequence) {
int end = digitSequence.length() - getRepeatingEndingDigits(digitSequence);
return digitSequence.length() > 1 ? digitSequence.substring(0, end) : digitSequence;
}
}

View file

@ -0,0 +1,124 @@
package io.github.sspanak.tt9.ime.modes.predictions;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import io.github.sspanak.tt9.db.DataStore;
import io.github.sspanak.tt9.db.words.WordStore;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.NullLanguage;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
abstract public class Predictions {
protected final SettingsStore settings;
// settings
@NonNull protected String digitSequence = "";
@NonNull protected Language language = new NullLanguage();
protected int minWords = SettingsStore.SUGGESTIONS_MIN;
protected int maxWords = SettingsStore.SUGGESTIONS_MAX;
protected boolean onlyExactMatches = false;
@NonNull protected String stem = "";
// async operations
protected Runnable onWordsChanged = () -> {};
// data
protected boolean areThereDbWords = false;
protected boolean containsGeneratedWords = false;
@NonNull protected ArrayList<String> words = new ArrayList<>();
public Predictions(SettingsStore settings) {
this.settings = settings;
}
public Predictions setDigitSequence(String digitSequence) {
this.digitSequence = digitSequence;
return this;
}
public Predictions setLanguage(Language language) {
this.language = language;
return this;
}
public Predictions setMinWords(int minWords) {
this.minWords = minWords;
return this;
}
public Predictions setOnlyExactMatches(boolean onlyExactMatches) {
this.onlyExactMatches = onlyExactMatches;
return this;
}
public void setWordsChangedHandler(Runnable handler) {
onWordsChanged = handler;
}
public boolean containsGeneratedWords() {
return containsGeneratedWords;
}
public ArrayList<String> getList() {
return words;
}
public boolean noDbWords() {
return !areThereDbWords;
}
/**
* suggestMissingWords
* Takes a list of words and appends them to the words list, if they are missing.
*/
protected void suggestMissingWords(ArrayList<String> newWords) {
for (String newWord : newWords) {
if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) {
words.add(newWord);
}
}
}
/**
* load
* Queries the dictionary database for a list of words matching the current language and sequence.
*/
public void load() {
containsGeneratedWords = false;
if (digitSequence.isEmpty()) {
words.clear();
onWordsChanged.run();
return;
}
DataStore.getWords(
(dbWords) -> onDbWords(dbWords, isRetryAllowed()),
language,
digitSequence,
onlyExactMatches ? WordStore.FILTER_EXACT_MATCHES_ONLY : stem,
minWords,
maxWords
);
}
abstract public void onAccept(String word, String sequence);
abstract protected boolean isRetryAllowed();
abstract protected void onDbWords(ArrayList<String> dbWords, boolean retryAllowed);
abstract protected ArrayList<String> generateWordVariations(String baseWord);
}

View file

@ -0,0 +1,109 @@
package io.github.sspanak.tt9.ime.modes.predictions;
import java.util.ArrayList;
import io.github.sspanak.tt9.ime.modes.helpers.Cheonjiin;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
public class SyllablePredictions extends Predictions {
int defaultMinWords;
int loadAttempts;
String lastWord = "";
String lastStableWord = "";
int lastStableSequenceLength;
public SyllablePredictions(SettingsStore settings) {
super(settings);
}
@Override
public Predictions setMinWords(int minWords) {
defaultMinWords = minWords;
return super.setMinWords(minWords);
}
@Override
protected boolean isRetryAllowed() {
return loadAttempts == 0;
}
@Override
public void load() {
loadAttempts = 0;
minWords = defaultMinWords;
super.load();
}
private void loadSimilar() {
loadAttempts++;
minWords = defaultMinWords + 1;
super.load();
}
@Override
protected void onDbWords(ArrayList<String> dbWords, boolean retryAllowed) {
areThereDbWords = !dbWords.isEmpty();
if (loadAttempts == 0) {
words.clear();
} else {
onWordsChanged.run();
return;
}
if (digitSequence.length() < lastStableSequenceLength) {
lastStableWord = "";
lastStableSequenceLength = 0;
}
if (areThereDbWords) {
lastWord = dbWords.get(0);
words.addAll(dbWords);
} else {
if (lastStableWord.isEmpty() && !lastWord.isEmpty()) {
lastStableWord = lastWord;
lastStableSequenceLength = digitSequence.length();
}
lastWord = "";
words.addAll(generateWordVariations(lastStableWord));
}
if (retryAllowed && !areThereDbWords) {
loadSimilar();
return;
}
onWordsChanged.run();
}
@Override
protected ArrayList<String> generateWordVariations(String baseWord) {
baseWord = baseWord == null ? "" : baseWord;
ArrayList<String> variants = new ArrayList<>();
try {
int charIndex = Cheonjiin.getRepeatingEndingDigits(digitSequence) - 1;
int key = digitSequence.charAt(digitSequence.length() - 1) - '0';
String variant = baseWord + language.getKeyCharacters(key).get(charIndex);
variants.add(variant);
} catch (Exception ignored) {
variants.add(baseWord);
}
return variants;
}
@Override
public void onAccept(String word, String sequence) {
lastWord = lastStableWord = "";
lastStableSequenceLength = 0;
}
}

View file

@ -1,109 +1,46 @@
package io.github.sspanak.tt9.ime.modes.helpers;
package io.github.sspanak.tt9.ime.modes.predictions;
import java.util.ArrayList;
import io.github.sspanak.tt9.db.DataStore;
import io.github.sspanak.tt9.ime.helpers.TextField;
import io.github.sspanak.tt9.languages.EmojiLanguage;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Characters;
public class Predictions {
private final SettingsStore settings;
public class WordPredictions extends Predictions {
private final TextField textField;
private String digitSequence;
private String inputWord;
private boolean isStemFuzzy;
private Language language;
private String stem;
private String lastEnforcedTopWord = "";
// async operations
private Runnable onWordsChanged = () -> {};
// data
private boolean areThereDbWords = false;
private boolean containsGeneratedWords = false;
private ArrayList<String> words = new ArrayList<>();
public Predictions(SettingsStore settings, TextField textField) {
this.settings = settings;
public WordPredictions(SettingsStore settings, TextField textField) {
super(settings);
stem = "";
this.textField = textField;
}
public Predictions setLanguage(Language language) {
this.language = language;
return this;
}
public Predictions setDigitSequence(String digitSequence) {
this.digitSequence = digitSequence;
return this;
}
public Predictions setIsStemFuzzy(boolean yes) {
public WordPredictions setIsStemFuzzy(boolean yes) {
this.isStemFuzzy = yes;
return this;
}
public Predictions setStem(String stem) {
public WordPredictions setStem(String stem) {
this.stem = stem;
return this;
}
public Predictions setInputWord(String inputWord) {
public WordPredictions setInputWord(String inputWord) {
this.inputWord = inputWord.toLowerCase(language.getLocale());
return this;
}
public Predictions setWordsChangedHandler(Runnable handler) {
onWordsChanged = handler;
return this;
}
public boolean containsGeneratedWords() {
return containsGeneratedWords;
}
public ArrayList<String> getList() {
return words;
}
public boolean noDbWords() {
return !areThereDbWords;
}
/**
* load
* Queries the dictionary database for a list of words matching the current language and
* sequence or loads the static ones.
*/
public void load() {
containsGeneratedWords = false;
if (digitSequence == null || digitSequence.isEmpty()) {
words.clear();
onWordsChanged.run();
return;
}
boolean retryAllowed = !digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE);
DataStore.getWords(
(dbWords) -> onDbWords(dbWords, retryAllowed),
language,
digitSequence,
stem,
SettingsStore.SUGGESTIONS_MIN,
SettingsStore.SUGGESTIONS_MAX
);
}
private void loadWithoutLeadingPunctuation() {
DataStore.getWords(
@ -123,13 +60,18 @@ public class Predictions {
}
@Override
protected boolean isRetryAllowed() {
return !EmojiLanguage.CUSTOM_EMOJI_SEQUENCE.equals(digitSequence);
}
/**
* dbWordsHandler
* Callback for when the database has finished loading words. If there were no matches in the database,
* they will be generated based on the "inputWord". After the word list is compiled, it notifies the
* external handler it is now possible to use it with "getList()".
*/
private void onDbWords(ArrayList<String> dbWords, boolean isRetryAllowed) {
protected void onDbWords(ArrayList<String> dbWords, boolean isRetryAllowed) {
// only the first round matters, the second one is just for getting the letters for a given key
areThereDbWords = !dbWords.isEmpty() && isRetryAllowed;
@ -167,29 +109,15 @@ public class Predictions {
}
/**
* suggestMissingWords
* Takes a list of words and appends them to the words list, if they are missing.
*/
private void suggestMissingWords(ArrayList<String> newWords) {
for (String newWord : newWords) {
if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) {
words.add(newWord);
}
}
}
/**
* generateWordVariations
* When there are no matching suggestions after the last key press, generate a list of possible
* ones, so that the user can complete a missing word that is completely different from the ones
* in the dictionary.
*
* For example, if the word is "missin_" and the last pressed key is "4", the results would be:
* | missing | missinh | missini |
*/
private ArrayList<String> generateWordVariations(String baseWord) {
protected ArrayList<String> generateWordVariations(String baseWord) {
ArrayList<String> generatedWords = new ArrayList<>();
// This function is called from async context, so by the time it is executed, the digit sequence
@ -265,9 +193,9 @@ public class Predictions {
* Similar to generatePossibleCompletions(), but uses the current filter as a base word. This is
* used to complement the database results with all possible variations for the next key, when
* the stem filter is on.
*
* <p>
* It will not generate anything if more than one key was pressed after filtering though.
*
* <p>
* For example, if the filter is "extr", the current word is "extr_" and the user has pressed "1",
* the database would have returned only "extra", but this function would also
* generate: "extrb" and "extrc". This is useful for typing an unknown word, that is similar to
@ -312,7 +240,7 @@ public class Predictions {
!settings.getPredictWordPairs()
// If the accepted word is longer than the sequence, it is some different word, not a textonym
// of the fist suggestion. We don't need to store it.
|| word == null || digitSequence == null
|| word == null
|| word.length() != digitSequence.length()
// If the word is the first suggestion, we have already guessed it right, and it makes no
// sense to store it as a popular pair.

View file

@ -16,6 +16,7 @@ abstract public class Language {
protected String name;
protected boolean hasSpaceBetweenWords = true;
protected boolean hasUpperCase = true;
protected boolean isSyllabary = false;
public int getId() {
@ -65,6 +66,10 @@ abstract public class Language {
return hasUpperCase;
}
final public boolean isSyllabary() {
return isSyllabary;
}
@NonNull
@Override
final public String toString() {

View file

@ -30,19 +30,17 @@ public class LanguageCollection {
}
public static LanguageCollection getInstance(Context context) {
public static void init(Context context) {
if (self == null) {
self = new LanguageCollection(context);
}
return self;
}
@Nullable
public static NaturalLanguage getLanguage(Context context, String langId) {
public static NaturalLanguage getLanguage(String langId) {
try {
return getLanguage(context, Integer.parseInt(langId));
return getLanguage(Integer.parseInt(langId));
} catch (NumberFormatException e) {
return null;
}
@ -50,23 +48,23 @@ public class LanguageCollection {
@Nullable
public static NaturalLanguage getLanguage(Context context, int langId) {
if (getInstance(context).languages.containsKey(langId)) {
return getInstance(context).languages.get(langId);
public static NaturalLanguage getLanguage(int langId) {
if (self.languages.containsKey(langId)) {
return self.languages.get(langId);
}
return null;
}
@NonNull public static Language getDefault(Context context) {
Language language = getByLocale(context, SystemSettings.getLocale());
language = language == null ? getByLocale(context, "en") : language;
@NonNull public static Language getDefault() {
Language language = getByLocale(SystemSettings.getLocale());
language = language == null ? getByLocale("en") : language;
return language == null ? new NullLanguage() : language;
}
@Nullable
public static NaturalLanguage getByLanguageCode(Context context, String languageCode) {
for (NaturalLanguage lang : getInstance(context).languages.values()) {
public static NaturalLanguage getByLanguageCode(String languageCode) {
for (NaturalLanguage lang : self.languages.values()) {
if (lang.getLocale().getLanguage().equals(new Locale(languageCode).getLanguage())) {
return lang;
}
@ -76,8 +74,8 @@ public class LanguageCollection {
}
@Nullable
public static NaturalLanguage getByLocale(Context context, String locale) {
for (NaturalLanguage lang : getInstance(context).languages.values()) {
public static NaturalLanguage getByLocale(String locale) {
for (NaturalLanguage lang : self.languages.values()) {
if (lang.getLocale().toString().equals(locale)) {
return lang;
}
@ -86,14 +84,14 @@ public class LanguageCollection {
return null;
}
public static ArrayList<Language> getAll(Context context, ArrayList<Integer> languageIds, boolean sort) {
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds, boolean sort) {
if (languageIds == null) {
return new ArrayList<>();
}
ArrayList<NaturalLanguage> langList = new ArrayList<>();
for (int languageId : languageIds) {
NaturalLanguage lang = getLanguage(context, languageId);
NaturalLanguage lang = getLanguage(languageId);
if (lang != null) {
langList.add(lang);
}
@ -106,12 +104,12 @@ public class LanguageCollection {
return new ArrayList<>(langList);
}
public static ArrayList<Language> getAll(Context context, ArrayList<Integer> languageIds) {
return getAll(context, languageIds, false);
public static ArrayList<Language> getAll(ArrayList<Integer> languageIds) {
return getAll(languageIds, false);
}
public static ArrayList<Language> getAll(Context context, boolean sort) {
ArrayList<NaturalLanguage> langList = new ArrayList<>(getInstance(context).languages.values());
public static ArrayList<Language> getAll(boolean sort) {
ArrayList<NaturalLanguage> langList = new ArrayList<>(self.languages.values());
if (sort) {
Collections.sort(langList);
@ -120,8 +118,8 @@ public class LanguageCollection {
return new ArrayList<>(langList);
}
public static ArrayList<Language> getAll(Context context) {
return getAll(context,false);
public static ArrayList<Language> getAll() {
return getAll(false);
}
public static String toString(ArrayList<Language> list) {

View file

@ -15,6 +15,8 @@ import io.github.sspanak.tt9.util.AssetFile;
import io.github.sspanak.tt9.util.Logger;
public class LanguageDefinition extends AssetFile {
private static final String LOG_TAG = LanguageDefinition.class.getSimpleName();
private static final String languagesDir = "languages";
private static final String definitionsDir = languagesDir + "/definitions";
@ -22,6 +24,7 @@ public class LanguageDefinition extends AssetFile {
public String dictionaryFile = "";
public boolean hasSpaceBetweenWords = true;
public boolean hasUpperCase = true;
public boolean isSyllabary = false;
public ArrayList<ArrayList<String>> layout = new ArrayList<>();
public String locale = "";
public String name = "";
@ -40,9 +43,9 @@ public class LanguageDefinition extends AssetFile {
ArrayList<String> files = new ArrayList<>();
try {
files.addAll(Arrays.asList(assets.list(definitionsDir)));
Logger.d("LanguageDefinition", "Found: " + files.size() + " languages.");
Logger.d(LOG_TAG, "Found: " + files.size() + " languages.");
} catch (IOException | NullPointerException e) {
Logger.e("tt9.LanguageDefinition", "Failed reading language definitions from: '" + definitionsDir + "'. " + e.getMessage());
Logger.e(LOG_TAG, "Failed reading language definitions from: '" + definitionsDir + "'. " + e.getMessage());
}
return files;
@ -95,6 +98,7 @@ public class LanguageDefinition extends AssetFile {
hasSpaceBetweenWords = getPropertyFromYaml(yaml, "hasSpaceBetweenWords", hasSpaceBetweenWords);
hasUpperCase = getPropertyFromYaml(yaml, "hasUpperCase", hasUpperCase);
isSyllabary = hasYamlProperty(yaml, "sounds");
layout = getLayoutFromYaml(yaml);
locale = getPropertyFromYaml(yaml, "locale", locale);
name = getPropertyFromYaml(yaml, "name", name);
@ -126,6 +130,19 @@ public class LanguageDefinition extends AssetFile {
}
private boolean hasYamlProperty(ArrayList<String> yaml, String property) {
final String yamlProperty = property + ":";
for (String line : yaml) {
if (line.startsWith(yamlProperty)) {
return true;
}
}
return false;
}
/**
* The boolean variant of getPropertyFromYaml. It returns true if the property is found and is:
* "true", "on", "yes" or "y".

View file

@ -1,13 +1,19 @@
package io.github.sspanak.tt9.languages;
import java.util.Locale;
public class LanguageKind {
public static final int KOREAN = 601579;
public static boolean isArabic(Language language) { return language != null && language.getId() == 502337; }
public static boolean isBulgarian(Language language) { return language != null && language.getId() == 231650; }
public static boolean isCyrillic(Language language) { return language != null && language.getKeyCharacters(2).contains("а"); }
public static boolean isEnglish(Language language) { return language != null && language.getLocale().equals(Locale.ENGLISH); }
public static boolean isFrench(Language language) { return language != null && language.getId() == 596550; }
public static boolean isGreek(Language language) { return language != null && language.getId() == 597381; }
public static boolean isHebrew(Language language) { return language != null && (language.getId() == 305450 || language.getId() == 403177); }
public static boolean isHinglish(Language language) { return language != null && language.getId() == 468421; }
public static boolean isKorean(Language language) { return language != null && language.getId() == KOREAN; }
public static boolean isLatinBased(Language language) { return language != null && language.getKeyCharacters(2).contains("a"); }
public static boolean isRTL(Language language) { return isArabic(language) || isHebrew(language); }
public static boolean isUkrainian(Language language) { return language != null && language.getId() == 54645; }

View file

@ -9,6 +9,7 @@ import java.util.Locale;
import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.util.Characters;
import io.github.sspanak.tt9.util.Text;
import io.github.sspanak.tt9.util.TextTools;
public class NaturalLanguage extends Language implements Comparable<NaturalLanguage> {
@ -31,6 +32,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
lang.dictionaryFile = definition.getDictionaryFile();
lang.hasSpaceBetweenWords = definition.hasSpaceBetweenWords;
lang.hasUpperCase = definition.hasUpperCase;
lang.isSyllabary = definition.isSyllabary;
lang.name = definition.name.isEmpty() ? lang.name : definition.name;
lang.setLocale(definition);
lang.setLayout(definition);
@ -78,6 +80,7 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
final String FRENCH_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_FR";
final String GERMAN_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_DE";
final String GREEK_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_GR";
final String KOREAN_PUNCTUATION_STYLE = PUNCTUATION_PLACEHOLDER + "_KR";
ArrayList<String> keyChars = new ArrayList<>();
for (String defChar : definitionChars) {
@ -100,6 +103,9 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
case GREEK_PUNCTUATION_STYLE:
keyChars.addAll(Characters.PunctuationGreek);
break;
case KOREAN_PUNCTUATION_STYLE:
keyChars.addAll(Characters.PunctuationKorean);
break;
default:
keyChars.add(defChar);
break;
@ -263,7 +269,12 @@ public class NaturalLanguage extends Language implements Comparable<NaturalLangu
public boolean isValidWord(String word) {
if (word == null || word.isEmpty() || (word.length() == 1 && Character.isDigit(word.charAt(0)))) {
if (
word == null
|| word.isEmpty()
|| (isSyllabary && LanguageKind.isKorean(this) && TextTools.isHangul(word))
|| (word.length() == 1 && Character.isDigit(word.charAt(0)))
) {
return true;
}

View file

@ -17,6 +17,7 @@ import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DataStore;
import io.github.sspanak.tt9.db.words.LegacyDb;
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
import io.github.sspanak.tt9.preferences.screens.BaseScreenFragment;
import io.github.sspanak.tt9.preferences.screens.UsageStatsScreen;
@ -43,10 +44,11 @@ public class PreferencesActivity extends ActivityWithNavigation implements Prefe
applyTheme();
Logger.setLevel(settings.getLogLevel());
LanguageCollection.init(this);
try (LegacyDb db = new LegacyDb(this)) { db.clear(); }
DataStore.init(this);
InputModeValidator.validateEnabledLanguages(this, settings.getEnabledLanguageIds());
InputModeValidator.validateEnabledLanguages(settings.getEnabledLanguageIds());
validateFunctionKeys();
super.onCreate(savedInstanceState);

View file

@ -94,6 +94,7 @@ public class Hotkeys {
defaultKeys.put(SectionKeymap.ITEM_NEXT_LANGUAGE, -KeyEvent.KEYCODE_POUND); // negative means "hold"
defaultKeys.put(SectionKeymap.ITEM_SELECT_KEYBOARD, KeyEvent.KEYCODE_UNKNOWN);
defaultKeys.put(SectionKeymap.ITEM_SHIFT, KeyEvent.KEYCODE_STAR);
defaultKeys.put(SectionKeymap.ITEM_SPACE_KOREAN, KeyEvent.KEYCODE_STAR);
defaultKeys.put(SectionKeymap.ITEM_SHOW_SETTINGS, KeyEvent.KEYCODE_UNKNOWN);
defaultKeys.put(SectionKeymap.ITEM_VOICE_INPUT, KeyEvent.KEYCODE_UNKNOWN);

View file

@ -83,7 +83,7 @@ public class UsageStatsScreen extends BaseScreenFragment {
private boolean deleteWordPairs(Preference ignored) {
DataStore.deleteWordPairs(
LanguageCollection.getAll(activity),
LanguageCollection.getAll(),
() -> UI.toastLongFromAsync(activity, "Word pairs deleted. You must reopen the screen manually.")
);
return true;

View file

@ -64,7 +64,7 @@ public class PreferenceDeletableWord extends ScreenPreference {
SettingsStore settings = new SettingsStore(getContext());
DataStore.deleteCustomWord(
this::onWordDeleted,
LanguageCollection.getLanguage(getContext(), settings.getInputLanguage()),
LanguageCollection.getLanguage(settings.getInputLanguage()),
word
);
}

View file

@ -32,6 +32,7 @@ public class HotkeysScreen extends BaseScreenFragment {
findPreference(SectionKeymap.ITEM_NEXT_LANGUAGE),
findPreference(SectionKeymap.ITEM_SELECT_KEYBOARD),
findPreference(SectionKeymap.ITEM_SHIFT),
findPreference(SectionKeymap.ITEM_SPACE_KOREAN),
findPreference(SectionKeymap.ITEM_SHOW_SETTINGS),
findPreference(SectionKeymap.ITEM_VOICE_INPUT),
};

View file

@ -25,6 +25,7 @@ public class SectionKeymap {
public static final String ITEM_NEXT_LANGUAGE = "key_next_language";
public static final String ITEM_SELECT_KEYBOARD = "key_select_keyboard";
public static final String ITEM_SHIFT = "key_shift";
public static final String ITEM_SPACE_KOREAN = "key_space_korean";
public static final String ITEM_SHOW_SETTINGS = "key_show_settings";
public static final String ITEM_VOICE_INPUT = "key_voice_input";
@ -155,7 +156,22 @@ public class SectionKeymap {
}
for (DropDownPreference item : items) {
if (item != null && !dropDown.getKey().equals(item.getKey()) && key.equals(item.getValue())) {
if (item == null) {
continue;
}
// "Shift" and "Korean Space" can be the same key. It is properly handled in HotkeyHandler.
if (
(
(dropDown.getKey().equals(ITEM_SHIFT) && item.getKey().equals(ITEM_SPACE_KOREAN))
|| (dropDown.getKey().equals(ITEM_SPACE_KOREAN) && item.getKey().equals(ITEM_SHIFT))
)
&& key.equals(item.getValue())
) {
continue;
}
if (!dropDown.getKey().equals(item.getKey()) && key.equals(item.getValue())) {
Logger.i("SectionKeymap.validateKey", "Key: '" + key + "' is already in use for function: " + item.getKey());
return false;
}

View file

@ -42,7 +42,7 @@ public class LanguageSelectionScreen extends BaseScreenFragment {
return;
}
ArrayList<Language> allLanguages = LanguageCollection.getAll(activity, true);
ArrayList<Language> allLanguages = LanguageCollection.getAll(true);
if (allLanguages.isEmpty()) {
return;
}

View file

@ -51,7 +51,7 @@ public class PreferenceSwitchLanguage extends SwitchPreferenceCompat {
);
// word count
WordFile wordFile = new WordFile(activity, language.getDictionaryFile(), activity.getAssets());
WordFile wordFile = new WordFile(activity, language, activity.getAssets());
summary
.append(", ")
.append(

View file

@ -31,7 +31,7 @@ class ItemExportDictionary extends ItemExportAbstract {
protected boolean onStartProcessing() {
return DictionaryExporter.getInstance()
.setLanguages(LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds()))
.setLanguages(LanguageCollection.getAll(activity.getSettings().getEnabledLanguageIds()))
.run(activity);
}

View file

@ -50,7 +50,7 @@ class ItemLoadDictionary extends ItemClickable {
private void onLoadingStatusChange(Bundle status) {
progressBar.show(activity, status);
progressBar.show(status);
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
if (progressBar.isCancelled()) {
@ -67,7 +67,7 @@ class ItemLoadDictionary extends ItemClickable {
@Override
protected boolean onClick(Preference p) {
ArrayList<Language> languages = LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds());
ArrayList<Language> languages = LanguageCollection.getAll(activity.getSettings().getEnabledLanguageIds());
setLoadingStatus();
if (!loader.load(activity, languages)) {

View file

@ -28,7 +28,7 @@ class ItemSelectLanguage {
}
item.setSummary(
LanguageCollection.toString(LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds(), true))
LanguageCollection.toString(LanguageCollection.getAll(activity.getSettings().getEnabledLanguageIds(), true))
);
}
}

View file

@ -29,7 +29,7 @@ class ItemTruncateAll extends ItemClickable {
@Override
protected boolean onClick(Preference p) {
onStartDeleting();
DataStore.deleteLanguages(this::onFinishDeleting, LanguageCollection.getAll(activity, false));
DataStore.deleteLanguages(this::onFinishDeleting, LanguageCollection.getAll(false));
return true;
}

View file

@ -25,7 +25,7 @@ class ItemTruncateUnselected extends ItemTruncateAll {
protected boolean onClick(Preference p) {
ArrayList<Language> unselectedLanguages = new ArrayList<>();
Set<Integer> selectedLanguageIds = new HashSet<>(activity.getSettings().getEnabledLanguageIds());
for (Language lang : LanguageCollection.getAll(activity, false)) {
for (Language lang : LanguageCollection.getAll(false)) {
if (!selectedLanguageIds.contains(lang.getId())) {
unselectedLanguages.add(lang);
}

View file

@ -36,7 +36,7 @@ class ItemPunctuationOrderLanguage extends ItemDropDown {
}
LinkedHashMap<String, String> values = new LinkedHashMap<>();
ArrayList<Language> languages = LanguageCollection.getAll(item.getContext(), settings.getEnabledLanguageIds(), true);
ArrayList<Language> languages = LanguageCollection.getAll(settings.getEnabledLanguageIds(), true);
if (languages.isEmpty()) {
return;
}

View file

@ -80,7 +80,7 @@ public class PunctuationScreen extends BaseScreenFragment {
restoreDefaults = new ItemRestoreDefaultPunctuation(activity.getSettings(), item, this::onLanguageChanged);
restoreDefaults
.setLanguage(LanguageCollection.getLanguage(activity, languageList.getValue()))
.setLanguage(LanguageCollection.getLanguage(languageList.getValue()))
.enableClickHandler();
}
@ -97,7 +97,7 @@ public class PunctuationScreen extends BaseScreenFragment {
private void onLanguageChanged(@Nullable String newLanguageId) {
Language language = LanguageCollection.getLanguage(activity, newLanguageId);
Language language = LanguageCollection.getLanguage(newLanguageId);
restoreDefaults.setLanguage(language);

View file

@ -37,6 +37,10 @@ class SettingsHacks extends BaseSettings {
/************* hack settings *************/
public boolean holdForPunctuationInKorean() {
return prefs.getBoolean("pref_hold_for_punctuation_in_korean", true);
}
public int getSuggestionScrollingDelay() {
boolean defaultOn = DeviceInfo.noTouchScreen(context) && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
return prefs.getBoolean("pref_alternative_suggestion_scrolling", defaultOn) ? 200 : 0;

View file

@ -66,6 +66,9 @@ class SettingsHotkeys extends SettingsHacks {
public int getKeyShift() {
return getFunctionKey(SectionKeymap.ITEM_SHIFT);
}
public int getKeySpaceKorean() {
return getFunctionKey(SectionKeymap.ITEM_SPACE_KOREAN);
}
public int getKeyShowSettings() {
return getFunctionKey(SectionKeymap.ITEM_SHOW_SETTINGS);
}

View file

@ -40,7 +40,7 @@ class SettingsInput extends SettingsHotkeys {
public Set<String> getEnabledLanguagesIdsAsStrings() {
Set<String> defaultLanguages = new HashSet<>(Collections.singletonList(
String.valueOf(LanguageCollection.getDefault(context).getId())
String.valueOf(LanguageCollection.getDefault().getId())
));
return new HashSet<>(prefs.getStringSet("pref_languages", defaultLanguages));
@ -51,7 +51,7 @@ class SettingsInput extends SettingsHotkeys {
Set<String> validLanguageIds = new HashSet<>();
for (String langId : languageIds) {
if (!Validators.validateInputLanguage(context, Integer.parseInt(langId), "saveEnabledLanguageIds")){
if (!Validators.validateInputLanguage(Integer.parseInt(langId), "saveEnabledLanguageIds")){
continue;
}
@ -69,12 +69,12 @@ class SettingsInput extends SettingsHotkeys {
public int getInputLanguage() {
return prefs.getInt("pref_input_language", LanguageCollection.getDefault(context).getId());
return prefs.getInt("pref_input_language", LanguageCollection.getDefault().getId());
}
public void saveInputLanguage(int language) {
if (Validators.validateInputLanguage(context, language, "saveInputLanguage")){
if (Validators.validateInputLanguage(language, "saveInputLanguage")){
prefsEditor.putInt("pref_input_language", language);
prefsEditor.apply();
}

View file

@ -88,6 +88,10 @@ class SettingsPunctuation extends SettingsInput {
orderedChars = language.getKeyCharacters(number);
}
if (number < 2) {
orderedChars = removeLettersFromList(orderedChars);
}
return orderedChars;
}
@ -114,4 +118,16 @@ class SettingsPunctuation extends SettingsInput {
return charsList;
}
private ArrayList<String> removeLettersFromList(ArrayList<String> list) {
ArrayList<String> cleanList = new ArrayList<>();
for (String s : list) {
if (!Character.isAlphabetic(s.codePointAt(0))) {
cleanList.add(s);
}
}
return cleanList;
}
}

View file

@ -28,7 +28,7 @@ public class SettingsUI extends SettingsTyping {
if (DeviceInfo.noKeyboard(context)) {
DEFAULT_LAYOUT = LAYOUT_NUMPAD;
} else if (DeviceInfo.noBackspaceKey(context) && !DeviceInfo.noTouchScreen(context)) {
} else if (DeviceInfo.noBackspaceKey() && !DeviceInfo.noTouchScreen(context)) {
DEFAULT_LAYOUT = LAYOUT_SMALL;
} else {
DEFAULT_LAYOUT = LAYOUT_TRAY;

View file

@ -1,7 +1,5 @@
package io.github.sspanak.tt9.preferences.settings;
import android.content.Context;
import java.util.ArrayList;
import java.util.Arrays;
@ -25,16 +23,16 @@ class Validators {
InputMode.CASE_CAPITALIZE
));
static boolean doesLanguageExist(Context context, int langId) {
return LanguageCollection.getLanguage(context, langId) != null;
static boolean doesLanguageExist(int langId) {
return LanguageCollection.getLanguage(langId) != null;
}
static boolean validateInputMode(int mode, String logTag, String logMsg) {
return Validators.isIntInList(mode, validInputModes, logTag, logMsg);
}
static boolean validateInputLanguage(Context context, int langId, String logTag) {
if (!doesLanguageExist(context, langId)) {
static boolean validateInputLanguage(int langId, String logTag) {
if (!doesLanguageExist(langId)) {
Logger.w(logTag, "Not saving invalid language with ID: " + langId);
return false;
}

View file

@ -35,7 +35,7 @@ public class AddWordDialog extends PopupDialog {
word = intent.getStringExtra(PARAMETER_WORD);
int languageId = intent.getIntExtra(PARAMETER_LANGUAGE, -1);
language = LanguageCollection.getLanguage(context, languageId);
language = LanguageCollection.getLanguage(languageId);
if (language == null) {
message = context.getString(R.string.add_word_invalid_language_x, languageId);

View file

@ -20,13 +20,13 @@ public class AutoUpdateMonologue extends PopupDialog {
AutoUpdateMonologue(@NonNull Context context, @NonNull Intent intent, ConsumerCompat<String> activityFinisher) {
super(context, activityFinisher);
parseIntent(context, intent);
parseIntent(intent);
}
private void parseIntent(@NonNull Context context, @NonNull Intent intent) {
private void parseIntent(@NonNull Intent intent) {
int languageId = intent.getIntExtra(PARAMETER_LANGUAGE, -1);
language = LanguageCollection.getLanguage(context, languageId);
language = LanguageCollection.getLanguage(languageId);
if (language == null) {
Logger.e(getClass().getSimpleName(), "Auto-updating is not possible. Intent parameter '" + PARAMETER_LANGUAGE + "' is invalid: " + languageId);

View file

@ -15,9 +15,12 @@ import java.util.Arrays;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.hacks.DeviceInfo;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
import io.github.sspanak.tt9.ui.main.keys.SoftKeyFn;
import io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber;
import io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber0;
import io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber1;
import io.github.sspanak.tt9.ui.main.keys.SoftKeyPunctuation;
import io.github.sspanak.tt9.ui.main.keys.SoftKeySettings;
@ -89,13 +92,14 @@ class MainLayoutNumpad extends BaseMainLayout {
@Override
void showTextEditingPalette() {
isTextEditingShown = true;
boolean notKorean = tt9 != null && !LanguageKind.isKorean(tt9.getLanguage());
for (SoftKey key : getKeys()) {
int keyId = key.getId();
if (keyId == R.id.soft_key_0) {
key.setEnabled(tt9 != null && !tt9.isInputModeNumeric());
} else if (key.getClass().equals(SoftKeyNumber.class)) {
key.setEnabled(tt9 != null && !tt9.isInputModeNumeric() && notKorean);
} else if (key.getClass().equals(SoftKeyNumber.class) || key instanceof SoftKeyNumber0 || key instanceof SoftKeyNumber1) {
key.setVisibility(View.GONE);
}
@ -115,7 +119,7 @@ class MainLayoutNumpad extends BaseMainLayout {
keyId == R.id.soft_key_add_word
|| keyId == R.id.soft_key_lf3
|| keyId == R.id.soft_key_lf4
|| keyId == R.id.soft_key_filter_suggestions
|| (keyId == R.id.soft_key_filter_suggestions && notKorean)
) {
key.setEnabled(false);
}
@ -127,7 +131,7 @@ class MainLayoutNumpad extends BaseMainLayout {
isTextEditingShown = false;
for (SoftKey key : getKeys()) {
if (key.getClass().equals(SoftKeyNumber.class) || key.getClass().equals(SoftKeyPunctuation.class)) {
if (key.getClass().equals(SoftKeyNumber.class) || key.getClass().equals(SoftKeyPunctuation.class) || key instanceof SoftKeyNumber0 || key instanceof SoftKeyNumber1) {
key.setVisibility(View.VISIBLE);
key.setEnabled(true);
}

View file

@ -27,7 +27,7 @@ public class SoftKeyAddWord extends SoftKey {
public void render() {
super.render();
if (tt9 != null) {
setEnabled(!tt9.isVoiceInputActive());
setEnabled(!tt9.isVoiceInputActive() && tt9.notLanguageSyllabary());
}
}
}

View file

@ -3,6 +3,7 @@ package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import io.github.sspanak.tt9.languages.LanguageKind;
import io.github.sspanak.tt9.ui.Vibration;
public class SoftKeyFilter extends SoftKey {
@ -10,11 +11,30 @@ public class SoftKeyFilter extends SoftKey {
public SoftKeyFilter(Context context, AttributeSet attrs) { super(context, attrs); }
public SoftKeyFilter(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
@Override protected float getTitleRelativeSize() { return super.getTitleRelativeSize() / 0.85f; }
@Override protected float getSubTitleRelativeSize() { return super.getSubTitleRelativeSize() / 0.85f; }
@Override
protected float getTitleRelativeSize() {
return isKorean() ? 1.1f : super.getTitleRelativeSize() / 0.85f;
}
@Override
protected float getSubTitleRelativeSize() {
return super.getSubTitleRelativeSize() / 0.85f;
}
private boolean isKorean() {
return tt9 != null && LanguageKind.isKorean(tt9.getLanguage());
}
@Override
protected void handleHold() {
if (isKorean()) {
handleRelease();
return;
}
preventRepeat();
if (validateTT9Handler() && tt9.onKeyFilterClear(false)) {
vibrate(Vibration.getHoldVibration());
@ -22,21 +42,30 @@ public class SoftKeyFilter extends SoftKey {
}
}
@Override
protected boolean handleRelease() {
return
validateTT9Handler()
&& tt9.onKeyFilterSuggestions(false, getLastPressedKey() == getId());
if (!validateTT9Handler()) {
return false;
}
if (isKorean()) {
return tt9.onKeySpaceKorean(false);
} else {
return tt9.onKeyFilterSuggestions(false, getLastPressedKey() == getId());
}
}
@Override
protected String getTitle() {
return "CLR";
return isKorean() ? "" : "CLR";
}
@Override
protected String getSubTitle() {
return "FLTR";
return isKorean() ? null : "FLTR";
}
@ -44,7 +73,12 @@ public class SoftKeyFilter extends SoftKey {
public void render() {
super.render();
if (tt9 != null) {
setEnabled(!tt9.isInputModeNumeric() && !tt9.isInputModeABC() && !tt9.isVoiceInputActive());
setEnabled(
!tt9.isInputModeNumeric()
&& !tt9.isInputModeABC()
&& !tt9.isVoiceInputActive()
&& (LanguageKind.isKorean(tt9.getLanguage()) || tt9.notLanguageSyllabary())
);
}
}
}

View file

@ -55,19 +55,7 @@ public class SoftKeyLF4 extends SwipeableKey {
}
protected String getPressIcon() {
if (tt9 == null || tt9.getLanguage() == null) {
return getContext().getString(R.string.virtual_key_input_mode);
}
if (tt9.isInputModeNumeric()) {
return "123";
}
if (tt9.isInputModeABC()) {
return tt9.getLanguage().getAbcString().toUpperCase(tt9.getLanguage().getLocale());
}
return "T9";
return tt9 != null ? tt9.getInputModeName() : getContext().getString(R.string.virtual_key_input_mode);
}
@Override

View file

@ -44,8 +44,6 @@ public class SoftKeyNumber extends SoftKey {
put(9, 3);
}};
private static final String PUNCTUATION_LABEL = ",:-)";
public SoftKeyNumber(Context context) {
super(context);
@ -71,16 +69,6 @@ public class SoftKeyNumber extends SoftKey {
}
@Override
protected float getSubTitleRelativeSize() {
if (tt9 != null && !tt9.isInputModeNumeric() && getNumber(getId()) == 0) {
return 1.1f;
}
return super.getSubTitleRelativeSize();
}
@Override
protected void handleHold() {
preventRepeat();
@ -140,30 +128,7 @@ public class SoftKeyNumber extends SoftKey {
@Override
protected String getSubTitle() {
if (tt9 == null) {
return null;
}
int number = getNumber(getId());
return switch (number) {
case 0 -> getSpecialCharList(tt9);
case 1 -> tt9.isNumericModeStrict() ? null : PUNCTUATION_LABEL;
default -> getKeyCharList(tt9, number);
};
}
private String getSpecialCharList(@NonNull TraditionalT9 tt9) {
if (tt9.isNumericModeSigned()) {
return "+/-";
} else if (tt9.isNumericModeStrict()) {
return null;
} else if (tt9.isInputModeNumeric()) {
return "+";
} else {
return "";
}
return tt9 == null ? null : getKeyCharList(tt9, getNumber(getId()));
}
@ -234,14 +199,16 @@ public class SoftKeyNumber extends SoftKey {
/**
* As suggested by the community, there is no need to display the accented letters.
* People are used to seeing just "ABC", "DEF", etc.
* People are used to seeing just "ABC", "DEF", etc. In the case of Korean, the keypad looks too
* cluttered, so we skip the double consonants, like on phones with a physical keypad.
*/
private boolean shouldSkipAccents(char currentLetter, boolean isGreek, boolean isLatinBased) {
return
currentLetter == 'ѝ'
|| currentLetter == 'ґ'
|| currentLetter == 'ς'
|| (currentLetter == 'ㄲ' || currentLetter == 'ㄸ' || currentLetter == 'ㅃ' || currentLetter == 'ㅆ' || currentLetter == 'ㅉ')
|| (isLatinBased && currentLetter > 'z')
|| currentLetter == 'ς'
|| (isGreek && (currentLetter < 'α' || currentLetter > 'ω'));
}
}

View file

@ -0,0 +1,74 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import io.github.sspanak.tt9.languages.LanguageKind;
public class SoftKeyNumber0 extends SoftKeyNumber {
private static final String CHARS_NUMERIC_MODE = "+%$";
public SoftKeyNumber0(Context context) { super(context); }
public SoftKeyNumber0(Context context, AttributeSet attrs) { super(context, attrs); }
public SoftKeyNumber0(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
@Override
protected String getTitle() {
if (tt9 == null) {
return super.getTitle();
}
if (tt9.isNumericModeStrict()) {
return "0";
} if (tt9.isNumericModeSigned()) {
return "+/-";
} else if (tt9.isInputModePhone()) {
return "+";
} else if (tt9.isInputModeNumeric() || LanguageKind.isKorean(tt9.getLanguage())) {
return CHARS_NUMERIC_MODE;
}
return super.getTitle();
}
@Override
protected String getSubTitle() {
if (tt9 == null) {
return null;
}
if (tt9.isNumericModeStrict()) {
return null;
} else if (tt9.isInputModeNumeric()) {
return "0";
} else if (LanguageKind.isKorean(tt9.getLanguage())) {
return getKoreanCharList();
} else {
return "";
}
}
private String getKoreanCharList() {
if (tt9 == null || tt9.getLanguage() == null) {
return null;
}
StringBuilder list = new StringBuilder();
for (String character : tt9.getLanguage().getKeyCharacters(0)) {
if (Character.isAlphabetic(character.charAt(0))) {
list.append(character);
}
}
return list.toString();
}
@Override
protected float getSubTitleRelativeSize() {
if (tt9 != null && !tt9.isInputModeNumeric() && !LanguageKind.isKorean(tt9.getLanguage())) {
return 1.1f;
}
return super.getSubTitleRelativeSize();
}
}

View file

@ -0,0 +1,48 @@
package io.github.sspanak.tt9.ui.main.keys;
import android.content.Context;
import android.util.AttributeSet;
import io.github.sspanak.tt9.languages.LanguageKind;
public class SoftKeyNumber1 extends SoftKeyNumber {
private static final String DEFAULT_LARGE_LABEL = ",:-)";
private static final String KOREAN_SMALL_LABEL = "1 :-)";
private static final String KOREAN_LARGE_LABEL = "";
public SoftKeyNumber1(Context context) { super(context); }
public SoftKeyNumber1(Context context, AttributeSet attrs) { super(context, attrs); }
public SoftKeyNumber1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
@Override
protected String getTitle() {
if (tt9 == null) {
return super.getTitle();
}
if (tt9.isInputModeNumeric() && !tt9.isNumericModeStrict()) {
return DEFAULT_LARGE_LABEL;
} else if (LanguageKind.isKorean(tt9.getLanguage())) {
return KOREAN_SMALL_LABEL;
} else {
return "1";
}
}
@Override
protected String getSubTitle() {
if (tt9 == null || tt9.isNumericModeStrict()) {
return null;
}
if (tt9.isNumericModeStrict()) {
return null;
} else if (tt9.isInputModeNumeric()) {
return "1";
} else if (LanguageKind.isKorean(tt9.getLanguage())) {
return KOREAN_LARGE_LABEL;
} else {
return DEFAULT_LARGE_LABEL;
}
}
}

View file

@ -57,13 +57,7 @@ public class SoftKeyRF3 extends SoftKey {
@Override
protected String getTitle() {
if (isTextEdtingActive()) {
if (tt9 == null) {
return "ABC";
} else if (tt9.isInputModeNumeric()) {
return "123";
} else if (tt9.getLanguage() != null) {
return tt9.getLanguage().getAbcString().toUpperCase(tt9.getLanguage().getLocale());
}
return tt9 == null ? "ABC" : tt9.getABCString();
}
return isTextEditingMissing() && !isVoiceInputMissing() ? "🎤" : getContext().getString(R.string.virtual_key_text_editing).toUpperCase();

View file

@ -64,7 +64,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
}
public void show(Context context, Bundle data) {
public void show(Bundle data) {
String error = data.getString("error", null);
int fileCount = data.getInt("fileCount", -1);
int progress = data.getInt("progress", -1);
@ -72,7 +72,6 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
if (error != null) {
hasFailed = true;
showError(
context,
error,
data.getInt("languageId", -1),
data.getLong("fileLine", -1)
@ -84,7 +83,6 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
}
showProgress(
context,
data.getLong("time", 0),
data.getInt("currentFile", 0),
data.getInt("progress", 0),
@ -94,8 +92,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
}
private String generateTitle(Context context, int languageId) {
Language lang = LanguageCollection.getLanguage(context, languageId);
private String generateTitle(int languageId) {
Language lang = LanguageCollection.getLanguage(languageId);
if (lang != null) {
return resources.getString(R.string.dictionary_loading, lang.getName());
@ -105,7 +103,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
}
private void showProgress(Context context, long time, int currentFile, int currentFileProgress, int languageId) {
private void showProgress(long time, int currentFile, int currentFileProgress, int languageId) {
if (currentFileProgress <= 0) {
hide();
isStopped = true;
@ -119,12 +117,12 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
if (progress >= maxProgress) {
progress = maxProgress = 0;
title = generateTitle(context, -1);
title = generateTitle(-1);
String timeFormat = time > 60000 ? " (%1.0fs)" : " (%1.1fs)";
message = resources.getString(R.string.completed) + String.format(Locale.ENGLISH, timeFormat, time / 1000.0);
} else {
title = generateTitle(context, languageId);
title = generateTitle(languageId);
message = currentFileProgress + "%";
}
@ -132,8 +130,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
}
private void showError(Context context, String errorType, int langId, long line) {
Language lang = LanguageCollection.getLanguage(context, langId);
private void showError(String errorType, int langId, long line) {
Language lang = LanguageCollection.getLanguage(langId);
if (lang == null || errorType.equals(InvalidLanguageException.class.getSimpleName())) {
message = resources.getString(R.string.add_word_invalid_language);
@ -147,7 +145,7 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
message = resources.getString(R.string.dictionary_load_error, lang.getName(), errorType);
}
title = generateTitle(context, -1);
title = generateTitle(-1);
progress = maxProgress = 0;
renderError();

View file

@ -47,6 +47,10 @@ public class Characters {
",", ".", "-", "«", "»", "(", ")", "&", "~", "`", "'", "\"", "·", ":", "!", GR_QUESTION_MARK
));
final public static ArrayList<String> PunctuationKorean = new ArrayList<>(Arrays.asList(
",", ".", "~", "1", "(", ")", "&", "-", "`", ";", ":", "'", "\"", "!", "?"
));
final public static ArrayList<String> Currency = new ArrayList<>(Arrays.asList(
"$", "", "", "", "", "¢", "¤", "", "", "¥", "", "£"
));
@ -107,15 +111,6 @@ public class Characters {
))
));
public static boolean isStaticEmoji(String emoji) {
for (ArrayList<String> group : Emoji) {
if (group.contains(emoji)) {
return true;
}
}
return false;
}
public static boolean isGraphic(char ch) {
return !(ch < 256 || Character.isLetterOrDigit(ch) || Character.isAlphabetic(ch));
}

View file

@ -10,6 +10,7 @@ public class TextTools {
private static final Pattern containsOtherThan1 = Pattern.compile("[02-9]");
private static final Pattern combiningString = Pattern.compile("^\\p{M}+$");
private static final Pattern nextIsPunctuation = Pattern.compile("^\\p{Punct}");
private static final Pattern isHangul = Pattern.compile("[\u1100-\u11FF\u302E-\u302F\u3131-\u318F\u3200-\u321F\u3260-\u327E\uA960-\uA97F\uAC00-\uD7FB\uFFA0-\uFFDF]+");
private static final Pattern nextToWord = Pattern.compile("\\b$");
private static final Pattern previousIsLetter = Pattern.compile("\\p{L}$");
private static final Pattern startOfSentence = Pattern.compile("(?<!\\.)(^|[.?!؟¿¡])\\s+$");
@ -40,6 +41,11 @@ public class TextTools {
}
public static boolean isHangul(String str) {
return str != null && isHangul.matcher(str).find();
}
public static boolean isStartOfSentence(String str) {
return str != null && startOfSentence.matcher(str).find();
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber1
android:id="@+id/soft_key_1"
style="@android:style/Widget.Holo.Button.Borderless"
android:layout_width="0dp"

View file

@ -8,7 +8,7 @@
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber
<io.github.sspanak.tt9.ui.main.keys.SoftKeyNumber0
android:id="@+id/soft_key_0"
style="@android:style/Widget.Holo.Button.Borderless"
android:layout_width="0dp"

View file

@ -74,6 +74,7 @@
<string name="donate_title">Дарете</string>
<string name="donate_summary">Ако харесвате %1$s, черпете една бира на: %2$s.</string>
<string name="function_edit_text">Редактиране на текст</string>
<string name="function_add_word_not_available">Добавянето на думи не е възможно на този език.</string>
<string name="function_backspace">Триене на текст</string>
<string name="dictionary_no_notifications">Речникови известия</string>
<string name="dictionary_no_notifications_summary">Получавайте известия за обновления на речника и за прогреса при зареждане.</string>
@ -81,6 +82,7 @@
<string name="function_show_command_palette">Списък с команди</string>
<string name="function_filter_clear">Изчистване на филтър</string>
<string name="function_filter_suggestions">Филтриране на думи</string>
<string name="function_filter_suggestions_not_available">Филтрирането не е възможно на този език.</string>
<string name="function_previous_suggestion">Предишна дума</string>
<string name="function_next_suggestion">Следваща дума</string>
<string name="function_next_language">Следващ eзик</string>

View file

@ -66,6 +66,7 @@
<string name="pref_category_function_keys">Tastenkürzel</string>
<string name="pref_category_keypad">Tastenfeld</string>
<string name="char_space">Leerzeichen</string>
<string name="function_add_word_not_available">Das Hinzufügen von Wörtern ist in dieser Sprache nicht möglich.</string>
<string name="function_backspace">Rücktaste</string>
<string name="setup_keyboard_status">Status</string>
<string name="setup_default_keyboard">Standardtastatur auswählen</string>
@ -174,4 +175,5 @@
<string name="punctuation_order_save">Reihenfolge speichern</string>
<string name="punctuation_order_forbidden_char">Verbotenes Zeichen:%1$s</string>
<string name="punctuation_order_forbidden_chars">Verbotene Zeichen:%1$s</string>
<string name="function_filter_suggestions_not_available">Das Filtern ist in dieser Sprache nicht möglich.</string>
</resources>

View file

@ -35,6 +35,7 @@
<string name="pref_dark_theme">Tema oscuro</string>
<string name="char_space">Espacio</string>
<string name="dictionary_truncating">Borrando…</string>
<string name="function_add_word_not_available">No es posible agregar palabras en este idioma.</string>
<string name="function_backspace">Retroceso</string>
<string name="setup_keyboard_status">Estado</string>
<string name="setup_default_keyboard">Selecciona teclado predeterminado</string>
@ -81,6 +82,7 @@
<string name="function_show_command_palette">Lista de comandos</string>
<string name="function_filter_clear">Limpiar el filtro</string>
<string name="function_filter_suggestions">Filtrar sugerencias</string>
<string name="function_filter_suggestions_not_available">No es posible filtrar en este idioma.</string>
<string name="function_previous_suggestion">Sugerencia previa</string>
<string name="function_next_suggestion">Sugerencia siguiente</string>
<string name="function_next_language">Idioma siguiente</string>

View file

@ -67,12 +67,14 @@
<string name="pref_auto_text_case_summary">Commencer automatiquement les phrases avec une majuscule.</string>
<string name="pref_category_keypad">Clavier</string>
<string name="char_space">Espace</string>
<string name="function_add_word_not_available">L\'ajout de mots n\'est pas possible dans cette langue.</string>
<string name="function_backspace">Retour arrière</string>
<string name="dictionary_no_notifications">Notifications du dictionnaire</string>
<string name="dictionary_no_notifications_summary">Recevoir des notifications sur les mises à jour du dictionnaire et sur la progression du chargement.</string>
<string name="function_show_command_palette">Liste des commandes</string>
<string name="function_filter_clear">Supprimer le filtre</string>
<string name="function_filter_suggestions">Filtrer les mots</string>
<string name="function_filter_suggestions_not_available">Le filtrage n\'est pas possible dans cette langue.</string>
<string name="function_previous_suggestion">Mot précédent</string>
<string name="function_next_suggestion">Mot suivant</string>
<string name="function_next_language">Langue suivante</string>

View file

@ -67,6 +67,7 @@
<string name="dictionary_load_cancelled">Caricamento annullato.</string>
<string name="pref_category_keypad">Tastiera</string>
<string name="char_space">Spazio</string>
<string name="function_add_word_not_available">Aggiungere parole non è possibile in questa lingua.</string>
<string name="function_backspace">Backspace</string>
<string name="setup_keyboard_status">Stato</string>
<string name="setup_default_keyboard">Seleziona la tastiera predefinita</string>
@ -174,5 +175,6 @@
<string name="punctuation_order_save">Salvare l\'ordine</string>
<string name="punctuation_order_forbidden_char">Carattere vietato:%1$s</string>
<string name="punctuation_order_forbidden_chars">Caratteri vietati:%1$s</string>
<string name="function_filter_suggestions_not_available">Il filtraggio non è possibile in questa lingua.</string>
</resources>

View file

@ -77,6 +77,7 @@
<string name="dictionary_truncated">המילון נמחק בהצלחה.</string>
<string name="dictionary_truncating">המחיקה מתבצעת…</string>
<string name="function_add_word_not_available">אין אפשרות להוסיף מילים בשפה זו.</string>
<string name="function_backspace">לחצן מחיקה</string>
<string name="function_next_language">לחצן למעבר לשפה הבאה</string>
<string name="function_next_mode">לחצן מצב קלט</string>
@ -187,4 +188,5 @@
<string name="punctuation_order_save">שמור את הסדר</string>
<string name="punctuation_order_forbidden_char">תו אסור:%1$s</string>
<string name="punctuation_order_forbidden_chars">תווים אסורים:%1$s</string>
<string name="function_filter_suggestions_not_available">לא ניתן לסנן בשפה זו.</string>
</resources>

View file

@ -77,12 +77,14 @@
<string name="dictionary_truncated">Žodynas sėkmingai ištrintas.</string>
<string name="dictionary_truncating">Ištrinama…</string>
<string name="function_add_word_not_available">Žodžių pridėjimas šia kalba nėra galimas.</string>
<string name="function_backspace">Trinti</string>
<string name="dictionary_no_notifications">Žodyno pranešimai</string>
<string name="dictionary_no_notifications_summary">Gaukite pranešimus apie žodynų atnaujinimus ir įkėlimo progresą.</string>
<string name="function_show_command_palette">Rodyti komandų sąrašą</string>
<string name="function_filter_clear">Panaikinti filtrą</string>
<string name="function_filter_suggestions">Filtruoti pasiūlymus</string>
<string name="function_filter_suggestions_not_available">Filtravimas šia kalba nėra galimas.</string>
<string name="function_previous_suggestion">Ankstesnis pasiūlytas žodis</string>
<string name="function_next_suggestion">Sekantis pasiūlytas žodis</string>
<string name="function_next_language">Rašymo kalba</string>

View file

@ -68,6 +68,7 @@
<string name="pref_category_function_keys">Sneltoetsen</string>
<string name="pref_category_keypad">Toetsenbord</string>
<string name="char_space">Spatie</string>
<string name="function_add_word_not_available">Het toevoegen van woorden is niet mogelijk in deze taal.</string>
<string name="function_backspace">Backspace</string>
<string name="setup_keyboard_status">Status</string>
<string name="setup_default_keyboard">Standaardtoetsenbord selecteren</string>
@ -173,4 +174,5 @@
<string name="punctuation_order_save">Volgorde opslaan</string>
<string name="punctuation_order_forbidden_char">Verboden teken:%1$s</string>
<string name="punctuation_order_forbidden_chars">Verboden tekens:%1$s</string>
<string name="function_filter_suggestions_not_available">Het filteren is niet mogelijk in deze taal.</string>
</resources>

View file

@ -72,6 +72,7 @@
<string name="dictionary_truncate_title">Limpar Dicionário</string>
<string name="dictionary_truncated">Dicionário apagado com sucesso.</string>
<string name="function_add_word_not_available">Não é possível adicionar palavras neste idioma.</string>
<string name="function_backspace">Backspace</string>
<string name="function_next_language">Próximo Idioma</string>
<string name="function_next_mode">Modo de Entrada</string>
@ -187,4 +188,5 @@
<string name="punctuation_order_save">Salvar ordem</string>
<string name="punctuation_order_forbidden_char">Caractere proibido:%1$s</string>
<string name="punctuation_order_forbidden_chars">Caracteres proibidos:%1$s</string>
<string name="function_filter_suggestions_not_available">Não é possível filtrar neste idioma.</string>
</resources>

View file

@ -67,12 +67,14 @@
<string name="pref_auto_text_case_summary">Автоматически начинать предложение с заглавной буквы.</string>
<string name="pref_double_zero_char">Символ при двойном нажатии клавиши 0</string>
<string name="dictionary_load_bad_char">Не удалось загрузить словарь. Проблема в слове в строке %1$d для языка «%2$s».</string>
<string name="function_add_word_not_available">Добавление слов невозможно на этом языке.</string>
<string name="function_backspace">Стереть</string>
<string name="dictionary_no_notifications">Уведомления словаря</string>
<string name="dictionary_no_notifications_summary">Получать уведомления о обновлениях словаря и о процессе загрузки.</string>
<string name="function_show_command_palette">Список команд</string>
<string name="function_filter_clear">Удалить фильтр</string>
<string name="function_filter_suggestions">Фильтровать слова</string>
<string name="function_filter_suggestions_not_available">Фильтрация невозможна на этом языке.</string>
<string name="function_previous_suggestion">Предыдущее слово</string>
<string name="function_next_suggestion">Следующее слово</string>
<string name="function_next_language">Следующий язык</string>

View file

@ -65,6 +65,7 @@
<string name="pref_category_function_keys">Klavye Kısayolları</string>
<string name="pref_category_keypad">Tuş Takımı</string>
<string name="char_space">Boşluk</string>
<string name="function_add_word_not_available">Bu dilde kelime eklemek mümkün değil.</string>
<string name="function_backspace">Geri Tuşu</string>
<string name="setup_keyboard_status">Durum</string>
<string name="setup_default_keyboard">Varsayılan Klavyeyi Seçin</string>
@ -136,6 +137,7 @@
<string name="function_show_command_palette">Komut listesini göster</string>
<string name="function_filter_clear">Filtre Temizle</string>
<string name="function_filter_suggestions">Tahminleri Filtrele</string>
<string name="function_filter_suggestions_not_available">Bu dilde filtreleme mümkün değil.</string>
<string name="function_previous_suggestion">Önceki Tahmin</string>
<string name="function_next_suggestion">Sonraki Tahmin</string>
<string name="function_next_language">Sonraki Dil</string>

View file

@ -110,12 +110,14 @@
<string name="donate_title">Підтримати</string>
<string name="donate_summary">Якщо вам подобається %1$s, ви можете пригостити мене пивом за адресою: %2$s.</string>
<string name="function_add_word_not_available">Додавання слів неможливе цією мовою.</string>
<string name="function_backspace">Стерти</string>
<string name="dictionary_no_notifications">Сповіщення словника</string>
<string name="dictionary_no_notifications_summary">Отримувати повідомлення про оновлення словника та процес завантаження.</string>
<string name="function_show_command_palette">Список команд</string>
<string name="function_filter_clear">Очистити фільтр</string>
<string name="function_filter_suggestions">Фільтрувати пропозиції</string>
<string name="function_filter_suggestions_not_available">Фільтрація неможлива цією мовою.</string>
<string name="function_previous_suggestion">Попередня пропозиція</string>
<string name="function_next_suggestion">Наступна пропозиція</string>
<string name="function_next_language">Наступна мова</string>

View file

@ -144,11 +144,13 @@
<string name="donate_url_short" translatable="false">www.buymeacoffee.com</string>
<string name="function_add_word">Add Word</string>
<string name="function_add_word_not_available">Adding words is not possible in this language.</string>
<string name="function_backspace">Backspace</string>
<string name="function_show_command_palette">Show Command List</string>
<string name="function_filter_clear">Clear Filter</string>
<string name="function_edit_text">Edit Text</string>
<string name="function_filter_suggestions">Filter Suggestions</string>
<string name="function_filter_suggestions_not_available">Filtering is not possible in this language.</string>
<string name="function_previous_suggestion">Previous Suggestion</string>
<string name="function_next_suggestion">Next Suggestion</string>
<string name="function_next_language">Next Language</string>
@ -221,6 +223,7 @@
<string name="virtual_key_input_mode" translatable="false">Mode</string>
<string name="virtual_key_settings" translatable="false">Cfg</string>
<string name="virtual_key_shift" translatable="false">Shift</string>
<string name="virtual_key_space_korean">Space (Korean)</string>
<string name="virtual_key_text_editing" translatable="false">Copy</string>
<string name="voice_input_listening">Speak</string>

View file

@ -19,6 +19,13 @@
<DropDownPreference
app:key="pref_input_handling_mode"
app:title="Keypad Handling Mode" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:key="pref_hold_for_punctuation_in_korean"
app:title="Hold to type special chars in Korean"
app:summaryOff="Type special chars by multi-pressing 1-key or 0-key"
app:summaryOn="Type special chars by holding 1-key or 0-key" />
</PreferenceCategory>
<PreferenceCategory app:title="Logging" app:singleLineTitle="true">

View file

@ -50,6 +50,10 @@
app:key="key_shift"
app:title="@string/virtual_key_shift" />
<DropDownPreference
app:key="key_space_korean"
app:title="@string/virtual_key_space_korean" />
<DropDownPreference
app:key="key_show_settings"
app:title="@string/function_show_settings" />

View file

@ -60,6 +60,7 @@ ext.parseLanguageDefintion = { File languageFile, String dictionariesDir ->
alphabet = languageFile.name.contains("Catalan") ? '·' : alphabet
alphabet = languageFile.name.contains("Hebrew") || languageFile.name.contains("Yiddish") ? '"' : alphabet
alphabet = languageFile.name.contains("Korean") ? '' : alphabet
for (String line : languageFile.readLines()) {
if (
@ -292,7 +293,7 @@ static def validateWord(String word, String validCharacters, boolean isAlphabeti
errors += "${errorMsgPrefix}. Found numbers on line ${lineNumber}. Remove all numbers.\n"
}
if (word.matches("^\\P{L}+\$")) {
if (word.matches("^\\P{L}+\$") && !validCharacters.contains(word)) {
errorCount++
errors += "${errorMsgPrefix}. Found a garbage word: '${word}' on line ${lineNumber}.\n"
}