Korean (#671)
* 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:
parent
f3c701fd55
commit
5a108dcda9
107 changed files with 13010 additions and 609 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
72
app/languages/definitions/Korean.yml
Normal file
72
app/languages/definitions/Korean.yml
Normal 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]
|
||||
11248
app/languages/dictionaries/ko-utf8.csv
Normal file
11248
app/languages/dictionaries/ko-utf8.csv
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 > 'ω'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue